diff --git a/doc/README.monitoring_tables b/doc/README.monitoring_tables index 70dff43e0ef..b0a476d05d7 100644 --- a/doc/README.monitoring_tables +++ b/doc/README.monitoring_tables @@ -242,6 +242,10 @@ Monitoring tables 4: call - MON$TABLE_NAME (table name) - MON$RECORD_STAT_ID (record-level statistics ID, refers to MON$RECORD_STATS) + - MON$TABLE_TYPE (table type) + PERSISTENT: Regular persistent table + GLOBAL TEMPORARY: Global temporary table + LOCAL TEMPORARY: Local temporary table MON$COMPILED_STATEMENTS (compiled statements) - MON$COMPILED_STATEMENT_ID (compiled statement ID) diff --git a/doc/sql.extensions/README.ddl.txt b/doc/sql.extensions/README.ddl.txt index 6bbbbdffb0a..bb11f8c1bbe 100644 --- a/doc/sql.extensions/README.ddl.txt +++ b/doc/sql.extensions/README.ddl.txt @@ -691,7 +691,7 @@ The following statements are supported: CREATE EXCEPTION [IF NOT EXISTS] ... CREATE INDEX [IF NOT EXISTS] ... CREATE PROCEDURE [IF NOT EXISTS] ... -CREATE TABLE [IF NOT EXISTS] ... +CREATE [{GLOBAL | LOCAL} TEMPORARY TABLE] TABLE [IF NOT EXISTS] ... CREATE TRIGGER [IF NOT EXISTS] ... CREATE VIEW [IF NOT EXISTS] ... CREATE FILTER [IF NOT EXISTS] ... diff --git a/doc/sql.extensions/README.local_temporary_tables.md b/doc/sql.extensions/README.local_temporary_tables.md new file mode 100644 index 00000000000..3ceb2dbab0c --- /dev/null +++ b/doc/sql.extensions/README.local_temporary_tables.md @@ -0,0 +1,290 @@ +# Created Local Temporary Tables (FB 6.0) + +Firebird 6.0 introduces support for SQL Created Local Temporary Tables (LTT). Unlike Global Temporary Tables (GTT), +which have permanent metadata stored in the system catalogue, Local Temporary Tables exist only within the connection +that created them. The table definition is private to the creating connection and is automatically discarded when the +connection ends. The data lifecycle depends on the `ON COMMIT` clause: with `ON COMMIT DELETE ROWS` (the default), data +is private to each transaction and deleted when the transaction ends; with `ON COMMIT PRESERVE ROWS`, data is shared +across all transactions in the connection and persists until the connection ends. + +## Why Local Temporary Tables? + +Local Temporary Tables are useful in scenarios where you need temporary storage without affecting the database metadata: + +### Temporary storage in read-only databases + +Since LTT definitions are not stored in the database, they can be created and used in read-only databases. This is not +possible with Global Temporary Tables, which require metadata modifications. + +### Session-private definitions + +Each connection can create its own temporary tables with the same names without conflicts. The table definitions are +completely isolated between connections. + +### Ad-hoc temporary storage + +LTTs provide a quick way to create temporary storage during a session for intermediate results, data transformations, +or other temporary processing needs without leaving any trace in the database after disconnection. + +## Comparison with Global Temporary Tables + +| Feature | GTT | LTT | +|-----------------------------|--------------------------------------|---------------------------------------| +| Metadata storage | System tables (`RDB$RELATIONS`, etc.)| Connection memory only | +| Visibility of definition | All connections | Creating connection only | +| Persistence of definition | Permanent (until explicitly dropped) | Until connection ends | +| Read-only database support | No | Yes | +| Schema support | Yes | Yes | +| Indexes | Full support | Basic support (no expression/partial) | +| DDL triggers | Fire on CREATE/DROP/ALTER | Do not fire | +| DML triggers support | Yes | Not supported | +| Constraints (PK, FK, CHECK) | Supported | Not supported | +| Explicit privileges | Supported | Not supported | + +## Syntax + +### CREATE LOCAL TEMPORARY TABLE + +```sql +{CREATE | RECREATE} LOCAL TEMPORARY TABLE [IF NOT EXISTS] [.] + () + [ON COMMIT {DELETE | PRESERVE} ROWS] +``` + +The `ON COMMIT` clause determines the data lifecycle: + +- `ON COMMIT DELETE ROWS` (default): Data is private to each transaction and deleted when the transaction ends. +- `ON COMMIT PRESERVE ROWS`: Data persists until the connection ends. + +Example: + +```sql +-- Create a simple LTT with default ON COMMIT DELETE ROWS +create local temporary table temp_results ( + id integer not null, + val varchar(100) +); + +-- Create an LTT that preserves data across transactions +create local temporary table session_cache ( + key varchar(50) not null, + data blob +) on commit preserve rows; + +-- Create an LTT in a specific schema +create local temporary table my_schema.work_table ( + x integer, + y integer +); +``` + +### ALTER TABLE + +Local Temporary Tables support the following `ALTER TABLE` operations: + +```sql +-- Add a column +ALTER TABLE ADD [NOT NULL]; + +-- Drop a column +ALTER TABLE DROP ; + +-- Rename a column +ALTER TABLE ALTER COLUMN TO ; + +-- Change column position +ALTER TABLE ALTER COLUMN POSITION ; + +-- Change nullability +ALTER TABLE ALTER COLUMN {DROP | SET} NOT NULL; + +-- Change column type +ALTER TABLE ALTER COLUMN TYPE ; +``` + +Example: + +```sql +create local temporary table temp_data (id integer); + +alter table temp_data add name varchar(50) not null; +alter table temp_data alter column name position 1; +alter table temp_data alter column name to full_name; +alter table temp_data alter column id type bigint; +``` + +### DROP TABLE + +```sql +DROP TABLE [IF EXISTS] [.] +``` + +The same `DROP TABLE` statement is used for both regular tables and Local Temporary Tables. If the table name matches +an LTT in the current connection, the LTT is dropped. + +### CREATE INDEX + +```sql +CREATE [UNIQUE] [ASC[ENDING] | DESC[ENDING]] INDEX [IF NOT EXISTS] [.] + ON [.] () +``` + +Example: + +```sql +create local temporary table temp_orders ( + order_id integer not null, + customer_id integer, + order_date date +); + +create unique index idx_temp_orders_pk on temp_orders (order_id); +create descending index idx_temp_orders_date on temp_orders (order_date); +create index idx_temp_orders_cust on temp_orders (customer_id); +``` + +### ALTER INDEX + +```sql +ALTER INDEX {ACTIVE | INACTIVE} +``` + +Indexes on LTTs can be deactivated and reactivated, which may be useful when performing bulk inserts in +`ON COMMIT PRESERVE ROWS` tables. + +### DROP INDEX + +```sql +DROP INDEX [IF EXISTS] +``` + +## Limitations + +Local Temporary Tables have the following restrictions: + +### Column restrictions + +- **DEFAULT values**: Columns cannot have default values. +- **COMPUTED BY columns**: Computed columns are not supported. +- **IDENTITY columns**: Identity (auto-increment) columns are not supported. +- **ARRAY types**: Array type columns are not supported. +- **Domain changes**: Columns can be defined using domains, but changes to the domain definition are not propagated to + existing LTT columns. The column retains the domain's characteristics as they were at the time the column was created. + +### Constraint restrictions + +- **PRIMARY KEY**: Primary key constraints are not supported. +- **FOREIGN KEY**: Foreign key constraints are not supported. +- **CHECK constraints**: Check constraints are not supported. +- **NOT NULL constraints**: Only unnamed NOT NULL constraints are supported. + +### Index restrictions + +- **Expression-based indexes**: Indexes based on expressions are not supported. +- **Partial indexes**: Partial (filtered) indexes are not supported. + +### Other restrictions + +- **EXTERNAL FILE**: LTTs cannot be linked to external files. +- **SQL SECURITY clause**: The SQL SECURITY clause is not applicable to LTTs. +- **Max number of LTTs**: The maximum number of active Local Temporary Tables per connection is 1024. +- **Persistent metadata references**: LTTs cannot be directly referenced in stored procedures, triggers, views, or + other persistent database objects. Attempting to do so will raise an error. However, LTTs can be accessed via + `EXECUTE STATEMENT` inside persistent objects, since the SQL text is parsed at runtime. + +## Data Lifecycle + +### ON COMMIT DELETE ROWS + +When a Local Temporary Table is created with `ON COMMIT DELETE ROWS` (the default), data is private to each transaction +and automatically deleted when the transaction ends, whether by commit or rollback. Since Firebird supports multiple +concurrent transactions within the same connection, each transaction has its own isolated view of the data. + +```sql +create local temporary table temp_work (id integer); + +insert into temp_work values (1); +insert into temp_work values (2); +select count(*) from temp_work; -- Returns 2 + +commit; + +select count(*) from temp_work; -- Returns 0 (data was deleted) +``` + +### ON COMMIT PRESERVE ROWS + +When created with `ON COMMIT PRESERVE ROWS`, data persists across transaction boundaries and remains available until +the connection ends. + +```sql +create local temporary table session_data (id integer) on commit preserve rows; + +insert into session_data values (1); +commit; + +insert into session_data values (2); +commit; + +select count(*) from session_data; -- Returns 2 (data preserved across commits) +``` + +### COMMIT/ROLLBACK RETAINING + +Similar to Global Temporary Tables, `COMMIT RETAINING` and `ROLLBACK RETAINING` preserve the data in LTTs with +`ON COMMIT DELETE ROWS`. + +## Transactional DDL + +DDL operations on Local Temporary Tables behave the same as DDL on persistent tables with respect to transactions. +Changes to the table structure (CREATE, ALTER, DROP) are only made permanent when the transaction commits. If the +transaction is rolled back, any LTT structural changes made within that transaction are undone. + +Savepoints are also supported. If a savepoint is rolled back, any LTT changes made after that savepoint are undone. + +```sql +set autoddl off; -- ISQL feature + +create local temporary table t1 (id integer); + +savepoint sp1; + +alter table t1 add name varchar(50); + +rollback to savepoint sp1; + +-- The ALTER TABLE is undone; column 'name' does not exist +``` + +This applies to all DDL operations on LTTs. + +## Schema Integration + +Local Temporary Tables are schema-bound objects, just like regular tables. They follow the same schema resolution +rules: + +- When creating an LTT without a schema qualifier, it is created in the current schema (the first valid schema in the + search path). +- When referencing an LTT without a schema qualifier, the search path is used to resolve the name. +- Index names for LTTs must be unique within the schema and cannot conflict with persistent index names. + +```sql +set search_path to my_schema; + +-- Creates LTT in my_schema +create local temporary table temp_data (id integer); + +-- References my_schema.temp_data +select * from temp_data; + +-- Explicit schema qualification +create local temporary table other_schema.temp_work (x integer); +``` + +## Implementation Notes + +Local Temporary Tables store their data and indexes in temporary files, similar to Global Temporary Tables. Each +connection has its own temporary file space for LTT data. + +When a connection ends (either normally or due to an error), all Local Temporary Tables created by that connection +are automatically discarded along with their data. No explicit cleanup is required. diff --git a/src/dsql/DdlNodes.epp b/src/dsql/DdlNodes.epp index 9097c30658e..c5065cee1d1 100644 --- a/src/dsql/DdlNodes.epp +++ b/src/dsql/DdlNodes.epp @@ -37,7 +37,9 @@ #include "../common/os/path_utils.h" #include "../jrd/CryptoManager.h" #include "../jrd/IntlManager.h" +#include "../jrd/LocalTemporaryTable.h" #include "../jrd/PreparedStatement.h" +#include "../jrd/Savepoint.h" #include "../jrd/ResultSet.h" #include "../jrd/UserManagement.h" #include "../jrd/blb_proto.h" @@ -46,6 +48,7 @@ #include "../jrd/dpm_proto.h" #include "../jrd/dyn_ut_proto.h" #include "../jrd/exe_proto.h" +#include "../jrd/idx_proto.h" #include "../jrd/intl_proto.h" #include "../common/isc_f_proto.h" #include "../jrd/lck_proto.h" @@ -64,6 +67,9 @@ #include "../auth/SecureRemotePassword/Message.h" #include "../jrd/Mapping.h" #include "../jrd/extds/ExtDS.h" +#include +#include +#include namespace Jrd { @@ -218,11 +224,11 @@ static void checkForeignKeyTempScope(thread_db* tdbb, jrd_tra* transaction, // Check temporary table reference rules between just created child relation and all // its master relations. static void checkRelationTempScope(thread_db* tdbb, jrd_tra* transaction, - const QualifiedName& childRelName, const rel_t childType) + const QualifiedName& childRelName, const rel_t childType) { if (childType != rel_persistent && - childType != rel_global_temp_preserve && - childType != rel_global_temp_delete) + childType != rel_temp_preserve && + childType != rel_temp_delete) { return; } @@ -555,6 +561,23 @@ static QualifiedName getIndexRelationName(thread_db* tdbb, jrd_tra* transaction, { systemIndex = false; + // Check LTT indexes first + const auto attachment = transaction->getAttachment(); + + for (const auto& lttEntry : attachment->att_local_temporary_tables) + { + const auto ltt = lttEntry.second; + + if (ltt->name.schema != indexName.schema) + continue; + + for (const auto& index : ltt->indexes) + { + if (index.name == indexName) + return ltt->name; + } + } + AutoCacheRequest request(tdbb, drq_l_index_relname, DYN_REQUESTS); FOR(REQUEST_HANDLE request TRANSACTION_HANDLE transaction) @@ -599,9 +622,9 @@ static const char* getRelationScopeName(const rel_t type) { switch(type) { - case rel_global_temp_preserve: + case rel_temp_preserve: return REL_SCOPE_GTT_PRESERVE; - case rel_global_temp_delete: + case rel_temp_delete: return REL_SCOPE_GTT_DELETE; case rel_external: return REL_SCOPE_EXTERNAL; @@ -621,8 +644,8 @@ static const char* getRelationScopeName(const rel_t type) static void checkRelationType(const rel_t type, const QualifiedName& name) { if (type == rel_persistent || - type == rel_global_temp_preserve || - type == rel_global_temp_delete) + type == rel_temp_preserve || + type == rel_temp_delete) { return; } @@ -637,7 +660,7 @@ static void checkFkPairTypes(const rel_t masterType, const QualifiedName& master const rel_t childType, const QualifiedName& childName) { if (masterType != childType && - !(masterType == rel_global_temp_preserve && childType == rel_global_temp_delete)) + !(masterType == rel_temp_preserve && childType == rel_temp_delete)) { string master, child; makeRelationScopeName(master, masterName, masterType); @@ -1554,6 +1577,12 @@ void CommentOnNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, j break; case obj_relation: + if (attachment->att_local_temporary_tables.exist(name)) + { + status_exception::raise(Arg::Gds(isc_dsql_ltt_invalid_reference) << + name.toQuotedString() << "COMMENT ON"); + } + if (subName.hasData()) { tableClause = "rdb$relation_fields"; @@ -1858,13 +1887,13 @@ bool CreateAlterFunctionNode::executeCreate(thread_db* tdbb, DsqlCompilerScratch if (name.package.isEmpty()) { - if (createIfNotExistsOnly && !DYN_UTIL_check_unique_name_nothrow(tdbb, transaction, name, obj_udf)) + if (createIfNotExistsOnly && !DYN_UTIL_check_unique_name_nothrow(tdbb, name, obj_udf)) return false; executeDdlTrigger(tdbb, dsqlScratch, transaction, DTW_BEFORE, DDL_TRIGGER_CREATE_FUNCTION, name, {}); - DYN_UTIL_check_unique_name(tdbb, transaction, name, obj_udf); + DYN_UTIL_check_unique_name(tdbb, name, obj_udf); } AutoCacheRequest requestHandle(tdbb, drq_s_funcs2, DYN_REQUESTS); @@ -2897,12 +2926,12 @@ bool CreateAlterProcedureNode::executeCreate(thread_db* tdbb, DsqlCompilerScratc if (name.package.isEmpty()) { - if (createIfNotExistsOnly && !DYN_UTIL_check_unique_name_nothrow(tdbb, transaction, name, obj_procedure)) + if (createIfNotExistsOnly && !DYN_UTIL_check_unique_name_nothrow(tdbb, name, obj_procedure)) return false; executeDdlTrigger(tdbb, dsqlScratch, transaction, DTW_BEFORE, DDL_TRIGGER_CREATE_PROCEDURE, name, {}); - DYN_UTIL_check_unique_name(tdbb, transaction, name, obj_procedure); + DYN_UTIL_check_unique_name(tdbb, name, obj_procedure); } AutoCacheRequest requestHandle(tdbb, drq_s_prcs2, DYN_REQUESTS); @@ -3861,12 +3890,12 @@ void CreateAlterTriggerNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlS void CreateAlterTriggerNode::executeCreate(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, jrd_tra* transaction) { - if (createIfNotExistsOnly && !DYN_UTIL_check_unique_name_nothrow(tdbb, transaction, name, obj_trigger)) + if (createIfNotExistsOnly && !DYN_UTIL_check_unique_name_nothrow(tdbb, name, obj_trigger)) return; executeDdlTrigger(tdbb, dsqlScratch, transaction, DTW_BEFORE, DDL_TRIGGER_CREATE_TRIGGER, name, {}); - DYN_UTIL_check_unique_name(tdbb, transaction, name, obj_trigger); + DYN_UTIL_check_unique_name(tdbb, name, obj_trigger); store(tdbb, dsqlScratch, transaction); @@ -4160,12 +4189,12 @@ void CreateCollationNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScra // run all statements under savepoint control AutoSavePoint savePoint(tdbb, transaction); - if (createIfNotExistsOnly && !DYN_UTIL_check_unique_name_nothrow(tdbb, transaction, name, obj_collation)) + if (createIfNotExistsOnly && !DYN_UTIL_check_unique_name_nothrow(tdbb, name, obj_collation)) return; executeDdlTrigger(tdbb, dsqlScratch, transaction, DTW_BEFORE, DDL_TRIGGER_CREATE_COLLATION, name, {}); - DYN_UTIL_check_unique_name(tdbb, transaction, name, obj_collation); + DYN_UTIL_check_unique_name(tdbb, name, obj_collation); AutoCacheRequest request(tdbb, drq_s_colls, DYN_REQUESTS); @@ -4572,12 +4601,12 @@ void CreateDomainNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch // run all statements under savepoint control AutoSavePoint savePoint(tdbb, transaction); - if (createIfNotExistsOnly && !DYN_UTIL_check_unique_name_nothrow(tdbb, transaction, name, obj_field)) + if (createIfNotExistsOnly && !DYN_UTIL_check_unique_name_nothrow(tdbb, name, obj_field)) return; executeDdlTrigger(tdbb, dsqlScratch, transaction, DTW_BEFORE, DDL_TRIGGER_CREATE_DOMAIN, name, {}); - DYN_UTIL_check_unique_name(tdbb, transaction, name, obj_field); + DYN_UTIL_check_unique_name(tdbb, name, obj_field); storeGlobalField(tdbb, transaction, name, type); @@ -5726,12 +5755,12 @@ void CreateAlterExceptionNode::executeCreate(thread_db* tdbb, DsqlCompilerScratc Attachment* const attachment = transaction->getAttachment(); const MetaString& ownerName = attachment->getEffectiveUserName(); - if (createIfNotExistsOnly && !DYN_UTIL_check_unique_name_nothrow(tdbb, transaction, name, obj_exception)) + if (createIfNotExistsOnly && !DYN_UTIL_check_unique_name_nothrow(tdbb, name, obj_exception)) return; executeDdlTrigger(tdbb, dsqlScratch, transaction, DTW_BEFORE, DDL_TRIGGER_CREATE_EXCEPTION, name, {}); - DYN_UTIL_check_unique_name(tdbb, transaction, name, obj_exception); + DYN_UTIL_check_unique_name(tdbb, name, obj_exception); AutoCacheRequest request(tdbb, drq_s_xcp, DYN_REQUESTS); int faults = 0; @@ -5950,12 +5979,12 @@ void CreateAlterSequenceNode::putErrorPrefix(Firebird::Arg::StatusVector& status void CreateAlterSequenceNode::executeCreate(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, jrd_tra* transaction) { - if (createIfNotExistsOnly && !DYN_UTIL_check_unique_name_nothrow(tdbb, transaction, name, obj_generator)) + if (createIfNotExistsOnly && !DYN_UTIL_check_unique_name_nothrow(tdbb, name, obj_generator)) return; executeDdlTrigger(tdbb, dsqlScratch, transaction, DTW_BEFORE, DDL_TRIGGER_CREATE_SEQUENCE, name, {}); - DYN_UTIL_check_unique_name(tdbb, transaction, name, obj_generator); + DYN_UTIL_check_unique_name(tdbb, name, obj_generator); const SINT64 val = value.value_or(1); SLONG initialStep = 1; @@ -6084,7 +6113,7 @@ SSHORT CreateAlterSequenceNode::store(thread_db* tdbb, jrd_tra* transaction, con Attachment* const attachment = transaction->tra_attachment; const MetaString& ownerName = attachment->getEffectiveUserName(); - DYN_UTIL_check_unique_name(tdbb, transaction, name, obj_generator); + DYN_UTIL_check_unique_name(tdbb, name, obj_generator); AutoCacheRequest request(tdbb, drq_s_gens, DYN_REQUESTS); int faults = 0; @@ -6671,6 +6700,55 @@ bool RelationNode::deleteLocalField(thread_db* tdbb, jrd_tra* transaction, return found; } +// Validate column clause for local temporary table limitations. +// Throws exception if unsupported features are used. +void RelationNode::validateLttColumnClause(const AddColumnClause* addColumnClause) +{ + if (addColumnClause->defaultValue) + { + status_exception::raise( + Arg::Gds(isc_random) << "DEFAULT is not allowed for LOCAL TEMPORARY TABLE columns"); + } + + if (addColumnClause->computed) + { + status_exception::raise( + Arg::Gds(isc_random) << "COMPUTED BY is not allowed for LOCAL TEMPORARY TABLE columns"); + } + + if (addColumnClause->identityOptions) + { + status_exception::raise( + Arg::Gds(isc_random) << "IDENTITY columns are not allowed for LOCAL TEMPORARY TABLEs"); + } + + if (addColumnClause->field->dimensions != 0) + { + status_exception::raise( + Arg::Gds(isc_random) << "Array type columns are not allowed for LOCAL TEMPORARY TABLEs"); + } + + // Only unnamed NOT NULL constraints are supported on LTT columns + const bool hasInvalidConstraint = std::any_of( + addColumnClause->constraints.begin(), + addColumnClause->constraints.end(), + [](const auto& constraint) + { + return constraint.name.hasData() || + constraint.constraintType != AddConstraintClause::CTYPE_NOT_NULL; + } + ); + + if (hasInvalidConstraint) + { + status_exception::raise( + Arg::Gds(isc_sqlerr) << Arg::Num(-607) << + Arg::Gds(isc_dsql_command_err) << + Arg::Gds(isc_random) << + "Only NOT NULL constraints without names are supported on LOCAL TEMPORARY TABLEs"); + } +} + void RelationNode::defineField(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, jrd_tra* transaction, AddColumnClause* clause, SSHORT position, const ObjectsArray* pkCols) @@ -6763,13 +6841,16 @@ void RelationNode::defineField(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch field->resolve(dsqlScratch); - // Generate a domain. + if (tempFlag != REL_temp_ltt) + { + // Generate a domain. - if (fieldDefinition.fieldSource.schema.isEmpty()) - fieldDefinition.fieldSource.schema = name.schema; + if (fieldDefinition.fieldSource.schema.isEmpty()) + fieldDefinition.fieldSource.schema = name.schema; - storeGlobalField(tdbb, transaction, fieldDefinition.fieldSource, field, - computedSource, computedValue); + storeGlobalField(tdbb, transaction, fieldDefinition.fieldSource, field, + computedSource, computedValue); + } } else if (field->collate.object.hasData()) { @@ -6837,16 +6918,20 @@ void RelationNode::defineField(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch fieldDefinition.defaultValue = defaultValue; if (field->typeOfName.object.isEmpty() || field->collate.object.hasData()) fieldDefinition.collationId = field->collationId; - fieldDefinition.store(tdbb, transaction); - // Define the field constraints. - for (ObjectsArray::iterator constraint(constraints.begin()); - constraint != constraints.end(); - ++constraint) + if (tempFlag != REL_temp_ltt) { - if (constraint->create->type != Constraint::TYPE_FK) - constraint->create->columns.add(field->fld_name); - defineConstraint(tdbb, dsqlScratch, transaction, constraint->name, *constraint->create); + fieldDefinition.store(tdbb, transaction); + + // Define the field constraints. + for (ObjectsArray::iterator constraint(constraints.begin()); + constraint != constraints.end(); + ++constraint) + { + if (constraint->create->type != Constraint::TYPE_FK) + constraint->create->columns.add(field->fld_name); + defineConstraint(tdbb, dsqlScratch, transaction, constraint->name, *constraint->create); + } } } catch (const Exception&) @@ -6886,6 +6971,7 @@ void RelationNode::makeConstraint(thread_db* tdbb, DsqlCompilerScratch* dsqlScra jrd_tra* transaction, AddConstraintClause* clause, ObjectsArray& constraints, bool* notNull) { + const auto attachment = tdbb->getAttachment(); MemoryPool& pool = dsqlScratch->getPool(); switch (clause->constraintType) @@ -6926,6 +7012,13 @@ void RelationNode::makeConstraint(thread_db* tdbb, DsqlCompilerScratch* dsqlScra case AddConstraintClause::CTYPE_FK: { + // Check if it's a local temporary table, which cannot be referenced in FK + if (attachment->att_local_temporary_tables.exist(clause->refRelation)) + { + status_exception::raise( + Arg::Gds(isc_dsql_ltt_invalid_reference) << clause->refRelation.toQuotedString() << "FOREIGN KEY"); + } + CreateDropConstraint& constraint = constraints.add(); constraint.create = FB_NEW_POOL(pool) Constraint(pool); constraint.create->type = Constraint::TYPE_FK; @@ -7820,7 +7913,8 @@ string CreateRelationNode::internalPrint(NodePrinter& printer) const RelationNode::internalPrint(printer); NODE_PRINT(printer, externalFile); - NODE_PRINT(printer, relationType); + NODE_PRINT(printer, tempFlag); + NODE_PRINT(printer, tempRowsFlag); return "CreateRelationNode"; } @@ -7833,7 +7927,7 @@ void CreateRelationNode::checkPermission(thread_db* tdbb, jrd_tra* transaction) void CreateRelationNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, jrd_tra* transaction) { - if (createIfNotExistsOnly && !DYN_UTIL_check_unique_name_nothrow(tdbb, transaction, name, obj_relation)) + if (createIfNotExistsOnly && !DYN_UTIL_check_unique_name_nothrow(tdbb, name, obj_relation)) return; saveRelation(tdbb, dsqlScratch, name, false, true); @@ -7847,13 +7941,33 @@ void CreateRelationNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScrat // run all statements under savepoint control AutoSavePoint savePoint(tdbb, transaction); - executeDdlTrigger(tdbb, dsqlScratch, transaction, DTW_BEFORE, DDL_TRIGGER_CREATE_TABLE, name, {}); + if (tempFlag != REL_temp_ltt) + executeDdlTrigger(tdbb, dsqlScratch, transaction, DTW_BEFORE, DDL_TRIGGER_CREATE_TABLE, name, {}); + + DYN_UTIL_check_unique_name(tdbb, name, obj_relation); + + if (tempFlag == REL_temp_ltt) + { + defineLocalTempTable(tdbb, dsqlScratch, transaction); + + dsqlScratch->relation->rel_flags &= ~REL_creating; + + savePoint.release(); // everything is ok + + // Update DSQL cache + METD_drop_relation(transaction, name); - DYN_UTIL_check_unique_name(tdbb, transaction, name, obj_relation); + return; + } - fb_assert(relationType.has_value()); + const rel_t relationType = tempFlag.has_value() ? + (tempRowsFlag.value_or(REL_temp_tran) == REL_temp_tran ? + rel_temp_delete : + rel_temp_preserve + ) : + rel_persistent; - checkRelationTempScope(tdbb, transaction, name, relationType.value()); + checkRelationTempScope(tdbb, transaction, name, relationType); AutoCacheRequest request(tdbb, drq_s_rels2, DYN_REQUESTS); @@ -7864,7 +7978,7 @@ void CreateRelationNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScrat strcpy(REL.RDB$RELATION_NAME, name.object.c_str()); REL.RDB$SYSTEM_FLAG = 0; REL.RDB$FLAGS = REL_sql; - REL.RDB$RELATION_TYPE = relationType.value(); + REL.RDB$RELATION_TYPE = relationType; if (ssDefiner.isAssigned()) { @@ -7963,6 +8077,7 @@ void CreateRelationNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScrat // Update DSQL cache METD_drop_relation(transaction, name); + MET_dsql_cache_release(tdbb, SYM_relation, name); } @@ -7984,6 +8099,133 @@ const ObjectsArray* CreateRelationNode::findPkColumns() return NULL; } +void CreateRelationNode::defineLocalTempTable(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, jrd_tra* transaction) +{ + dsqlScratch->flags |= DsqlCompilerScratch::FLAG_ALLOW_LTT_REFERENCES; + + const auto attachment = tdbb->getAttachment(); + auto& attPool = *attachment->att_pool; + + // Check for duplicate local temporary table names + if (attachment->att_local_temporary_tables.exist(name)) + { + if (createIfNotExistsOnly) + return; + + status_exception::raise(Arg::PrivateDyn(132) << name.toQuotedString()); + } + + // Prevent external file for local temporary tables + if (externalFile) + status_exception::raise(Arg::Gds(isc_random) << "External file is not allowed for local temporary tables"); + + const auto ltt = FB_NEW_POOL(attPool) LocalTemporaryTable(attPool, name); + ltt->relationType = tempRowsFlag.value_or(REL_temp_tran) == REL_temp_tran ? rel_temp_delete : rel_temp_preserve; + + USHORT position = 0; + + for (const auto clause : clauses) + { + if (clause->type == Clause::TYPE_ADD_COLUMN) + { + const auto addColumnClause = static_cast(clause.getObject()); + const auto field = addColumnClause->field; + + // Check for duplicate field names + const bool fieldExists = std::any_of(ltt->pendingFields.begin(), ltt->pendingFields.end(), + [&field](const auto& existingField) { return existingField.name == field->fld_name; }); + + if (fieldExists) + { + // msg 323: "Column @1 already exists in table @2" + status_exception::raise( + Arg::PrivateDyn(323) << + field->fld_name.toQuotedString() << + name.toQuotedString()); + } + + validateLttColumnClause(addColumnClause); + + field->resolve(dsqlScratch); + + const bool notNullFlag = std::any_of( + addColumnClause->constraints.begin(), + addColumnClause->constraints.end(), + [](const auto& constraint) + { + return constraint.constraintType == AddConstraintClause::CTYPE_NOT_NULL; + } + ); + + auto& lttField = ltt->pendingFields.add(); + lttField.id = ltt->nextFieldId++; + lttField.position = position++; + lttField.name = field->fld_name; + lttField.source = field->typeOfName; + lttField.notNullFlag = notNullFlag; + lttField.collationId = field->collationId != 0 ? std::optional(field->collationId) : std::nullopt; + lttField.charSetId = field->charSetId != 0 ? std::optional(field->charSetId) : std::nullopt; + lttField.segLength = field->segLength; + lttField.charLength = field->charLength; + lttField.precision = field->precision; + + lttField.desc.dsc_dtype = field->dtype; + lttField.desc.dsc_length = field->length; + lttField.desc.dsc_scale = field->scale; + lttField.desc.dsc_sub_type = field->subType; + } + else if (clause->type == Clause::TYPE_ADD_CONSTRAINT) + { + status_exception::raise( + Arg::Gds(isc_sqlerr) << Arg::Num(-607) << + Arg::Gds(isc_dsql_command_err) << + Arg::Gds(isc_random) << + "Only NOT NULL constraints without names are supported on local temporary tables"); + } + } + + attachment->att_local_temporary_tables.put(name, ltt); + + // Assign LTT relation ID + + if (!attachment->att_next_ltt_id.has_value()) + attachment->att_next_ltt_id = MIN_LTT_ID; + else + { + if (attachment->att_next_ltt_id.value() >= MAX_LTT_ID) + attachment->att_next_ltt_id = MIN_LTT_ID; + else + ++attachment->att_next_ltt_id.value(); + } + + const USHORT initialLttIdProbe = attachment->att_next_ltt_id.value(); + + while (MET_lookup_relation_id(tdbb, attachment->att_next_ltt_id.value(), true)) + { + if (attachment->att_next_ltt_id.value() >= MAX_LTT_ID) + attachment->att_next_ltt_id = MIN_LTT_ID; + else + ++attachment->att_next_ltt_id.value(); + + if (attachment->att_next_ltt_id.value() == initialLttIdProbe) + { + ERR_post(Arg::Gds(isc_imp_exc) << + Arg::Gds(isc_random) << + Arg::Str("Local temporary table limit exceeded")); + } + } + + ltt->relationId = attachment->att_next_ltt_id.value(); + + // Register undo action with the savepoint + transaction->tra_save_point->createLttAction(LttUndoItem::LTT_UNDO_CREATE, name); + + // Mark LTT as having uncommitted changes + ltt->hasPendingChanges = true; + + DFW_post_work(transaction, dfw_update_ltt_format, string(ltt->name.object.c_str()), ltt->name.schema, 0); +} + //---------------------- @@ -8002,10 +8244,16 @@ void AlterRelationNode::checkPermission(thread_db* tdbb, jrd_tra* transaction) void AlterRelationNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, jrd_tra* transaction) { + const auto attachment = tdbb->getAttachment(); + + // Check if this is an LTT and set flag before METD_get_relation (called by saveRelation) + // to avoid false "LTT cannot be referenced in persistent metadata" error + if (attachment->att_local_temporary_tables.exist(name)) + dsqlScratch->flags |= DsqlCompilerScratch::FLAG_ALLOW_LTT_REFERENCES; + saveRelation(tdbb, dsqlScratch, name, false, false); - dsql_rel* relation; - relation = METD_get_relation(dsqlScratch->getTransaction(), dsqlScratch, name); + dsql_rel* relation = dsqlScratch->relation; if (!relation || (relation->rel_flags & REL_view)) { @@ -8030,6 +8278,13 @@ void AlterRelationNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScratc Arg::Gds(isc_random) << linecol***/); } + // Handle Local Temporary Tables differently + if (relation->rel_flags & REL_ltt_created) + { + alterLocalTempTable(tdbb, dsqlScratch, transaction); + return; + } + bool beforeTriggerWasExecuted = false; const auto executeBeforeTrigger = [&]() @@ -8420,7 +8675,9 @@ void AlterRelationNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScratc // Update DSQL cache METD_drop_relation(transaction, name); - MET_dsql_cache_release(tdbb, SYM_relation, name); + + if (!(relation->rel_flags & REL_ltt_created)) + MET_dsql_cache_release(tdbb, SYM_relation, name); } catch (const Exception&) { @@ -8922,6 +9179,339 @@ void AlterRelationNode::modifyField(thread_db* tdbb, DsqlCompilerScratch* dsqlSc } +// Alter a Local Temporary Table +void AlterRelationNode::alterLocalTempTable(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, + jrd_tra* transaction) +{ + // FLAG_ALLOW_LTT_REFERENCES is already set by execute() before calling this method + + const auto attachment = tdbb->getAttachment(); + auto& attPool = *attachment->att_pool; + + // Get the LTT + const auto lttPtr = attachment->att_local_temporary_tables.get(name); + if (!lttPtr) + { + status_exception::raise( + Arg::Gds(isc_sqlerr) << Arg::Num(-607) << + Arg::Gds(isc_dsql_command_err) << + Arg::Gds(isc_dsql_table_not_found) << name.toQuotedString()); + } + + const auto ltt = *lttPtr; + + // Run all statements under savepoint control + AutoSavePoint savePoint(tdbb, transaction); + + // Register undo action with the savepoint + transaction->tra_save_point->createLttAction( + LttUndoItem::LTT_UNDO_ALTER, name, FB_NEW_POOL(attPool) LocalTemporaryTable(attPool, *ltt)); + + SortedArray checkNotNullFields; + + // On first ALTER: copy committed fields to pending for modification + if (!ltt->hasPendingChanges) + ltt->pendingFields = ltt->fields; + + // Process each clause (modifying pendingFields) + for (const auto& clause : clauses) + { + switch (clause->type) + { + case Clause::TYPE_ADD_COLUMN: + { + const auto addColumnClause = static_cast(clause.getObject()); + const auto field = addColumnClause->field; + + // Check for duplicate field name in pending state + if (std::any_of(ltt->pendingFields.begin(), ltt->pendingFields.end(), + [&field](const auto& existingField) { return existingField.name == field->fld_name; })) + { + // msg 323: "Column @1 already exists in table @2" + status_exception::raise( + Arg::PrivateDyn(323) << + field->fld_name.toQuotedString() << + name.toQuotedString()); + } + + validateLttColumnClause(addColumnClause); + + field->resolve(dsqlScratch); + + const bool notNullFlag = std::any_of( + addColumnClause->constraints.begin(), + addColumnClause->constraints.end(), + [](const auto& constraint) + { + return constraint.constraintType == AddConstraintClause::CTYPE_NOT_NULL; + } + ); + + auto& lttField = ltt->pendingFields.add(); + lttField.id = ltt->nextFieldId++; + lttField.position = ltt->pendingFields.getCount() - 1; + lttField.name = field->fld_name; + lttField.source = field->typeOfName; + lttField.notNullFlag = notNullFlag; + lttField.collationId = field->collationId != 0 ? std::optional(field->collationId) : std::nullopt; + lttField.charSetId = field->charSetId != 0 ? std::optional(field->charSetId) : std::nullopt; + lttField.segLength = field->segLength; + lttField.charLength = field->charLength; + lttField.precision = field->precision; + + lttField.desc.dsc_dtype = field->dtype; + lttField.desc.dsc_length = field->length; + lttField.desc.dsc_scale = field->scale; + lttField.desc.dsc_sub_type = field->subType; + + if (notNullFlag) + checkNotNullFields.add(lttField.id); + + break; + } + + case Clause::TYPE_DROP_COLUMN: + { + const auto dropClause = static_cast(clause.getObject()); + const auto fieldFoundIt = std::find_if(ltt->pendingFields.begin(), ltt->pendingFields.end(), + [&dropClause](const auto& existingField) { return existingField.name == dropClause->name; }); + + if (fieldFoundIt != ltt->pendingFields.end()) + { + if (ltt->pendingFields.getCount() == 1) + { + // Msg354: last column in a table cannot be deleted + status_exception::raise(Arg::Gds(isc_no_meta_update) << + Arg::Gds(isc_del_last_field)); + } + + ltt->pendingFields.remove(fieldFoundIt); + } + else if (!dropClause->silent) + { + // msg 176: "column %s does not exist in table/view %s" + status_exception::raise(Arg::PrivateDyn(176) << dropClause->name << name.toQuotedString()); + } + break; + } + + case Clause::TYPE_ALTER_COL_NAME: + { + const auto renameClause = static_cast(clause.getObject()); + + // Check target name doesn't exist in pending state + if (std::any_of(ltt->pendingFields.begin(), ltt->pendingFields.end(), + [&renameClause](const auto& existingField) { return existingField.name == renameClause->toName; })) + { + // msg 205: Cannot rename field %s to %s. A field with that name already exists + status_exception::raise( + Arg::PrivateDyn(205) << renameClause->fromName << renameClause->toName << name.toQuotedString()); + } + + auto fieldFoundIt = std::find_if(ltt->pendingFields.begin(), ltt->pendingFields.end(), + [&renameClause](const auto& existingField) { return existingField.name == renameClause->fromName; }); + + if (fieldFoundIt == ltt->pendingFields.end()) + { + // msg 176: "column %s does not exist in table/view %s" + status_exception::raise(Arg::PrivateDyn(176) << renameClause->fromName << name.toQuotedString()); + } + else + fieldFoundIt->name = renameClause->toName; + + break; + } + + case Clause::TYPE_ALTER_COL_NULL: + { + const auto nullClause = static_cast(clause.getObject()); + bool found = false; + + for (auto& lttField : ltt->pendingFields) + { + if (lttField.name == nullClause->name) + { + if (nullClause->notNullFlag && !lttField.notNullFlag) + checkNotNullFields.add(lttField.id); + + lttField.notNullFlag = nullClause->notNullFlag; + found = true; + break; + } + } + + if (!found) + { + // msg 176: "column %s does not exist in table/view %s" + status_exception::raise(Arg::PrivateDyn(176) << nullClause->name << name.toQuotedString()); + } + break; + } + + case Clause::TYPE_ALTER_COL_POS: + { + const auto posClause = static_cast(clause.getObject()); + const SSHORT newPos = posClause->newPos - 1; // 1-based to 0-based + + if (newPos < 0 || static_cast(newPos) >= ltt->pendingFields.getCount()) + { + status_exception::raise( + Arg::Gds(isc_sqlerr) << Arg::Num(-607) << + Arg::Gds(isc_dsql_command_err) << + Arg::Gds(isc_random) << "Invalid column position"); + } + + // Find the field and its current position + FB_SIZE_T currentPos = ltt->pendingFields.getCount(); + for (FB_SIZE_T i = 0; i < ltt->pendingFields.getCount(); ++i) + { + if (ltt->pendingFields[i].name == posClause->name) + { + currentPos = i; + break; + } + } + + if (currentPos >= ltt->pendingFields.getCount()) + { + // msg 176: "column %s does not exist in table/view %s" + status_exception::raise(Arg::PrivateDyn(176) << posClause->name << name.toQuotedString()); + } + + // Move the field + if (currentPos != static_cast(newPos)) + { + LocalTemporaryTable::Field tempField = ltt->pendingFields[currentPos]; + ltt->pendingFields.remove(currentPos); + ltt->pendingFields.insert(newPos, tempField); + + // Update positions + for (FB_SIZE_T i = 0; i < ltt->pendingFields.getCount(); ++i) + ltt->pendingFields[i].position = i; + } + break; + } + + case Clause::TYPE_ALTER_COL_TYPE: + { + const auto typeClause = static_cast(clause.getObject()); + const auto field = typeClause->field; + bool found = false; + + field->resolve(dsqlScratch); + + for (auto& lttField : ltt->pendingFields) + { + if (lttField.name == field->fld_name) + { + // Validate that the type change is allowed, using the same rules as persistent tables. + dyn_fld origDom, newDom; + + DSC_make_descriptor(&origDom.dyn_dsc, blr_dtypes[lttField.desc.dsc_dtype], lttField.desc.dsc_scale, + lttField.desc.dsc_length, lttField.desc.dsc_sub_type, + lttField.charSetId.value_or(CS_NONE), lttField.collationId.value_or(COLLATE_NONE)); + + origDom.dyn_fld_name.object = lttField.name; + origDom.dyn_charbytelen = lttField.desc.dsc_length; + origDom.dyn_dtype = blr_dtypes[lttField.desc.dsc_dtype]; + origDom.dyn_precision = lttField.precision; + origDom.dyn_sub_type = lttField.desc.dsc_sub_type; + origDom.dyn_charlen = lttField.charLength; + origDom.dyn_collation = lttField.collationId.value_or(COLLATE_NONE); + origDom.dyn_null_flag = lttField.notNullFlag; + + DSC_make_descriptor(&newDom.dyn_dsc, blr_dtypes[field->dtype], field->scale, + field->length, field->subType, + field->charSetId.value_or(CS_NONE), field->collationId); + + newDom.dyn_fld_name.object = field->fld_name; + newDom.dyn_charbytelen = field->length; + newDom.dyn_dtype = blr_dtypes[field->dtype]; + newDom.dyn_precision = field->precision; + newDom.dyn_sub_type = field->subType; + newDom.dyn_charlen = field->charLength; + newDom.dyn_collation = field->collationId; + newDom.dyn_null_flag = field->notNull; + + AlterDomainNode::checkUpdate(origDom, newDom); + + lttField.source = field->typeOfName; + lttField.collationId = field->collationId != 0 ? std::optional(field->collationId) : std::nullopt; + lttField.charSetId = field->charSetId != 0 ? std::optional(field->charSetId) : std::nullopt; + lttField.segLength = field->segLength; + lttField.charLength = field->charLength; + lttField.precision = field->precision; + + lttField.desc.dsc_dtype = field->dtype; + lttField.desc.dsc_length = field->length; + lttField.desc.dsc_scale = field->scale; + lttField.desc.dsc_sub_type = field->subType; + found = true; + break; + } + } + + if (!found) + { + // msg 176: "column %s does not exist in table/view %s" + status_exception::raise( + Arg::PrivateDyn(176) << + field->fld_name.toQuotedString() << + name.toQuotedString()); + } + break; + } + + case Clause::TYPE_ADD_CONSTRAINT: + case Clause::TYPE_DROP_CONSTRAINT: + status_exception::raise( + Arg::Gds(isc_sqlerr) << Arg::Num(-607) << + Arg::Gds(isc_dsql_command_err) << + Arg::Gds(isc_random) << + "Only NOT NULL constraints without names are supported on local temporary tables"); + break; + + case Clause::TYPE_ALTER_SQL_SECURITY: + case Clause::TYPE_ALTER_PUBLICATION: + // These don't apply to LTTs + status_exception::raise( + Arg::Gds(isc_sqlerr) << Arg::Num(-607) << + Arg::Gds(isc_dsql_command_err) << + Arg::Gds(isc_random) << "Operation not supported for local temporary tables"); + break; + + default: + fb_assert(false); + break; + } + } + + // Validate NOT NULL constraints if needed + if (checkNotNullFields.hasData()) + { + dsc schemaDesc, nameDesc; + schemaDesc.makeText((USHORT) name.schema.length(), ttype_metadata, (UCHAR*) name.schema.c_str()); + nameDesc.makeText((USHORT) name.object.length(), ttype_metadata, (UCHAR*) name.object.c_str()); + + const auto work = DFW_post_work(transaction, dfw_check_not_null, &nameDesc, &schemaDesc, 0); + SortedArray& dfwFieldIds = DFW_get_ids(work); + + for (auto id : checkNotNullFields) + { + FB_SIZE_T pos; + if (!dfwFieldIds.find(id, pos)) + dfwFieldIds.insert(pos, id); + } + } + + // Post deferred work to update the LTT format + ltt->hasPendingChanges = true; + DFW_post_work(transaction, dfw_update_ltt_format, string(ltt->name.object.c_str()), ltt->name.schema, 0); + + savePoint.release(); // everything is ok +} + + //---------------------- @@ -8977,6 +9567,41 @@ void DropRelationNode::checkPermission(thread_db* tdbb, jrd_tra* transaction) void DropRelationNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, jrd_tra* transaction) { + // run all statements under savepoint control + AutoSavePoint savePoint(tdbb, transaction); + + const auto attachment = tdbb->getAttachment(); + + if (const auto lttIt = attachment->att_local_temporary_tables.get(name)) + { + if (view) + { + status_exception::raise( + Arg::Gds(isc_sqlerr) << Arg::Num(-607) << + Arg::Gds(isc_dsql_command_err) << + Arg::Gds(isc_dsql_view_not_found) << name.toQuotedString()); + } + + const auto ltt = *lttIt; + + // Post deferred work to handle the LTT deletion at commit time + DFW_post_work(transaction, dfw_delete_relation, + string(ltt->name.object.c_str()), ltt->name.schema, ltt->relationId); + + // Register undo action with the savepoint before removing + transaction->tra_save_point->createLttAction(LttUndoItem::LTT_UNDO_DROP, name, ltt); + + // Remove from in-memory storage (without deleting - ownership transferred to undo or already deleted) + attachment->att_local_temporary_tables.remove(name); + + savePoint.release(); // everything is ok + + // Update DSQL cache + METD_drop_relation(transaction, name); + + return; + } + jrd_rel* rel_drop = MET_lookup_relation(tdbb, name); if (rel_drop) MET_scan_relation(tdbb, rel_drop); @@ -8986,6 +9611,9 @@ void DropRelationNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch if (!relation && silent) return; + if (tdbb->getDatabase()->readOnly()) + ERRD_post(Arg::Gds(isc_read_only_database)); + // Check that DROP TABLE is dropping a table and that DROP VIEW is dropping a view. if (view) { @@ -9010,9 +9638,6 @@ void DropRelationNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch const int ddlTriggerAction = (view ? DDL_TRIGGER_DROP_VIEW : DDL_TRIGGER_DROP_TABLE); - // run all statements under savepoint control - AutoSavePoint savePoint(tdbb, transaction); - AutoCacheRequest request(tdbb, drq_l_relation, DYN_REQUESTS); bool found = false; @@ -9258,7 +9883,7 @@ void CreateAlterViewNode::checkPermission(thread_db* tdbb, jrd_tra* transaction) void CreateAlterViewNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, jrd_tra* transaction) { - if (createIfNotExistsOnly && !DYN_UTIL_check_unique_name_nothrow(tdbb, transaction, name, obj_relation)) + if (createIfNotExistsOnly && !DYN_UTIL_check_unique_name_nothrow(tdbb, name, obj_relation)) return; Attachment* const attachment = transaction->tra_attachment; @@ -9284,7 +9909,7 @@ void CreateAlterViewNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScra executeDdlTrigger(tdbb, dsqlScratch, transaction, DTW_BEFORE, ddlTriggerAction, name, {}); if (!modifyingView) - DYN_UTIL_check_unique_name(tdbb, transaction, name, obj_relation); + DYN_UTIL_check_unique_name(tdbb, name, obj_relation); // Compile the SELECT statement into a record selection expression, making sure to bump the // context number since view contexts start at 1 (except for computed fields) -- note that @@ -10077,7 +10702,7 @@ void CreateIndexNode::store(thread_db* tdbb, jrd_tra* transaction, QualifiedName fb_assert(name.schema == definition.relation.schema); - DYN_UTIL_check_unique_name(tdbb, transaction, name, obj_index); + DYN_UTIL_check_unique_name(tdbb, name, obj_index); AutoCacheRequest request(tdbb, drq_s_indices, DYN_REQUESTS); @@ -10472,7 +11097,18 @@ void CreateIndexNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, // run all statements under savepoint control AutoSavePoint savePoint(tdbb, transaction); - if (createIfNotExistsOnly && !DYN_UTIL_check_unique_name_nothrow(tdbb, transaction, name, obj_index)) + // Check if target relation is a Local Temporary Table + if (const auto lttPtr = attachment->att_local_temporary_tables.get(relation->dsqlName)) + { + defineLocalTempIndex(tdbb, dsqlScratch, transaction, *lttPtr); + savePoint.release(); // everything is ok + return; + } + + if (tdbb->getDatabase()->readOnly()) + ERRD_post(Arg::Gds(isc_read_only_database)); + + if (createIfNotExistsOnly && !DYN_UTIL_check_unique_name_nothrow(tdbb, name, obj_index)) return; executeDdlTrigger(tdbb, dsqlScratch, transaction, DTW_BEFORE, DDL_TRIGGER_CREATE_INDEX, name, {}); @@ -10529,6 +11165,114 @@ void CreateIndexNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, savePoint.release(); // everything is ok } +// Define an index on a Local Temporary Table. +void CreateIndexNode::defineLocalTempIndex(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, + jrd_tra* transaction, LocalTemporaryTable* ltt) +{ + dsqlScratch->flags |= DsqlCompilerScratch::FLAG_ALLOW_LTT_REFERENCES; + + const auto attachment = transaction->tra_attachment; + auto& attPool = *attachment->att_pool; + + if (computed) + { + status_exception::raise( + Arg::Gds(isc_sqlerr) << Arg::Num(-607) << + Arg::Gds(isc_wish_list) << + Arg::Gds(isc_random) << "Expression-based indexes are not supported for local temporary tables"); + } + + if (partial) + { + status_exception::raise( + Arg::Gds(isc_sqlerr) << Arg::Num(-607) << + Arg::Gds(isc_wish_list) << + Arg::Gds(isc_random) << "Partial indexes are not supported for local temporary tables"); + } + + // Check index name uniqueness within the LTT scope + for (const auto& existingIndex : ltt->indexes) + { + if (existingIndex.name == name) + { + if (createIfNotExistsOnly) + return; + + status_exception::raise( + Arg::Gds(isc_no_dup) << name.toQuotedString()); + } + } + + // Also check that the index name doesn't conflict with persistent indexes + // in the same schema + if (!DYN_UTIL_check_unique_name_nothrow(tdbb, name, obj_index)) + { + if (createIfNotExistsOnly) + return; + + status_exception::raise(Arg::Gds(isc_no_dup) << name.toQuotedString()); + } + + if (columns->items.getCount() > MAX_INDEX_SEGMENTS) + status_exception::raise(Arg::Gds(isc_idx_key_err) << name.toQuotedString()); + + const auto& fields = ltt->hasPendingChanges ? ltt->pendingFields : ltt->fields; + + for (const auto& col : columns->items) + { + const auto& colName = nodeAs(col)->dsqlName; + + auto fieldIt = std::find_if(fields.begin(), fields.end(), + [&colName](const auto& field) { return field.name == colName; }); + + if (fieldIt == fields.end()) + { + // Column not found in LTT + status_exception::raise( + Arg::Gds(isc_dyn_column_does_not_exist) << colName.c_str() << ltt->name.toQuotedString()); + } + + // Check if field type is indexable (no blobs or arrays) + if (fieldIt->desc.dsc_dtype == dtype_blob) + status_exception::raise(Arg::Gds(isc_blob_idx_err) << colName.c_str()); + } + + // Register undo action with the savepoint + transaction->tra_save_point->createLttAction(LttUndoItem::LTT_UNDO_ALTER, ltt->name, + FB_NEW_POOL(attPool) LocalTemporaryTable(attPool, *ltt)); + + const USHORT initialIdProbe = ltt->nextIndexId; + + std::unordered_set usedIds; + for (const auto& index : ltt->indexes) + usedIds.insert(index.id); + + while (true) + { + if (usedIds.find(ltt->nextIndexId) == usedIds.end()) + break; + + ltt->nextIndexId++; + + if (ltt->nextIndexId == initialIdProbe) + status_exception::raise(Arg::Gds(isc_no_meta_update) << Arg::Gds(isc_max_idx) << Arg::Num(MAX_USHORT)); + } + + // Add the index to the LTT + auto& newIndex = ltt->indexes.add(); + newIndex.name = name; + newIndex.unique = unique; + newIndex.descending = descending; + newIndex.inactive = !active; + newIndex.id = ltt->nextIndexId++; + + for (const auto& col : columns->items) + newIndex.columns.add(nodeAs(col)->dsqlName); + + // Post deferred work to create the actual B-tree structure + DFW_post_work(transaction, dfw_create_ltt_index, string(name.object.c_str()), name.schema, 0); +} + DdlNode* CreateIndexNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) { dsqlScratch->qualifyExistingName(relation->dsqlName, obj_relation); @@ -10576,6 +11320,20 @@ void AlterIndexNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, // run all statements under savepoint control AutoSavePoint savePoint(tdbb, transaction); + // Check if index belongs to a Local Temporary Table + LocalTemporaryTable* ltt = nullptr; + LocalTemporaryTable::Index* lttIndex = nullptr; + + if (MET_get_ltt_index(transaction->getAttachment(), name, <t, <tIndex)) + { + alterLocalTempIndex(tdbb, dsqlScratch, transaction, ltt, lttIndex); + savePoint.release(); // everything is ok + return; + } + + if (tdbb->getDatabase()->readOnly()) + ERRD_post(Arg::Gds(isc_read_only_database)); + AutoCacheRequest request(tdbb, drq_m_index, DYN_REQUESTS); bool found = false; @@ -10606,6 +11364,40 @@ void AlterIndexNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, savePoint.release(); // everything is ok } +// Alter an index on a Local Temporary Table (ACTIVE/INACTIVE). +void AlterIndexNode::alterLocalTempIndex(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, + jrd_tra* transaction, LocalTemporaryTable* ltt, LocalTemporaryTable::Index* lttIndex) +{ + const auto attachment = transaction->tra_attachment; + auto& attPool = *attachment->att_pool; + + // Register undo action with the savepoint + transaction->tra_save_point->createLttAction( + LttUndoItem::LTT_UNDO_ALTER, ltt->name, FB_NEW_POOL(attPool) LocalTemporaryTable(attPool, *ltt)); + + const bool wasInactive = lttIndex->inactive; + const bool willBeInactive = !active; + + // Update the inactive flag + lttIndex->inactive = willBeInactive; + + // If the index is being activated, post deferred work to create the B-tree + // If it's being deactivated, delete the B-tree + if (wasInactive && !willBeInactive) + { + // Index is being activated - create the B-tree + DFW_post_work(transaction, dfw_create_ltt_index, string(name.object.c_str()), name.schema, 0); + } + else if (!wasInactive && willBeInactive && ltt->relationId) + { + // Index is being deactivated - delete the B-tree + // We pass the index ID in dfw_id, and the relation ID in dfw_ids + const auto work = DFW_post_work(transaction, dfw_delete_ltt_index, + string(name.object.c_str()), name.schema, lttIndex->id); + DFW_get_ids(work).add(ltt->relationId); + } +} + //---------------------- @@ -10632,6 +11424,17 @@ void SetStatisticsNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScratc // run all statements under savepoint control AutoSavePoint savePoint(tdbb, transaction); + // Check if index belongs to a Local Temporary Table + LocalTemporaryTable* ltt = nullptr; + LocalTemporaryTable::Index* lttIndex = nullptr; + + if (MET_get_ltt_index(transaction->getAttachment(), name, <t, <tIndex)) + { + setStatisticsLocalTempIndex(tdbb, dsqlScratch, transaction, ltt, lttIndex); + savePoint.release(); // everything is ok + return; + } + AutoCacheRequest request(tdbb, drq_m_set_statistics, DYN_REQUESTS); bool found = false; @@ -10663,6 +11466,23 @@ void SetStatisticsNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScratc savePoint.release(); // everything is ok } +// Set statistics for an index on a Local Temporary Table. +void SetStatisticsNode::setStatisticsLocalTempIndex(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, + jrd_tra* transaction, LocalTemporaryTable* ltt, LocalTemporaryTable::Index* lttIndex) +{ + const auto attachment = transaction->tra_attachment; + + // Register undo action with the savepoint + transaction->tra_save_point->createLttAction( + LttUndoItem::LTT_UNDO_ALTER, ltt->name, FB_NEW_POOL(*attachment->att_pool) LocalTemporaryTable(*attachment->att_pool, *ltt)); + + // Post deferred work to recalculate statistics during commit + // We pass the index ID in dfw_id and leave dfw_ids empty to signal "set statistics" + // (as opposed to delete which has dfw_ids containing the relation ID) + DFW_post_work(transaction, dfw_modify_ltt_index, + string(name.object.c_str()), name.schema, lttIndex->id); +} + //---------------------- @@ -10709,6 +11529,20 @@ void DropIndexNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, j // run all statements under savepoint control AutoSavePoint savePoint(tdbb, transaction); + // Check if index belongs to a Local Temporary Table + LocalTemporaryTable* ltt = nullptr; + LocalTemporaryTable::Index* lttIndex = nullptr; + + if (MET_get_ltt_index(transaction->getAttachment(), name, <t, <tIndex)) + { + dropLocalTempIndex(tdbb, dsqlScratch, transaction, ltt, lttIndex); + savePoint.release(); // everything is ok + return; + } + + if (tdbb->getDatabase()->readOnly()) + ERRD_post(Arg::Gds(isc_read_only_database)); + AutoCacheRequest request(tdbb, drq_e_indices, DYN_REQUESTS); bool found = false; @@ -10742,6 +11576,40 @@ void DropIndexNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, j savePoint.release(); // everything is ok } +// Drop an index from a Local Temporary Table. +void DropIndexNode::dropLocalTempIndex(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, + jrd_tra* transaction, LocalTemporaryTable* ltt, LocalTemporaryTable::Index* lttIndex) +{ + Attachment* const attachment = transaction->tra_attachment; + auto& attPool = *attachment->att_pool; + + // Register undo action with the savepoint + transaction->tra_save_point->createLttAction( + LttUndoItem::LTT_UNDO_ALTER, ltt->name, FB_NEW_POOL(attPool) LocalTemporaryTable(attPool, *ltt)); + + // Get the index ID before removing from the LTT + const USHORT indexId = lttIndex->id; + + // Remove the index from the LTT + for (FB_SIZE_T i = 0; i < ltt->indexes.getCount(); ++i) + { + if (<t->indexes[i] == lttIndex) + { + ltt->indexes.remove(i); + break; + } + } + + if (ltt->relationId) + { + // Post deferred work to delete the actual B-tree structure + // We pass the index ID in dfw_id, and the relation ID in dfw_ids + const auto work = DFW_post_work(transaction, dfw_delete_ltt_index, + string(name.object.c_str()), name.schema, indexId); + DFW_get_ids(work).add(ltt->relationId); + } +} + //---------------------- @@ -10771,10 +11639,10 @@ void CreateFilterNode::execute(thread_db* tdbb, DsqlCompilerScratch* /*dsqlScrat const MetaString& ownerName = attachment->getEffectiveUserName(); const QualifiedName qualifiedName(name); - if (createIfNotExistsOnly && !DYN_UTIL_check_unique_name_nothrow(tdbb, transaction, qualifiedName, obj_blob_filter)) + if (createIfNotExistsOnly && !DYN_UTIL_check_unique_name_nothrow(tdbb, qualifiedName, obj_blob_filter)) return; - DYN_UTIL_check_unique_name(tdbb, transaction, qualifiedName, obj_blob_filter); + DYN_UTIL_check_unique_name(tdbb, qualifiedName, obj_blob_filter); // run all statements under savepoint control AutoSavePoint savePoint(tdbb, transaction); @@ -12384,6 +13252,12 @@ void GrantRevokeNode::grantRevoke(thread_db* tdbb, jrd_tra* transaction, const G break; case obj_relation: + if (tdbb->getAttachment()->att_local_temporary_tables.exist(objName)) + { + status_exception::raise(Arg::Gds(isc_dsql_ltt_invalid_reference) << + objName.toQuotedString() << (isGrant ? "GRANT" : "REVOKE")); + } + if (!checkObjectExist(tdbb, transaction, objName, objType)) status_exception::raise(Arg::PrivateDyn(306) << objName.toQuotedString()); // Table @1 does not exist @@ -13708,12 +14582,12 @@ void CreateAlterSchemaNode::executeCreate(thread_db* tdbb, DsqlCompilerScratch* const auto attachment = transaction->getAttachment(); const auto& ownerName = attachment->getEffectiveUserName(); - if (createIfNotExistsOnly && !DYN_UTIL_check_unique_name_nothrow(tdbb, transaction, qualifiedName, obj_schema)) + if (createIfNotExistsOnly && !DYN_UTIL_check_unique_name_nothrow(tdbb, qualifiedName, obj_schema)) return; executeDdlTrigger(tdbb, dsqlScratch, transaction, DTW_BEFORE, DDL_TRIGGER_CREATE_SCHEMA, qualifiedName, {}); - DYN_UTIL_check_unique_name(tdbb, transaction, qualifiedName, obj_schema); + DYN_UTIL_check_unique_name(tdbb, qualifiedName, obj_schema); static const CachedRequestId requestHandleId; AutoCacheRequest requestHandle(tdbb, requestHandleId); @@ -13955,6 +14829,21 @@ bool DropSchemaNode::collectObjects(thread_db* tdbb, jrd_tra* transaction, if (objects) objects->clear(); + const auto attachment = tdbb->getAttachment(); + + for (const auto& lttEntry : attachment->att_local_temporary_tables) + { + const auto& lttName = lttEntry.first; + + if (lttName.schema == name) + { + if (objects) + objects->add({obj_relation, lttName.object}); + else + return true; + } + } + { // fields static const CachedRequestId requestHandleId; AutoCacheRequest requestHandle(tdbb, requestHandleId); diff --git a/src/dsql/DdlNodes.h b/src/dsql/DdlNodes.h index ab73673a487..e2551a9ea3e 100644 --- a/src/dsql/DdlNodes.h +++ b/src/dsql/DdlNodes.h @@ -36,6 +36,7 @@ #include "../common/classes/array.h" #include "../common/classes/ByteChunk.h" #include "../common/classes/TriState.h" +#include "../jrd/Relation.h" #include "../jrd/Savepoint.h" #include "../dsql/errd_proto.h" @@ -49,6 +50,7 @@ enum SqlSecurity }; class LocalDeclarationsNode; +class LocalTemporaryTable; class RelationSourceNode; class ValueListNode; class SecDbContext; @@ -230,6 +232,11 @@ class RecreateNode : public DdlNode return DdlNode::dsqlPass(dsqlScratch); } + bool disallowedInReadOnlyDatabase() const override + { + return createNode->disallowedInReadOnlyDatabase(); + } + protected: void putErrorPrefix(Firebird::Arg::StatusVector& statusVector) override { @@ -1614,6 +1621,8 @@ class RelationNode : public DdlNode DdlNode* dsqlPass(DsqlCompilerScratch* dsqlScratch) override; protected: + static void validateLttColumnClause(const AddColumnClause* addColumnClause); + Firebird::string internalPrint(NodePrinter& printer) const override { DdlNode::internalPrint(printer); @@ -1655,6 +1664,8 @@ class RelationNode : public DdlNode NestConst dsqlNode; QualifiedName name; Firebird::Array > clauses; + std::optional tempFlag; // REL_temp_gtt, REL_temp_ltt + std::optional tempRowsFlag; // REL_temp_tran, REL_temp_conn Firebird::TriState ssDefiner; Firebird::TriState replicationState; }; @@ -1684,6 +1695,11 @@ class CreateRelationNode final : public RelationNode return RelationNode::dsqlPass(dsqlScratch); } + bool disallowedInReadOnlyDatabase() const override + { + return tempFlag != REL_temp_ltt; + } + protected: void putErrorPrefix(Firebird::Arg::StatusVector& statusVector) override { @@ -1692,12 +1708,10 @@ class CreateRelationNode final : public RelationNode private: const Firebird::ObjectsArray* findPkColumns(); + void defineLocalTempTable(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, jrd_tra* transaction); public: const Firebird::string* externalFile; - std::optional relationType = rel_persistent; - bool preserveRowsOpt = false; - bool deleteRowsOpt = false; bool createIfNotExistsOnly = false; }; @@ -1719,6 +1733,11 @@ class AlterRelationNode final : public RelationNode return RelationNode::dsqlPass(dsqlScratch); } + bool disallowedInReadOnlyDatabase() const override + { + return false; // Deferred to execute() - LTT status unknown at parse time + } + public: Firebird::string internalPrint(NodePrinter& printer) const override; void checkPermission(thread_db* tdbb, jrd_tra* transaction) override; @@ -1733,6 +1752,7 @@ class AlterRelationNode final : public RelationNode private: void modifyField(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, jrd_tra* transaction, AlterColTypeClause* clause); + void alterLocalTempTable(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, jrd_tra* transaction); }; @@ -1767,6 +1787,11 @@ class DropRelationNode final : public DdlNode return DdlNode::dsqlPass(dsqlScratch); } + bool disallowedInReadOnlyDatabase() const override + { + return false; // Deferred to execute() - LTT status unknown at parse time + } + protected: void putErrorPrefix(Firebird::Arg::StatusVector& statusVector) override { @@ -1890,6 +1915,15 @@ class CreateIndexNode final : public DdlNode void execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, jrd_tra* transaction) override; DdlNode* dsqlPass(DsqlCompilerScratch* dsqlScratch) override; + bool disallowedInReadOnlyDatabase() const override + { + return false; // Deferred to execute() - LTT status unknown at parse time + } + +private: + void defineLocalTempIndex(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, + jrd_tra* transaction, LocalTemporaryTable* ltt); + protected: void putErrorPrefix(Firebird::Arg::StatusVector& statusVector) override { @@ -1932,6 +1966,15 @@ class AlterIndexNode final : public DdlNode return DdlNode::dsqlPass(dsqlScratch); } + bool disallowedInReadOnlyDatabase() const override + { + return false; // Deferred to execute() - LTT status unknown at parse time + } + +private: + void alterLocalTempIndex(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, + jrd_tra* transaction, LocalTemporaryTable* ltt, LocalTemporaryTable::Index* lttIndex); + protected: void putErrorPrefix(Firebird::Arg::StatusVector& statusVector) override { @@ -1966,6 +2009,10 @@ class SetStatisticsNode final : public DdlNode return DdlNode::dsqlPass(dsqlScratch); } +private: + void setStatisticsLocalTempIndex(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, + jrd_tra* transaction, LocalTemporaryTable* ltt, LocalTemporaryTable::Index* lttIndex); + protected: void putErrorPrefix(Firebird::Arg::StatusVector& statusVector) override { @@ -2002,6 +2049,15 @@ class DropIndexNode final : public DdlNode return DdlNode::dsqlPass(dsqlScratch); } + bool disallowedInReadOnlyDatabase() const override + { + return false; // Deferred to execute() - LTT status unknown at parse time + } + +private: + void dropLocalTempIndex(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, + jrd_tra* transaction, LocalTemporaryTable* ltt, LocalTemporaryTable::Index* lttIndex); + protected: void putErrorPrefix(Firebird::Arg::StatusVector& statusVector) override { diff --git a/src/dsql/DsqlCompilerScratch.h b/src/dsql/DsqlCompilerScratch.h index 68854524f46..f63aa70dd4b 100644 --- a/src/dsql/DsqlCompilerScratch.h +++ b/src/dsql/DsqlCompilerScratch.h @@ -76,6 +76,7 @@ class DsqlCompilerScratch : public BlrDebugWriter static const unsigned FLAG_FETCH = 0x4000; static const unsigned FLAG_VIEW_WITH_CHECK = 0x8000; static const unsigned FLAG_EXEC_BLOCK = 0x010000; + static const unsigned FLAG_ALLOW_LTT_REFERENCES = 0x020000; static const unsigned MAX_NESTING = 512; diff --git a/src/dsql/DsqlStatements.cpp b/src/dsql/DsqlStatements.cpp index d656534fc5e..34aec832e7b 100644 --- a/src/dsql/DsqlStatements.cpp +++ b/src/dsql/DsqlStatements.cpp @@ -254,7 +254,7 @@ void DsqlDdlStatement::dsqlPass(thread_db* tdbb, DsqlCompilerScratch* scratch, n rethrowDdlException(ex, false, node); } - if (dbb->readOnly()) + if (dbb->readOnly() && node->disallowedInReadOnlyDatabase()) ERRD_post(Arg::Gds(isc_read_only_database)); // In read-only replica, only replicator is allowed to execute DDL. diff --git a/src/dsql/Nodes.h b/src/dsql/Nodes.h index f769b4b33ed..50a82b0452b 100644 --- a/src/dsql/Nodes.h +++ b/src/dsql/Nodes.h @@ -275,6 +275,11 @@ class DdlNode : public Node { return true; } + + virtual bool disallowedInReadOnlyDatabase() const + { + return true; + } }; diff --git a/src/dsql/PackageNodes.epp b/src/dsql/PackageNodes.epp index 2cf20722aa0..994e6795ccb 100644 --- a/src/dsql/PackageNodes.epp +++ b/src/dsql/PackageNodes.epp @@ -378,12 +378,12 @@ void CreateAlterPackageNode::executeCreate(thread_db* tdbb, DsqlCompilerScratch* Attachment* const attachment = transaction->getAttachment(); const MetaString& ownerName = attachment->getEffectiveUserName(); - if (createIfNotExistsOnly && !DYN_UTIL_check_unique_name_nothrow(tdbb, transaction, name, obj_package_header)) + if (createIfNotExistsOnly && !DYN_UTIL_check_unique_name_nothrow(tdbb, name, obj_package_header)) return; executeDdlTrigger(tdbb, dsqlScratch, transaction, DTW_BEFORE, DDL_TRIGGER_CREATE_PACKAGE, name, {}); - DYN_UTIL_check_unique_name(tdbb, transaction, name, obj_package_header); + DYN_UTIL_check_unique_name(tdbb, name, obj_package_header); AutoCacheRequest requestHandle(tdbb, drq_s_pkg, DYN_REQUESTS); diff --git a/src/dsql/dsql.h b/src/dsql/dsql.h index 1a9c9b90181..fd35e31f164 100644 --- a/src/dsql/dsql.h +++ b/src/dsql/dsql.h @@ -171,7 +171,8 @@ enum rel_flags_vals { REL_dropped = 2, // relation has been dropped 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_creating = 16, // we are creating the bare relation in memory + REL_ltt_created = 32 // relation is created local temporary table }; class TypeClause diff --git a/src/dsql/metd.epp b/src/dsql/metd.epp index b6ff06615ea..0f688a36904 100644 --- a/src/dsql/metd.epp +++ b/src/dsql/metd.epp @@ -53,6 +53,7 @@ #include "../yvalve/why_proto.h" #include "../common/utils_proto.h" #include "../common/classes/init.h" +#include "../jrd/LocalTemporaryTable.h" using namespace Jrd; using namespace Firebird; @@ -266,7 +267,9 @@ void METD_drop_relation(jrd_tra* transaction, const QualifiedName& name) if (dbb->dbb_relations.get(name, relation)) { - MET_dsql_cache_use(tdbb, SYM_relation, name); + if (!(relation->rel_flags & REL_temp_ltt)) + MET_dsql_cache_use(tdbb, SYM_relation, name); + relation->rel_flags |= REL_dropped; dbb->dbb_relations.remove(name); } @@ -1199,7 +1202,8 @@ dsql_rel* METD_get_relation(jrd_tra* transaction, DsqlCompilerScratch* dsqlScrat validateTransaction(transaction); - dsql_dbb* dbb = transaction->getDsqlAttachment(); + const auto attachment = transaction->getAttachment(); + const auto dbb = transaction->getDsqlAttachment(); // See if the relation is the one currently being defined in this statement @@ -1211,7 +1215,7 @@ dsql_rel* METD_get_relation(jrd_tra* transaction, DsqlCompilerScratch* dsqlScrat if (dbb->dbb_relations.get(name, temp) && !(temp->rel_flags & REL_dropped)) { - if (MET_dsql_cache_use(tdbb, SYM_relation, name)) + if (!(temp->rel_flags & REL_ltt_created) && MET_dsql_cache_use(tdbb, SYM_relation, name)) temp->rel_flags |= REL_dropped; else return temp; @@ -1221,60 +1225,91 @@ dsql_rel* METD_get_relation(jrd_tra* transaction, DsqlCompilerScratch* dsqlScrat // and this is a type of statement which does not use ids, prepare a // temporary relation block to provide information without caching it - bool permanent = true; + const auto lttPtr = attachment->att_local_temporary_tables.get(name); + const auto ltt = lttPtr ? *lttPtr : nullptr; + + // LTTs cannot be referenced in persistent metadata (DDL other than LTT DDL itself) + if (ltt && + (dsqlScratch->flags & DsqlCompilerScratch::FLAG_DDL) && + !(dsqlScratch->flags & DsqlCompilerScratch::FLAG_ALLOW_LTT_REFERENCES)) + { + status_exception::raise(Arg::Gds(isc_dsql_ltt_invalid_reference) << name.toQuotedString() << "persistent metadata"); + } - AutoCacheRequest handle1(tdbb, irq_rel_ids, IRQ_REQUESTS); + bool stored = !ltt; - FOR(REQUEST_HANDLE handle1 TRANSACTION_HANDLE transaction) - REL IN RDB$RELATIONS - CROSS RFR IN RDB$RELATION_FIELDS OVER RDB$SCHEMA_NAME, RDB$RELATION_NAME - WITH REL.RDB$SCHEMA_NAME EQ name.schema.c_str() AND - REL.RDB$RELATION_NAME EQ name.object.c_str() AND - (REL.RDB$RELATION_ID MISSING OR RFR.RDB$FIELD_ID MISSING) + if (!ltt) { - permanent = false; + static const CachedRequestId handle1bCacheId; + AutoCacheRequest handle1b(tdbb, handle1bCacheId); + + FOR(REQUEST_HANDLE handle1b TRANSACTION_HANDLE transaction) + REL IN RDB$RELATIONS + CROSS RFR IN RDB$RELATION_FIELDS OVER RDB$SCHEMA_NAME, RDB$RELATION_NAME + WITH REL.RDB$SCHEMA_NAME EQ name.schema.c_str() AND + REL.RDB$RELATION_NAME EQ name.object.c_str() AND + (REL.RDB$RELATION_ID MISSING OR RFR.RDB$FIELD_ID MISSING) + { + stored = false; + } + END_FOR } - END_FOR // Now see if it is in the database - MemoryPool& pool = permanent ? dbb->dbb_pool : *tdbb->getDefaultPool(); + MemoryPool& pool = stored ? dbb->dbb_pool : *tdbb->getDefaultPool(); dsql_rel* relation = NULL; - AutoCacheRequest handle2(tdbb, irq_relation, IRQ_REQUESTS); + if (ltt) + { + fb_assert(ltt->relationId); - FOR(REQUEST_HANDLE handle2 TRANSACTION_HANDLE transaction) - X IN RDB$RELATIONS - WITH X.RDB$SCHEMA_NAME EQ name.schema.c_str() AND - X.RDB$RELATION_NAME EQ name.object.c_str() + relation = FB_NEW_POOL(pool) dsql_rel(pool); + relation->rel_id = ltt->relationId; + relation->rel_name = name; + relation->rel_dbkey_length = 8; + relation->rel_flags = REL_ltt_created; + } + else { - // Allocate from default or permanent pool as appropriate + static const CachedRequestId handle2bCacheId; + AutoCacheRequest handle2b(tdbb, handle2bCacheId); - if (!X.RDB$RELATION_ID.NULL) + FOR(REQUEST_HANDLE handle2b TRANSACTION_HANDLE transaction) + X IN RDB$RELATIONS + WITH X.RDB$SCHEMA_NAME EQ name.schema.c_str() AND + X.RDB$RELATION_NAME EQ name.object.c_str() { - relation = FB_NEW_POOL(pool) dsql_rel(pool); - relation->rel_id = X.RDB$RELATION_ID; - } - else if (!DDL_ids(dsqlScratch)) - relation = FB_NEW_POOL(pool) dsql_rel(pool); + fb_utils::exact_name(X.RDB$OWNER_NAME); - // fill out the relation information + // Allocate from default or permanent pool as appropriate - if (relation) - { - relation->rel_name = name; - relation->rel_owner = X.RDB$OWNER_NAME; - if (!(relation->rel_dbkey_length = X.RDB$DBKEY_LENGTH)) - relation->rel_dbkey_length = 8; - // CVC: let's see if this is a table or a view. - if (!X.RDB$VIEW_BLR.NULL) - relation->rel_flags |= REL_view; - if (!X.RDB$EXTERNAL_FILE.NULL) - relation->rel_flags |= REL_external; + if (!X.RDB$RELATION_ID.NULL) + { + relation = FB_NEW_POOL(pool) dsql_rel(pool); + relation->rel_id = X.RDB$RELATION_ID; + } + else if (!DDL_ids(dsqlScratch)) + relation = FB_NEW_POOL(pool) dsql_rel(pool); + + // fill out the relation information + + if (relation) + { + relation->rel_name = name; + relation->rel_owner = X.RDB$OWNER_NAME; + if (!(relation->rel_dbkey_length = X.RDB$DBKEY_LENGTH)) + relation->rel_dbkey_length = 8; + // CVC: let's see if this is a table or a view. + if (!X.RDB$VIEW_BLR.NULL) + relation->rel_flags |= REL_view; + if (!X.RDB$EXTERNAL_FILE.NULL) + relation->rel_flags |= REL_external; + } } + END_FOR } - END_FOR if (!relation) return NULL; @@ -1283,80 +1318,120 @@ dsql_rel* METD_get_relation(jrd_tra* transaction, DsqlCompilerScratch* dsqlScrat dsql_fld** ptr = &relation->rel_fields; - AutoCacheRequest handle3(tdbb, irq_fields, IRQ_REQUESTS); - - FOR(REQUEST_HANDLE handle3 TRANSACTION_HANDLE transaction) - FLX IN RDB$FIELDS - CROSS RFR IN RDB$RELATION_FIELDS - WITH RFR.RDB$SCHEMA_NAME EQ name.schema.c_str() AND - RFR.RDB$RELATION_NAME EQ name.object.c_str() AND - FLX.RDB$SCHEMA_NAME EQUIV RFR.RDB$FIELD_SOURCE_SCHEMA_NAME AND - FLX.RDB$FIELD_NAME EQ RFR.RDB$FIELD_SOURCE - SORTED BY RFR.RDB$FIELD_POSITION + if (ltt) { - // Allocate the field block - // Allocate from default or permanent pool as appropriate - - dsql_fld* field = NULL; - - if (!RFR.RDB$FIELD_ID.NULL) + for (const auto& lttField : ltt->fields) { - field = FB_NEW_POOL(pool) dsql_fld(pool); - field->fld_id = RFR.RDB$FIELD_ID; - } - else if (!DDL_ids(dsqlScratch)) - field = FB_NEW_POOL(pool) dsql_fld(pool); + dsql_fld* field = FB_NEW_POOL(pool) dsql_fld(pool); - if (field) - { *ptr = field; ptr = &field->fld_next; - // get field information - - field->fld_name = RFR.RDB$FIELD_NAME; - field->fieldSource = QualifiedName(RFR.RDB$FIELD_SOURCE, RFR.RDB$FIELD_SOURCE_SCHEMA_NAME); - field->length = FLX.RDB$FIELD_LENGTH; - field->scale = FLX.RDB$FIELD_SCALE; - field->subType = FLX.RDB$FIELD_SUB_TYPE; field->fld_relation = relation; + field->fld_id = lttField.id; + field->fld_name = lttField.name; + field->fieldSource = lttField.source; - if (!FLX.RDB$COMPUTED_BLR.NULL) - field->flags |= FLD_computed; + field->dtype = lttField.desc.dsc_dtype; + field->length = lttField.desc.dsc_length; + field->scale = lttField.desc.dsc_scale; + field->subType = lttField.desc.dsc_sub_type; + field->segLength = lttField.segLength; + field->charLength = lttField.charLength; + field->precision = lttField.precision; - convert_dtype(field, FLX.RDB$FIELD_TYPE); + if (lttField.charSetId.has_value()) + field->charSetId = lttField.charSetId.value(); - if (FLX.RDB$FIELD_TYPE == blr_blob) { - field->segLength = FLX.RDB$SEGMENT_LENGTH; - } + if (lttField.collationId.has_value()) + field->collationId = lttField.collationId.value(); - if (!FLX.RDB$DIMENSIONS.NULL && FLX.RDB$DIMENSIONS) - { - field->elementDtype = field->dtype; - field->elementLength = field->length; - field->dtype = dtype_array; - field->length = sizeof(ISC_QUAD); - field->dimensions = FLX.RDB$DIMENSIONS; - } + if (!lttField.notNullFlag) + field->flags |= FLD_nullable; + } + } + else + { + static const CachedRequestId handle3bCacheId; + AutoCacheRequest handle3b(tdbb, handle3bCacheId); + + FOR(REQUEST_HANDLE handle3b TRANSACTION_HANDLE transaction) + FLX IN RDB$FIELDS + CROSS RFR IN RDB$RELATION_FIELDS + WITH RFR.RDB$SCHEMA_NAME EQ name.schema.c_str() AND + RFR.RDB$RELATION_NAME EQ name.object.c_str() AND + FLX.RDB$SCHEMA_NAME EQUIV RFR.RDB$FIELD_SOURCE_SCHEMA_NAME AND + FLX.RDB$FIELD_NAME EQ RFR.RDB$FIELD_SOURCE + SORTED BY RFR.RDB$FIELD_POSITION + { + // allocate the field block - if (!FLX.RDB$CHARACTER_SET_ID.NULL) - field->charSetId = FLX.RDB$CHARACTER_SET_ID; + fb_utils::exact_name(RFR.RDB$FIELD_NAME); + fb_utils::exact_name(RFR.RDB$FIELD_SOURCE); - if (!RFR.RDB$COLLATION_ID.NULL) - field->collationId = RFR.RDB$COLLATION_ID; - else if (!FLX.RDB$COLLATION_ID.NULL) - field->collationId = FLX.RDB$COLLATION_ID; + // Allocate from default or permanent pool as appropriate - if (!(RFR.RDB$NULL_FLAG || FLX.RDB$NULL_FLAG) || (relation->rel_flags & REL_view)) + dsql_fld* field = NULL; + + if (!RFR.RDB$FIELD_ID.NULL) { - field->flags |= FLD_nullable; + field = FB_NEW_POOL(pool) dsql_fld(pool); + field->fld_id = RFR.RDB$FIELD_ID; } + else if (!DDL_ids(dsqlScratch)) + field = FB_NEW_POOL(pool) dsql_fld(pool); + + if (field) + { + *ptr = field; + ptr = &field->fld_next; + + // get field information + + field->fld_name = RFR.RDB$FIELD_NAME; + field->fieldSource = QualifiedName(RFR.RDB$FIELD_SOURCE, RFR.RDB$FIELD_SOURCE_SCHEMA_NAME); + field->length = FLX.RDB$FIELD_LENGTH; + field->scale = FLX.RDB$FIELD_SCALE; + field->subType = FLX.RDB$FIELD_SUB_TYPE; + field->fld_relation = relation; - if (RFR.RDB$SYSTEM_FLAG == 1 || FLX.RDB$SYSTEM_FLAG == 1) - field->flags |= FLD_system; + if (!FLX.RDB$COMPUTED_BLR.NULL) + field->flags |= FLD_computed; + + convert_dtype(field, FLX.RDB$FIELD_TYPE); + + if (FLX.RDB$FIELD_TYPE == blr_blob) { + field->segLength = FLX.RDB$SEGMENT_LENGTH; + } + + if (!FLX.RDB$DIMENSIONS.NULL && FLX.RDB$DIMENSIONS) + { + field->elementDtype = field->dtype; + field->elementLength = field->length; + field->dtype = dtype_array; + field->length = sizeof(ISC_QUAD); + field->dimensions = FLX.RDB$DIMENSIONS; + } + + if (!FLX.RDB$CHARACTER_SET_ID.NULL) + field->charSetId = FLX.RDB$CHARACTER_SET_ID; + + if (!RFR.RDB$COLLATION_ID.NULL) + field->collationId = RFR.RDB$COLLATION_ID; + else if (!FLX.RDB$COLLATION_ID.NULL) + field->collationId = FLX.RDB$COLLATION_ID; + + if (!(RFR.RDB$NULL_FLAG || FLX.RDB$NULL_FLAG) || (relation->rel_flags & REL_view)) + { + field->flags |= FLD_nullable; + } + + if (RFR.RDB$SYSTEM_FLAG == 1 || FLX.RDB$SYSTEM_FLAG == 1) + field->flags |= FLD_system; + } } + END_FOR } - END_FOR if (dbb->dbb_relations.get(name, temp) && !(temp->rel_flags & REL_dropped)) { @@ -1366,10 +1441,12 @@ dsql_rel* METD_get_relation(jrd_tra* transaction, DsqlCompilerScratch* dsqlScrat // Add relation to the list - if (permanent) + if (stored) { dbb->dbb_relations.put(relation->rel_name, relation); - MET_dsql_cache_use(tdbb, SYM_relation, relation->rel_name); + + if (!ltt) + MET_dsql_cache_use(tdbb, SYM_relation, relation->rel_name); } else relation->rel_flags |= REL_new_relation; diff --git a/src/dsql/parse-conflicts.txt b/src/dsql/parse-conflicts.txt index 08c7529f156..41dbe10bf44 100644 --- a/src/dsql/parse-conflicts.txt +++ b/src/dsql/parse-conflicts.txt @@ -1 +1 @@ -134 shift/reduce conflicts, 7 reduce/reduce conflicts. +135 shift/reduce conflicts, 7 reduce/reduce conflicts. diff --git a/src/dsql/parse.y b/src/dsql/parse.y index 98625c8b02c..140aa27c903 100644 --- a/src/dsql/parse.y +++ b/src/dsql/parse.y @@ -1661,6 +1661,12 @@ create_clause node->createIfNotExistsOnly = $4; $$ = node; } + | LOCAL TEMPORARY TABLE if_not_exists_opt ltt_table_clause + { + const auto node = $5; + node->createIfNotExistsOnly = $4; + $$ = node; + } | TRIGGER if_not_exists_opt trigger_clause { const auto node = $3; @@ -1765,6 +1771,8 @@ recreate_clause { $$ = newNode($2); } | GLOBAL TEMPORARY TABLE gtt_table_clause { $$ = newNode($4); } + | LOCAL TEMPORARY TABLE ltt_table_clause + { $$ = newNode($4); } | VIEW view_clause { $$ = newNode($2); } | TRIGGER trigger_clause @@ -2407,13 +2415,11 @@ gtt_table_clause : simple_table_name { $$ = newNode($1); - $$->relationType = std::nullopt; + $$->tempFlag = REL_temp_gtt; } '(' table_elements($2) ')' gtt_subclauses_opt($2) { $$ = $2; - if (!$$->relationType.has_value()) - $$->relationType = rel_global_temp_delete; } ; @@ -2433,10 +2439,34 @@ gtt_subclauses($createRelationNode) gtt_subclause($createRelationNode) : sql_security_clause { setClause($createRelationNode->ssDefiner, "SQL SECURITY", $1); } - | ON COMMIT DELETE ROWS - { setClause($createRelationNode->relationType, "ON COMMIT DELETE ROWS", rel_global_temp_delete); } + | temp_table_rows_type($createRelationNode) + ; + +%type temp_table_rows_type() +temp_table_rows_type($createRelationNode) + : ON COMMIT DELETE ROWS + { setClause($createRelationNode->tempRowsFlag, "ON COMMIT DELETE ROWS", REL_temp_tran); } | ON COMMIT PRESERVE ROWS - { setClause($createRelationNode->relationType, "ON COMMIT PRESERVE ROWS", rel_global_temp_preserve); } + { setClause($createRelationNode->tempRowsFlag, "ON COMMIT PRESERVE ROWS", REL_temp_conn); } + ; + +%type ltt_table_clause +ltt_table_clause + : simple_table_name + { + $$ = newNode($1); + $$->tempFlag = REL_temp_ltt; + } + '(' table_elements($2) ')' ltt_subclause_opt($2) + { + $$ = $2; + } + ; + +%type ltt_subclause_opt() +ltt_subclause_opt($createRelationNode) + : // nothing by default. Will be set "on commit delete rows" in dsqlPass + | temp_table_rows_type($createRelationNode) ; %type external_file diff --git a/src/include/firebird/impl/msg/dsql.h b/src/include/firebird/impl/msg/dsql.h index 21ac4ecd321..80fb197c052 100644 --- a/src/include/firebird/impl/msg/dsql.h +++ b/src/include/firebird/impl/msg/dsql.h @@ -38,3 +38,4 @@ FB_IMPL_MSG(DSQL, 38, dsql_no_output_sqlda, -802, "07", "002", "No SQLDA for out FB_IMPL_MSG(DSQL, 39, dsql_wrong_param_num, -313, "07", "001", "Wrong number of parameters (expected @1, got @2)") FB_IMPL_MSG(DSQL, 40, dsql_invalid_drop_ss_clause, -817, "42", "000", "Invalid DROP SQL SECURITY clause") FB_IMPL_MSG(DSQL, 41, upd_ins_cannot_default, -313, "42", "000", "UPDATE OR INSERT value for field @1, part of the implicit or explicit MATCHING clause, cannot be DEFAULT") +FB_IMPL_MSG(DSQL, 42, dsql_ltt_invalid_reference, -607, "42", "000", "LOCAL TEMPORARY TABLE @1 cannot be referenced in @2") diff --git a/src/include/firebird/impl/msg/dyn.h b/src/include/firebird/impl/msg/dyn.h index 598b1da1e64..480663c8fdb 100644 --- a/src/include/firebird/impl/msg/dyn.h +++ b/src/include/firebird/impl/msg/dyn.h @@ -312,3 +312,4 @@ FB_IMPL_MSG(DYN, 319, dyn_cannot_mod_obj_sys_schema, -607, "28", "000", "Cannot FB_IMPL_MSG(DYN, 320, dyn_cannot_create_reserved_schema, -607, "HY", "000", "Schema name @1 is reserved and cannot be created") FB_IMPL_MSG(DYN, 321, dyn_cannot_infer_schema, -901, "42", "000", "Cannot infer schema name as there is no valid schema in the search path") FB_IMPL_MSG_SYMBOL(DYN, 322, dyn_dup_blob_filter, "Blob filter @1 already exists") +FB_IMPL_MSG(DYN, 323, dyn_column_name_exists, -612, "42", "000", "Column @1 already exists in table @2") diff --git a/src/include/gen/Firebird.pas b/src/include/gen/Firebird.pas index eb0cfeae5a0..f5de43ff25f 100644 --- a/src/include/gen/Firebird.pas +++ b/src/include/gen/Firebird.pas @@ -6022,6 +6022,7 @@ IPerformanceStatsImpl = class(IPerformanceStats) isc_dsql_wrong_param_num = 336003111; isc_dsql_invalid_drop_ss_clause = 336003112; isc_upd_ins_cannot_default = 336003113; + isc_dsql_ltt_invalid_reference = 336003114; isc_dyn_filter_not_found = 336068645; isc_dyn_func_not_found = 336068649; isc_dyn_index_not_found = 336068656; @@ -6118,6 +6119,7 @@ IPerformanceStatsImpl = class(IPerformanceStats) isc_dyn_cannot_mod_obj_sys_schema = 336068927; isc_dyn_cannot_create_reserved_schema = 336068928; isc_dyn_cannot_infer_schema = 336068929; + isc_dyn_column_name_exists = 336068931; isc_gbak_unknown_switch = 336330753; isc_gbak_page_size_missing = 336330754; isc_gbak_page_size_toobig = 336330755; diff --git a/src/isql/extract.epp b/src/isql/extract.epp index 52f4876a5a9..1e651a52c98 100644 --- a/src/isql/extract.epp +++ b/src/isql/extract.epp @@ -371,7 +371,7 @@ int EXTRACT_list_table(const QualifiedMetaString& relation_name, IUTILS_name_to_string(ownerName).c_str(), NEWLINE); - if (rel_type == rel_global_temp_preserve || rel_type == rel_global_temp_delete) + if (rel_type == rel_temp_preserve || rel_type == rel_temp_delete) isqlGlob.printf("CREATE GLOBAL TEMPORARY TABLE "); else isqlGlob.printf("CREATE TABLE "); @@ -618,8 +618,8 @@ int EXTRACT_list_table(const QualifiedMetaString& relation_name, if (first) // we extracted nothing return FINI_ERROR; - const char* gtt_scope = (rel_type == rel_global_temp_preserve) ? "ON COMMIT PRESERVE ROWS" : - ((rel_type == rel_global_temp_delete) ? "ON COMMIT DELETE ROWS" : ""); + const char* gtt_scope = (rel_type == rel_temp_preserve) ? "ON COMMIT PRESERVE ROWS" : + ((rel_type == rel_temp_delete) ? "ON COMMIT DELETE ROWS" : ""); const char* opt_delim = *gtt_scope && *ss ? ", " : ""; diff --git a/src/jrd/Attachment.cpp b/src/jrd/Attachment.cpp index 3222bb1a742..ef6befa6089 100644 --- a/src/jrd/Attachment.cpp +++ b/src/jrd/Attachment.cpp @@ -263,6 +263,7 @@ Jrd::Attachment::Attachment(MemoryPool* pool, Database* dbb, JProvider* provider att_unqualified_charset_resolved_cache_search_path(att_schema_search_path), att_unqualified_charset_resolved_cache(*pool), att_parallel_workers(0), + att_local_temporary_tables(*pool), att_repl_appliers(*pool), att_utility(UTIL_NONE), att_procedures(*pool), @@ -434,18 +435,33 @@ void Jrd::Attachment::releaseBatches() delete att_batches.pop(); } -void Jrd::Attachment::releaseGTTs(thread_db* tdbb) +void Jrd::Attachment::releaseTempTables(thread_db* tdbb) { - if (!att_relations) - return; + Array tempRelations; + + if (att_relations) + { + for (FB_SIZE_T i = 1; i < att_relations->count(); i++) + { + const auto relation = (*att_relations)[i]; + if (relation && (relation->rel_flags & REL_temp_gtt)) + tempRelations.add(relation); + } + } + + for (auto& lttEntry : att_local_temporary_tables) + { + const auto ltt = lttEntry.second; + if (ltt->relation) + tempRelations.add(ltt->relation); + } - for (FB_SIZE_T i = 1; i < att_relations->count(); i++) + for (const auto tempRelation : tempRelations) { - jrd_rel* relation = (*att_relations)[i]; - if (relation && (relation->rel_flags & REL_temp_conn) && - !(relation->rel_flags & (REL_deleted | REL_deleting))) + if ((tempRelation->rel_flags & REL_temp_conn) && + !(tempRelation->rel_flags & (REL_deleted | REL_deleting))) { - relation->delPages(tdbb); + tempRelation->delPages(tdbb); } } } @@ -561,8 +577,8 @@ void Jrd::Attachment::resetSession(thread_db* tdbb, jrd_tra** traHandle) if (att_user->resetRole()) SCL_release_all(att_security_classes); - // reset GTT's - releaseGTTs(tdbb); + // reset temporary tables (GTTs and LTTs) + releaseTempTables(tdbb); // Run ON CONNECT trigger after reset if (!(att_flags & ATT_no_db_triggers)) diff --git a/src/jrd/Attachment.h b/src/jrd/Attachment.h index 26097f0dfe7..201a8dd5642 100644 --- a/src/jrd/Attachment.h +++ b/src/jrd/Attachment.h @@ -33,6 +33,7 @@ #include "../jrd/RandomGenerator.h" #include "../jrd/RuntimeStatistics.h" #include "../jrd/Coercion.h" +#include "../jrd/LocalTemporaryTable.h" #include "../common/classes/ByteChunk.h" #include "../common/classes/GenericMap.h" @@ -51,6 +52,7 @@ #include #include +#include //#define DEBUG_LCK_LIST @@ -661,6 +663,9 @@ class Attachment : public pool_alloc PageToBufferMap* att_bdb_cache; // managed in CCH, created in att_pool, freed with it + Firebird::LeftPooledMap att_local_temporary_tables; + std::optional att_next_ltt_id; // Next available LTT relation ID + Firebird::RefPtr att_replicator; Firebird::AutoPtr att_repl_matcher; Firebird::Array att_repl_appliers; @@ -746,7 +751,7 @@ class Attachment : public pool_alloc const Firebird::ByteChunk& chunk); void releaseBatches(); - void releaseGTTs(thread_db* tdbb); + void releaseTempTables(thread_db* tdbb); void resetSession(thread_db* tdbb, jrd_tra** traHandle); void signalCancel(); diff --git a/src/jrd/LocalTemporaryTable.h b/src/jrd/LocalTemporaryTable.h new file mode 100644 index 00000000000..af9b2a56cff --- /dev/null +++ b/src/jrd/LocalTemporaryTable.h @@ -0,0 +1,151 @@ +/* + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.0 (the "License"); + * you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * http://www.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl. + * + * Software distributed under the License is distributed AS IS, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. + * See the License for the specific language governing rights + * and limitations under the License. + * + * The Original Code was created by Adriano dos Santos Fernandes + * for the Firebird Open Source RDBMS project. + * + * Copyright (c) 2025 Adriano dos Santos Fernandes + * and all contributors signed below. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + */ + +#ifndef JRD_LOCAL_TEMPORARY_TABLE_H +#define JRD_LOCAL_TEMPORARY_TABLE_H + +#include "firebird.h" +#include "../jrd/constants.h" +#include "../jrd/MetaName.h" +#include "../jrd/QualifiedName.h" +#include "../common/dsc.h" +#include "../common/classes/alloc.h" +#include "../common/classes/objects_array.h" +#include + +namespace Jrd +{ + class jrd_rel; + + class LocalTemporaryTable + { + public: + class Field + { + public: + explicit Field(MemoryPool& pool) + : name(pool) + { + desc.clear(); + } + + Field(MemoryPool& pool, const Field& other) + : name(pool, other.name), + source(pool, other.source), + collationId(other.collationId), + charSetId(other.charSetId), + id(other.id), + position(other.position), + segLength(other.segLength), + charLength(other.charLength), + precision(other.precision), + notNullFlag(other.notNullFlag), + desc(other.desc) + { + } + + public: + MetaName name; + QualifiedName source; + std::optional collationId; + std::optional charSetId; + USHORT id = 0; + USHORT position = 0; + USHORT segLength = 0; + USHORT charLength = 0; + USHORT precision = 0; + bool notNullFlag = false; + dsc desc; + }; + + class Index + { + public: + explicit Index(MemoryPool& pool) + : name(pool), + columns(pool) + { + } + + Index(MemoryPool& pool, const QualifiedName& aName) + : name(pool, aName), + columns(pool) + { + } + + Index(MemoryPool& pool, const Index& other) + : name(pool, other.name), + columns(pool, other.columns), + unique(other.unique), + descending(other.descending), + inactive(other.inactive), + id(other.id) + { + } + + public: + QualifiedName name; + Firebird::ObjectsArray columns; + bool unique = false; + bool descending = false; + bool inactive = false; + USHORT id = 0; + }; + + public: + LocalTemporaryTable(MemoryPool& pool, const QualifiedName& aName) + : name(pool, aName), + fields(pool), + pendingFields(pool), + indexes(pool) + { + } + + LocalTemporaryTable(MemoryPool& pool, const LocalTemporaryTable& other) + : name(pool, other.name), + fields(pool, other.fields), + pendingFields(pool, other.pendingFields), + indexes(pool, other.indexes), + relationType(other.relationType), + relation(other.relation), + relationId(other.relationId), + nextFieldId(other.nextFieldId), + nextIndexId(other.nextIndexId), + hasPendingChanges(other.hasPendingChanges) + { + } + + public: + QualifiedName name; + Firebird::ObjectsArray fields; // Committed state (stable) + Firebird::ObjectsArray pendingFields; // Uncommitted changes (working copy) + Firebird::ObjectsArray indexes; + rel_t relationType; + jrd_rel* relation = nullptr; + USHORT relationId = 0; + USHORT nextFieldId = 0; + USHORT nextIndexId = 0; + bool hasPendingChanges = false; + }; +} // namespace Jrd + +#endif // JRD_LOCAL_TEMPORARY_TABLE_H diff --git a/src/jrd/Monitoring.cpp b/src/jrd/Monitoring.cpp index 7e56fa88388..e3a94a889f6 100644 --- a/src/jrd/Monitoring.cpp +++ b/src/jrd/Monitoring.cpp @@ -1023,20 +1023,20 @@ void Monitoring::putDatabase(thread_db* tdbb, SnapshotData::DumpRecord& record) if (dbb->dbb_flags & DBB_shared) { MutexLockGuard guard(dbb->dbb_stats_mutex, FB_FUNCTION); - putStatistics(record, dbb->dbb_stats, stat_id, stat_database); + putStatistics(tdbb, record, dbb->dbb_stats, stat_id, stat_database); putMemoryUsage(record, dbb->dbb_memory_stats, stat_id, stat_database); } else { RuntimeStatistics zero_rt_stats; MemoryStats zero_mem_stats; - putStatistics(record, zero_rt_stats, stat_id, stat_database); + putStatistics(tdbb, record, zero_rt_stats, stat_id, stat_database); putMemoryUsage(record, zero_mem_stats, stat_id, stat_database); } } -void Monitoring::putAttachment(SnapshotData::DumpRecord& record, const Jrd::Attachment* attachment) +void Monitoring::putAttachment(thread_db* tdbb, SnapshotData::DumpRecord& record, const Jrd::Attachment* attachment) { fb_assert(attachment); if (!attachment->att_user) @@ -1141,13 +1141,13 @@ void Monitoring::putAttachment(SnapshotData::DumpRecord& record, const Jrd::Atta if (attachment->att_database->dbb_flags & DBB_shared) { - putStatistics(record, attachment->att_stats, stat_id, stat_attachment); + putStatistics(tdbb, record, attachment->att_stats, stat_id, stat_attachment); putMemoryUsage(record, attachment->att_memory_stats, stat_id, stat_attachment); } else { MutexLockGuard guard(attachment->att_database->dbb_stats_mutex, FB_FUNCTION); - putStatistics(record, attachment->att_database->dbb_stats, stat_id, stat_attachment); + putStatistics(tdbb, record, attachment->att_database->dbb_stats, stat_id, stat_attachment); putMemoryUsage(record, attachment->att_database->dbb_memory_stats, stat_id, stat_attachment); } @@ -1156,7 +1156,7 @@ void Monitoring::putAttachment(SnapshotData::DumpRecord& record, const Jrd::Atta } -void Monitoring::putTransaction(SnapshotData::DumpRecord& record, const jrd_tra* transaction) +void Monitoring::putTransaction(thread_db* tdbb, SnapshotData::DumpRecord& record, const jrd_tra* transaction) { fb_assert(transaction); @@ -1220,7 +1220,7 @@ void Monitoring::putTransaction(SnapshotData::DumpRecord& record, const jrd_tra* record.write(); - putStatistics(record, transaction->tra_stats, stat_id, stat_transaction); + putStatistics(tdbb, record, transaction->tra_stats, stat_id, stat_transaction); putMemoryUsage(record, transaction->tra_memory_stats, stat_id, stat_transaction); // context vars @@ -1274,7 +1274,7 @@ void Monitoring::putStatement(SnapshotData::DumpRecord& record, const Statement* } -void Monitoring::putRequest(SnapshotData::DumpRecord& record, const Request* request, +void Monitoring::putRequest(thread_db* tdbb, SnapshotData::DumpRecord& record, const Request* request, const string& plan) { fb_assert(request); @@ -1333,12 +1333,12 @@ void Monitoring::putRequest(SnapshotData::DumpRecord& record, const Request* req record.write(); - putStatistics(record, request->req_stats, stat_id, stat_statement); + putStatistics(tdbb, record, request->req_stats, stat_id, stat_statement); putMemoryUsage(record, request->req_memory_stats, stat_id, stat_statement); } -void Monitoring::putCall(SnapshotData::DumpRecord& record, const Request* request) +void Monitoring::putCall(thread_db* tdbb, SnapshotData::DumpRecord& record, const Request* request) { fb_assert(request); @@ -1405,12 +1405,12 @@ void Monitoring::putCall(SnapshotData::DumpRecord& record, const Request* reques record.write(); - putStatistics(record, request->req_stats, stat_id, stat_call); + putStatistics(tdbb, record, request->req_stats, stat_id, stat_call); putMemoryUsage(record, request->req_memory_stats, stat_id, stat_call); } -void Monitoring::putStatistics(SnapshotData::DumpRecord& record, const RuntimeStatistics& statistics, +void Monitoring::putStatistics(thread_db* tdbb, SnapshotData::DumpRecord& record, const RuntimeStatistics& statistics, int stat_id, int stat_group) { // statistics id @@ -1459,8 +1459,34 @@ void Monitoring::putStatistics(SnapshotData::DumpRecord& record, const RuntimeSt record.reset(rel_mon_tab_stats); record.storeGlobalId(f_mon_tab_stat_id, id); record.storeInteger(f_mon_tab_stat_group, stat_group); - record.storeTableIdSchemaName(f_mon_tab_sch_name, counts.getGroupId()); - record.storeTableIdObjectName(f_mon_tab_name, counts.getGroupId()); + + bool lttFound = false; + std::string_view tableType = "PERSISTENT"; + + if (stat_group != stat_database) + { + if (const auto relation = MET_lookup_relation_id(tdbb, counts.getGroupId(), false)) + { + if ((relation->rel_flags & REL_temp_ltt)) + { + record.storeString(f_mon_tab_sch_name, relation->rel_name.schema); + record.storeString(f_mon_tab_name, relation->rel_name.object); + lttFound = true; + tableType = "LOCAL TEMPORARY"; + } + else if ((relation->rel_flags & REL_temp_gtt)) + tableType = "GLOBAL TEMPORARY"; + } + } + + if (!lttFound) + { + record.storeTableIdSchemaName(f_mon_tab_sch_name, counts.getGroupId()); + record.storeTableIdObjectName(f_mon_tab_name, counts.getGroupId()); + } + + record.storeString(f_mon_tab_type, tableType); + record.storeGlobalId(f_mon_tab_rec_stat_id, rec_stat_id); record.write(); @@ -1572,7 +1598,7 @@ void Monitoring::dumpAttachment(thread_db* tdbb, Attachment* attachment, ULONG g DumpWriter writer(dbb->dbb_monitoring_data, attId, userName.c_str(), generation); SnapshotData::DumpRecord record(pool, writer); - putAttachment(record, attachment); + putAttachment(tdbb, record, attachment); jrd_tra* transaction = nullptr; @@ -1581,7 +1607,7 @@ void Monitoring::dumpAttachment(thread_db* tdbb, Attachment* attachment, ULONG g for (transaction = attachment->att_transactions; transaction; transaction = transaction->tra_next) { - putTransaction(record, transaction); + putTransaction(tdbb, record, transaction); } // Call stack information @@ -1599,7 +1625,7 @@ void Monitoring::dumpAttachment(thread_db* tdbb, Attachment* attachment, ULONG g (Statement::FLAG_INTERNAL | Statement::FLAG_SYS_TRIGGER)) && request->req_caller) { - putCall(record, request); + putCall(tdbb, record, request); } } } @@ -1628,7 +1654,7 @@ void Monitoring::dumpAttachment(thread_db* tdbb, Attachment* attachment, ULONG g { const string plan = (dbb->getEncodedOdsVersion() >= ODS_13_1) ? "" : Optimizer::getPlan(tdbb, statement, true); - putRequest(record, request, plan); + putRequest(tdbb, record, request, plan); } } } diff --git a/src/jrd/Monitoring.h b/src/jrd/Monitoring.h index 80e4b1665c0..914bf385f11 100644 --- a/src/jrd/Monitoring.h +++ b/src/jrd/Monitoring.h @@ -31,6 +31,7 @@ #include "../jrd/val.h" #include "../jrd/recsrc/RecordSource.h" #include "../jrd/TempSpace.h" +#include namespace Jrd { @@ -162,6 +163,12 @@ class SnapshotData storeField(field_id, VALUE_STRING, value.length(), value.c_str()); } + void storeString(int field_id, const std::string_view value) + { + if (!value.empty()) + storeField(field_id, VALUE_STRING, value.length(), value.data()); + } + void storeBoolean(int field_id, bool value) { const UCHAR boolean = value ? 1 : 0; @@ -413,12 +420,12 @@ class Monitoring private: static SINT64 getGlobalId(int) noexcept; - static void putAttachment(SnapshotData::DumpRecord&, const Attachment*); - static void putTransaction(SnapshotData::DumpRecord&, const jrd_tra*); + static void putAttachment(thread_db*, SnapshotData::DumpRecord&, const Attachment*); + static void putTransaction(thread_db*, SnapshotData::DumpRecord&, const jrd_tra*); static void putStatement(SnapshotData::DumpRecord&, const Statement*, const Firebird::string&); - static void putRequest(SnapshotData::DumpRecord&, const Request*, const Firebird::string&); - static void putCall(SnapshotData::DumpRecord&, const Request*); - static void putStatistics(SnapshotData::DumpRecord&, const RuntimeStatistics&, int, int); + static void putRequest(thread_db*, SnapshotData::DumpRecord&, const Request*, const Firebird::string&); + static void putCall(thread_db*, SnapshotData::DumpRecord&, const Request*); + static void putStatistics(thread_db*, SnapshotData::DumpRecord&, const RuntimeStatistics&, int, int); static void putContextVars(SnapshotData::DumpRecord&, const Firebird::StringMap&, SINT64, bool); static void putMemoryUsage(SnapshotData::DumpRecord&, const Firebird::MemoryStats&, int, int); }; diff --git a/src/jrd/RecordSourceNodes.cpp b/src/jrd/RecordSourceNodes.cpp index 5e892959c32..3f014946c4e 100644 --- a/src/jrd/RecordSourceNodes.cpp +++ b/src/jrd/RecordSourceNodes.cpp @@ -64,7 +64,7 @@ namespace typedef HalfStaticArray SpecialJoinList; - void appendContextAlias(DsqlCompilerScratch* dsqlScratch, const string& alias) + void appendContextAlias(DsqlCompilerScratch* dsqlScratch, const string& alias) { const auto len = alias.length(); if (len <= MAX_UCHAR) @@ -888,7 +888,7 @@ void RelationSourceNode::genBlr(DsqlCompilerScratch* dsqlScratch) // if this is a trigger or procedure, don't want relation id used - if (DDL_ids(dsqlScratch)) + if (DDL_ids(dsqlScratch) && !(relation->rel_flags & REL_ltt_created)) { dsqlScratch->appendUChar(dsqlContext->ctx_alias.hasData() ? blr_rid2 : blr_rid); dsqlScratch->appendUShort(relation->rel_id); diff --git a/src/jrd/Relation.cpp b/src/jrd/Relation.cpp index 9713d45344b..c6d7c55fa5d 100644 --- a/src/jrd/Relation.cpp +++ b/src/jrd/Relation.cpp @@ -83,7 +83,7 @@ RelationPages* jrd_rel::getPagesInternal(thread_db* tdbb, TraNumber tran, bool a if (!rel_pages_inst->find(inst_id, pos)) { if (!allocPages) - return 0; + return nullptr; RelationPages* newPages = rel_pages_free; if (!newPages) { diff --git a/src/jrd/Relation.h b/src/jrd/Relation.h index cfd6a93cdc9..ec3b02d5de0 100644 --- a/src/jrd/Relation.h +++ b/src/jrd/Relation.h @@ -387,24 +387,26 @@ class jrd_rel : public pool_alloc // rel_flags -inline constexpr ULONG REL_scanned = 0x0001; // Field expressions scanned (or being scanned) -inline constexpr ULONG REL_system = 0x0002; -inline constexpr ULONG REL_deleted = 0x0004; // Relation known gonzo -inline constexpr ULONG REL_get_dependencies = 0x0008; // New relation needs dependencies during scan -inline constexpr ULONG REL_check_existence = 0x0010; // Existence lock released pending drop of relation -inline constexpr ULONG REL_blocking = 0x0020; // Blocking someone from dropping relation -inline constexpr ULONG REL_sql_relation = 0x0080; // Relation defined as sql table -inline constexpr ULONG REL_check_partners = 0x0100; // Rescan primary dependencies and foreign references -inline constexpr ULONG REL_being_scanned = 0x0200; // relation scan in progress -inline constexpr ULONG REL_deleting = 0x0800; // relation delete in progress -inline constexpr ULONG REL_temp_tran = 0x1000; // relation is a GTT delete rows -inline constexpr ULONG REL_temp_conn = 0x2000; // relation is a GTT preserve rows -inline constexpr ULONG REL_virtual = 0x4000; // relation is virtual -inline constexpr ULONG REL_jrd_view = 0x8000; // relation is VIEW -inline constexpr ULONG REL_gc_blocking = 0x10000; // request to downgrade\release gc lock -inline constexpr ULONG REL_gc_disabled = 0x20000; // gc is disabled temporarily -inline constexpr ULONG REL_gc_lockneed = 0x40000; // gc lock should be acquired -inline constexpr ULONG REL_rescan = 0x100000; // rescan request was submitted while relation being scanning +inline constexpr ULONG REL_scanned = 0x0001; // Field expressions scanned (or being scanned) +inline constexpr ULONG REL_system = 0x0002; +inline constexpr ULONG REL_deleted = 0x0004; // Relation known gonzo +inline constexpr ULONG REL_get_dependencies = 0x0008; // New relation needs dependencies during scan +inline constexpr ULONG REL_check_existence = 0x0010; // Existence lock released pending drop of relation +inline constexpr ULONG REL_blocking = 0x0020; // Blocking someone from dropping relation +inline constexpr ULONG REL_sql_relation = 0x0080; // Relation defined as sql table +inline constexpr ULONG REL_check_partners = 0x0100; // Rescan primary dependencies and foreign references +inline constexpr ULONG REL_being_scanned = 0x0200; // relation scan in progress +inline constexpr ULONG REL_deleting = 0x0800; // relation delete in progress +inline constexpr ULONG REL_temp_tran = 0x1000; // relation is a GTT delete rows +inline constexpr ULONG REL_temp_conn = 0x2000; // relation is a GTT preserve rows +inline constexpr ULONG REL_virtual = 0x4000; // relation is virtual +inline constexpr ULONG REL_jrd_view = 0x8000; // relation is VIEW +inline constexpr ULONG REL_gc_blocking = 0x10000; // request to downgrade\release gc lock +inline constexpr ULONG REL_gc_disabled = 0x20000; // gc is disabled temporarily +inline constexpr ULONG REL_gc_lockneed = 0x40000; // gc lock should be acquired +inline constexpr ULONG REL_temp_gtt = 0x80000; // relation is a GTT +inline constexpr ULONG REL_temp_ltt = 0x100000; // relation is a LTT +inline constexpr ULONG REL_rescan = 0x200000; // rescan request was submitted while relation being scanning /// class jrd_rel diff --git a/src/jrd/Savepoint.cpp b/src/jrd/Savepoint.cpp index 852df16f181..72702ab955d 100644 --- a/src/jrd/Savepoint.cpp +++ b/src/jrd/Savepoint.cpp @@ -19,6 +19,7 @@ #include "firebird.h" #include "../common/gdsassert.h" +#include "../jrd/Attachment.h" #include "../jrd/tra.h" #include "../jrd/blb_proto.h" #include "../jrd/cch_proto.h" @@ -26,6 +27,7 @@ #include "../jrd/dpm_proto.h" #include "../jrd/idx_proto.h" #include "../jrd/vio_proto.h" +#include "../jrd/LocalTemporaryTable.h" #include "Savepoint.h" @@ -393,9 +395,19 @@ VerbAction* Savepoint::createAction(jrd_rel* relation) } +void Savepoint::createLttAction(LttUndoItem::UndoType type, const QualifiedName& name, + LocalTemporaryTable* original) +{ + // Create LTT undo action. For ALTER, make a copy of the original LTT state. + const auto item = FB_NEW_POOL(*m_transaction->tra_pool) LttUndoItem(type, name, original); + item->next = m_lttActions; + m_lttActions = item; +} + + void Savepoint::cleanupTempData() { - // Find all global temporary tables with DELETE ROWS action + // Find all temporary tables with DELETE ROWS action // and release their undo data for (VerbAction* action = m_actions; action; action = action->vct_next) @@ -453,6 +465,46 @@ Savepoint* Savepoint::rollback(thread_db* tdbb, Savepoint* prior, bool preserveL m_freeActions = action; } + // Undo LTT changes + const auto attachment = m_transaction->tra_attachment; + auto& lttMap = attachment->att_local_temporary_tables; + + while (m_lttActions) + { + const auto item = m_lttActions; + m_lttActions = item->next; + + switch (item->type) + { + case LttUndoItem::LTT_UNDO_CREATE: + // LTT was created in this savepoint - remove it + if (const auto lttPtr = lttMap.get(item->name)) + { + delete (*lttPtr)->relation; + delete *lttPtr; + lttMap.remove(item->name); + } + break; + + case LttUndoItem::LTT_UNDO_ALTER: + // LTT was altered - restore original state + if (const auto lttPtr = lttMap.get(item->name)) + { + delete *lttPtr; + *lttPtr = item->original.release(); + } + break; + + case LttUndoItem::LTT_UNDO_DROP: + // LTT was dropped - restore it + if (!lttMap.exist(item->name)) + lttMap.put(item->name, item->original.release()); + break; + } + + delete item; + } + tdbb->setTransaction(old_tran); } catch (const Exception& ex) @@ -532,6 +584,95 @@ Savepoint* Savepoint::rollforward(thread_db* tdbb, Savepoint* prior) m_freeActions = action; } + // Merge LTT undo data to parent savepoint + // When releasing a savepoint, we need to keep the undo data so that + // rolling back a parent savepoint will also undo the LTT changes + if (m_next) + { + while (m_lttActions) + { + const auto item = m_lttActions; + m_lttActions = item->next; + + // Check if parent savepoint already has an undo item for the same LTT + LttUndoItem* parentItem = nullptr; + LttUndoItem** parentItemPtr = nullptr; + + for (auto lookupItemPtr = &m_next->m_lttActions; + *lookupItemPtr; + lookupItemPtr = &(*lookupItemPtr)->next) + { + if ((*lookupItemPtr)->name == item->name) + { + parentItem = *lookupItemPtr; + parentItemPtr = lookupItemPtr; + break; + } + } + + if (parentItem) + { + // Parent already has an undo item for this LTT. + // Determine what to do based on the combination of types. + bool keepParent = true; + + if (item->type == LttUndoItem::LTT_UNDO_DROP) + { + if (parentItem->type == LttUndoItem::LTT_UNDO_CREATE) + { + // CREATE followed by DROP: they cancel out - remove both + *parentItemPtr = parentItem->next; + delete parentItem; + delete item; + continue; + } + else if (parentItem->type == LttUndoItem::LTT_UNDO_ALTER) + { + // ALTER followed by DROP: replace parent's ALTER with DROP + // (DROP undo contains the original full LTT state) + *parentItemPtr = parentItem->next; + delete parentItem; + keepParent = false; + } + // DROP followed by DROP: keep parent's (shouldn't normally happen) + } + // Other cases (ALTER after CREATE, ALTER after ALTER, etc.): + // keep parent's undo (oldest state) + + if (keepParent) + { + // In case of DROP (parent) -> CREATE (current), we must keep both + // to ensure that on rollback we first drop the new table and then + // restore the original one. + if (item->type == LttUndoItem::LTT_UNDO_CREATE && + parentItem->type == LttUndoItem::LTT_UNDO_DROP) + { + // do nothing, fall through to add item + } + else + { + delete item; + continue; + } + } + } + + // Move item to parent savepoint + item->next = m_next->m_lttActions; + m_next->m_lttActions = item; + } + } + else + { + // No parent savepoint - just discard undo data + while (m_lttActions) + { + const auto item = m_lttActions; + m_lttActions = item->next; + delete item; + } + } + tdbb->setTransaction(old_tran); tdbb->tdbb_flags &= ~TDBB_verb_cleanup; } diff --git a/src/jrd/Savepoint.h b/src/jrd/Savepoint.h index cc628cf637e..f6f1841b004 100644 --- a/src/jrd/Savepoint.h +++ b/src/jrd/Savepoint.h @@ -22,12 +22,14 @@ #include "../common/classes/File.h" #include "../jrd/MetaName.h" +#include "../jrd/QualifiedName.h" #include "../jrd/Record.h" #include "../jrd/RecordNumber.h" namespace Jrd { class jrd_tra; + class LocalTemporaryTable; // Verb actions @@ -103,6 +105,30 @@ namespace Jrd void release(jrd_tra* transaction); }; + // LTT undo item - stores original state of a LocalTemporaryTable for savepoint rollback + class LttUndoItem + { + public: + enum UndoType + { + LTT_UNDO_CREATE, // LTT was created - on rollback, remove it + LTT_UNDO_ALTER, // LTT was altered - on rollback, restore original state + LTT_UNDO_DROP // LTT was dropped - on rollback, restore it + }; + + LttUndoItem(UndoType aType, const QualifiedName& aName, LocalTemporaryTable* aOriginal = nullptr) + : type(aType), + name(aName), + original(aOriginal), + next(nullptr) + {} + + UndoType type; + QualifiedName name; + Firebird::AutoPtr original; // Original LTT state (for ALTER/DROP), nullptr for CREATE + LttUndoItem* next; + }; + // Savepoint class class Savepoint @@ -119,8 +145,7 @@ namespace Jrd public: explicit Savepoint(jrd_tra* transaction) - : m_transaction(transaction), m_number(0), m_flags(0), m_count(0), - m_next(NULL), m_actions(NULL), m_freeActions(NULL) + : m_transaction(transaction) {} ~Savepoint() @@ -138,6 +163,13 @@ namespace Jrd delete m_freeActions; m_freeActions = next; } + + while (m_lttActions) + { + LttUndoItem* next = m_lttActions->next; + delete m_lttActions; + m_lttActions = next; + } } void init(SavNumber number, bool root, Savepoint* next) @@ -229,6 +261,8 @@ namespace Jrd } VerbAction* createAction(jrd_rel* relation); + void createLttAction(LttUndoItem::UndoType type, const QualifiedName& name, + LocalTemporaryTable* original = nullptr); void cleanupTempData(); @@ -321,16 +355,15 @@ namespace Jrd bool isLarge() const; Savepoint* release(Savepoint* prior = NULL); - jrd_tra* const m_transaction; // transaction this savepoint belongs to - SavNumber m_number; // savepoint number - USHORT m_flags; // misc flags - USHORT m_count; // active verb count - MetaName m_name; // savepoint name - Savepoint* m_next; // next savepoint in the list - - - VerbAction* m_actions; // verb action list - VerbAction* m_freeActions; // free verb actions + jrd_tra* const m_transaction; // transaction this savepoint belongs to + SavNumber m_number = 0; // savepoint number + USHORT m_flags = 0; // misc flags + USHORT m_count = 0; // active verb count + MetaName m_name; // savepoint name + Savepoint* m_next = nullptr; // next savepoint in the list + VerbAction* m_actions = nullptr; // verb action list + VerbAction* m_freeActions = nullptr; // free verb actions + LttUndoItem* m_lttActions = nullptr; // LTT undo action list }; // Start a savepoint and rollback it in destructor, diff --git a/src/jrd/blb.cpp b/src/jrd/blb.cpp index 95e385dc990..d72ddd8efba 100644 --- a/src/jrd/blb.cpp +++ b/src/jrd/blb.cpp @@ -1454,14 +1454,21 @@ blb* blb::open2(thread_db* tdbb, // know about the relation, the blob id has got to be invalid // anyway. - vec* vector = tdbb->getAttachment()->att_relations; + const auto relationId = blobId.bid_internal.bid_relation_id; - if (blobId.bid_internal.bid_relation_id >= vector->count() || - !(blob->blb_relation = (*vector)[blobId.bid_internal.bid_relation_id] ) ) + if (relationId >= MIN_LTT_ID && relationId <= MAX_LTT_ID) + blob->blb_relation = MET_relation(tdbb, relationId); + else { - ERR_post(Arg::Gds(isc_bad_segstr_id)); + vec* vector = tdbb->getAttachment()->att_relations; + + if (blobId.bid_internal.bid_relation_id < vector->count()) + blob->blb_relation = (*vector)[relationId]; } + if (!blob->blb_relation) + ERR_post(Arg::Gds(isc_bad_segstr_id)); + blob->blb_pg_space_id = blob->blb_relation->getPages(tdbb)->rel_pg_space_id; DPM_get_blob(tdbb, blob, blobId.get_permanent_number(), false, 0); diff --git a/src/jrd/btr.cpp b/src/jrd/btr.cpp index 5554dd4910c..ded2e2d8f69 100644 --- a/src/jrd/btr.cpp +++ b/src/jrd/btr.cpp @@ -2286,7 +2286,7 @@ void BTR_reserve_slot(thread_db* tdbb, IndexCreation& creation) // Leave the root pointer null for the time being. // Index id for temporary index instance of global temporary table is // already assigned, use it. - const bool use_idx_id = (relPages->rel_instance_id != 0); + const bool use_idx_id = relPages->rel_instance_id != 0 || (relation->rel_flags & REL_temp_ltt); if (use_idx_id) fb_assert(idx->idx_id <= dbb->dbb_max_idx); diff --git a/src/jrd/cmp.cpp b/src/jrd/cmp.cpp index a94dd1570c4..566489a0f02 100644 --- a/src/jrd/cmp.cpp +++ b/src/jrd/cmp.cpp @@ -295,9 +295,8 @@ IndexLock* CMP_get_index_lock(thread_db* tdbb, jrd_rel* relation, USHORT id) DEV_BLKCHK(relation, type_rel); - if (relation->rel_id < (USHORT) rel_MAX) { - return NULL; - } + if (relation->rel_id < (USHORT) rel_MAX || (relation->rel_flags & REL_temp_ltt)) + return nullptr; // for to find an existing block diff --git a/src/jrd/constants.h b/src/jrd/constants.h index 54102cb4a54..b173267e3e7 100644 --- a/src/jrd/constants.h +++ b/src/jrd/constants.h @@ -233,8 +233,8 @@ enum rel_t { rel_view = 1, rel_external = 2, rel_virtual = 3, - rel_global_temp_preserve = 4, - rel_global_temp_delete = 5 + rel_temp_preserve = 4, + rel_temp_delete = 5 }; // procedure types diff --git a/src/jrd/dfw.epp b/src/jrd/dfw.epp index b76345b78c4..896aa993c8b 100644 --- a/src/jrd/dfw.epp +++ b/src/jrd/dfw.epp @@ -64,6 +64,7 @@ #include #include #include +#include #include "../common/classes/fb_string.h" #include "../common/classes/VaryStr.h" @@ -108,6 +109,7 @@ #include "../jrd/lck_proto.h" #include "../jrd/met_proto.h" #include "../jrd/mov_proto.h" +#include "../jrd/par_proto.h" #include "../jrd/pag_proto.h" #include "../jrd/os/pio_proto.h" #include "../jrd/rlck_proto.h" @@ -128,6 +130,7 @@ #include "../jrd/CryptoManager.h" #include "../jrd/Mapping.h" #include "../jrd/shut_proto.h" +#include "../dsql/metd_proto.h" #ifdef HAVE_UNISTD_H #include @@ -138,11 +141,6 @@ // Pick up system relation ids #include "../jrd/ini.h" -// Define range of user relation ids - -inline constexpr int MIN_RELATION_ID = rel_MAX; -inline constexpr int MAX_RELATION_ID = 32767; - inline constexpr int COMPUTED_FLAG = 128; inline constexpr int WAIT_PERIOD = -1; @@ -394,9 +392,12 @@ public: void addRelation(jrd_rel* relation) { - FB_SIZE_T pos; - if (!m_locks.find(relation->rel_id, pos)) - m_locks.insert(pos, relLock(relation)); + if (!(relation->rel_flags & REL_temp_ltt)) + { + FB_SIZE_T pos; + if (!m_locks.find(relation->rel_id, pos)) + m_locks.insert(pos, relLock(relation)); + } } bool exists(USHORT rel_id) const @@ -465,7 +466,10 @@ static bool delete_shadow(thread_db*, SSHORT, DeferredWork*, jrd_tra*); static bool compute_security(thread_db*, SSHORT, DeferredWork*, jrd_tra*); static bool modify_index(thread_db*, SSHORT, DeferredWork*, jrd_tra*); static bool create_index(thread_db*, SSHORT, DeferredWork*, jrd_tra*); +static bool modify_ltt_index(thread_db*, SSHORT, DeferredWork*, jrd_tra*); static bool delete_index(thread_db*, SSHORT, DeferredWork*, jrd_tra*); +static bool create_ltt_index(thread_db*, SSHORT, DeferredWork*, jrd_tra*); +static bool delete_ltt_index(thread_db*, SSHORT, DeferredWork*, jrd_tra*); static bool create_relation(thread_db*, SSHORT, DeferredWork*, jrd_tra*); static bool delete_relation(thread_db*, SSHORT, DeferredWork*, jrd_tra*); static bool scan_relation(thread_db*, SSHORT, DeferredWork*, jrd_tra*); @@ -484,6 +488,7 @@ static bool delete_global(thread_db*, SSHORT, DeferredWork*, jrd_tra*); static bool delete_parameter(thread_db*, SSHORT, DeferredWork*, jrd_tra*); static bool delete_rfr(thread_db*, SSHORT, DeferredWork*, jrd_tra*); static bool make_version(thread_db*, SSHORT, DeferredWork*, jrd_tra*); +static bool make_ltt_version(thread_db*, SSHORT, DeferredWork*, jrd_tra*); static bool add_difference(thread_db*, SSHORT, DeferredWork*, jrd_tra*); static bool delete_difference(thread_db*, SSHORT, DeferredWork*, jrd_tra*); static bool begin_backup(thread_db*, SSHORT, DeferredWork*, jrd_tra*); @@ -1289,6 +1294,7 @@ static inline constexpr deferred_task task_table[] = { { dfw_add_shadow, add_shadow }, { dfw_delete_index, modify_index }, + { dfw_delete_ltt_index, delete_ltt_index }, { dfw_delete_rfr, delete_rfr }, { dfw_delete_relation, delete_relation }, { dfw_delete_shadow, delete_shadow }, @@ -1299,10 +1305,13 @@ static inline constexpr deferred_task task_table[] = { dfw_delete_global, delete_global }, { dfw_create_relation, create_relation }, { dfw_update_format, make_version }, + { dfw_update_ltt_format, make_ltt_version }, { dfw_scan_relation, scan_relation }, { dfw_compute_security, compute_security }, { dfw_create_index, modify_index }, { dfw_create_expression_index, modify_index }, + { dfw_create_ltt_index, create_ltt_index }, + { dfw_modify_ltt_index, modify_ltt_index }, { dfw_grant, grant_privileges }, { dfw_create_trigger, create_trigger }, { dfw_delete_trigger, delete_trigger }, @@ -2245,29 +2254,54 @@ static bool check_not_null(thread_db* tdbb, SSHORT phase, DeferredWork* work, jr if (!relation || relation->rel_view_rse || work->dfw_ids.isEmpty()) break; + // This function is not working correctly for GTTs and LTTs + if (relation->isTemporary()) + break; + // Protect relation from modification ProtectRelations protectRelation(tdbb, transaction, relation); SortedArray fields; AutoRequest handle; - for (SortedArray::iterator itr(work->dfw_ids.begin()); - itr != work->dfw_ids.end(); - ++itr) + if (relation->rel_flags & REL_temp_ltt) // Not used currently { - FOR(REQUEST_HANDLE handle) - RFL IN RDB$RELATION_FIELDS CROSS - FLD IN RDB$FIELDS - WITH RFL.RDB$SCHEMA_NAME EQ work->dfw_schema.c_str() AND - RFL.RDB$RELATION_NAME EQ work->dfw_name.c_str() AND - FLD.RDB$SCHEMA_NAME EQ RFL.RDB$FIELD_SOURCE_SCHEMA_NAME AND - FLD.RDB$FIELD_NAME EQ RFL.RDB$FIELD_SOURCE AND - RFL.RDB$FIELD_ID EQ *itr AND - (RFL.RDB$NULL_FLAG = TRUE OR FLD.RDB$NULL_FLAG = TRUE) + if (const auto lttPtr = attachment->att_local_temporary_tables.get(work->getQualifiedName())) + { + const auto ltt = *lttPtr; + + for (const auto fieldId : work->dfw_ids) + { + if (std::any_of(ltt->fields.begin(), ltt->fields.end(), + [fieldId](const auto& lttField) { + return lttField.id == (USHORT) fieldId && lttField.notNullFlag; + })) + { + fields.add(fieldId); + } + } + } + } + else + { + for (SortedArray::iterator itr(work->dfw_ids.begin()); + itr != work->dfw_ids.end(); + ++itr) { - fields.add(RFL.RDB$FIELD_ID); + FOR(REQUEST_HANDLE handle) + RFL IN RDB$RELATION_FIELDS CROSS + FLD IN RDB$FIELDS + WITH RFL.RDB$SCHEMA_NAME EQ work->dfw_schema.c_str() AND + RFL.RDB$RELATION_NAME EQ work->dfw_name.c_str() AND + FLD.RDB$SCHEMA_NAME EQ RFL.RDB$FIELD_SOURCE_SCHEMA_NAME AND + FLD.RDB$FIELD_NAME EQ RFL.RDB$FIELD_SOURCE AND + RFL.RDB$FIELD_ID EQ *itr AND + (RFL.RDB$NULL_FLAG = TRUE OR FLD.RDB$NULL_FLAG = TRUE) + { + fields.add(RFL.RDB$FIELD_ID); + } + END_FOR } - END_FOR } if (fields.hasData()) @@ -3207,7 +3241,7 @@ static bool modify_index(thread_db* tdbb, SSHORT phase, DeferredWork* work, jrd_ while (rs->fetch(tdbb)) { - gtt_preserve = (rdbRelationType == rel_global_temp_preserve); + gtt_preserve = (rdbRelationType == rel_temp_preserve); relation = MET_lookup_relation_id(tdbb, rdbRelationID, false); } } @@ -3604,6 +3638,99 @@ static bool create_index(thread_db* tdbb, SSHORT phase, DeferredWork* work, jrd_ } +// Create an index for a Local Temporary Table. +static bool create_ltt_index(thread_db* tdbb, SSHORT phase, DeferredWork* work, jrd_tra* transaction) +{ + switch (phase) + { + case 1: + case 2: + return true; + + case 3: + break; + + default: + return false; + } + + const auto attachment = tdbb->getAttachment(); + + // Find the LTT by looking up the index + LocalTemporaryTable* ltt = nullptr; + LocalTemporaryTable::Index* lttIndex = nullptr; + + if (!MET_get_ltt_index(attachment, work->getQualifiedName(), <t, <tIndex)) + return false; // LTT or index no longer exists (rolled back?) + + const auto relation = ltt->relation; + if (!relation) + return false; + + // Skip inactive indexes + if (lttIndex->inactive) + return false; + + // Build the index descriptor + index_desc idx; + memset(&idx, 0, sizeof(idx)); + + idx.idx_id = lttIndex->id; + idx.idx_count = lttIndex->columns.getCount(); + idx.idx_flags = 0; + + if (lttIndex->unique) + idx.idx_flags |= idx_unique; + if (lttIndex->descending) + idx.idx_flags |= idx_descending; + + // Build the index key descriptors from LTT field info + int keyPos = 0; + + for (const auto& colName : lttIndex->columns) + { + // Find the field in the LTT + bool found = false; + + for (const auto& field : ltt->fields) + { + if (field.name == colName) + { + idx.idx_rpt[keyPos].idx_field = field.id; + + // Determine index type based on field type + const USHORT idxType = DFW_assign_index_type(tdbb, work->getQualifiedName(), + field.desc.dsc_dtype, + DTYPE_IS_TEXT(field.desc.dsc_dtype) ? + INTL_CS_COLL_TO_TTYPE(field.charSetId.value_or(CS_NONE), field.collationId.value_or(COLLATE_NONE)) : 0); + idx.idx_rpt[keyPos].idx_itype = idxType; + + found = true; + break; + } + } + + if (!found) + { + ERR_post(Arg::Gds(isc_no_meta_update) << + Arg::Gds(isc_idx_seg_err) << work->getQualifiedName().toQuotedString()); + } + + ++keyPos; + } + + { // scope + AutoSetRestoreFlag dbPageSpace(&tdbb->tdbb_flags, TDBB_use_db_page_space, true); + + SelectivityList selectivity(*tdbb->getDefaultPool()); + IDX_create_index(tdbb, relation, &idx, work->getQualifiedName(), + <tIndex->id, transaction, selectivity); + } + + return false; +} + + static bool create_relation(thread_db* tdbb, SSHORT phase, DeferredWork* work, jrd_tra* transaction) { /************************************** @@ -3629,8 +3756,6 @@ static bool create_relation(thread_db* tdbb, SSHORT phase, DeferredWork* work, j Jrd::Attachment* attachment = tdbb->getAttachment(); Database* dbb = tdbb->getDatabase(); - constexpr USHORT local_min_relation_id = USER_DEF_REL_INIT_ID; - switch (phase) { case 0: @@ -3704,15 +3829,15 @@ static bool create_relation(thread_db* tdbb, SSHORT phase, DeferredWork* work, j MODIFY X USING rel_id = X.RDB$RELATION_ID; - if (rel_id < local_min_relation_id || rel_id > MAX_RELATION_ID) - rel_id = X.RDB$RELATION_ID = local_min_relation_id; + if (rel_id < MIN_RELATION_ID || rel_id > MAX_RELATION_ID) + rel_id = X.RDB$RELATION_ID = MIN_RELATION_ID; // Roman Simakov: We need to return deleted relations to skip them. // This maybe result of cleanup failure after phase 3. while ( (relation = MET_lookup_relation_id(tdbb, rel_id++, true)) ) { - if (rel_id < local_min_relation_id || rel_id > MAX_RELATION_ID) - rel_id = local_min_relation_id; + if (rel_id < MIN_RELATION_ID || rel_id > MAX_RELATION_ID) + rel_id = MIN_RELATION_ID; if (rel_id == X.RDB$RELATION_ID) { @@ -3722,7 +3847,7 @@ static bool create_relation(thread_db* tdbb, SSHORT phase, DeferredWork* work, j } } - X.RDB$RELATION_ID = (rel_id > MAX_RELATION_ID) ? local_min_relation_id : rel_id; + X.RDB$RELATION_ID = (rel_id > MAX_RELATION_ID) ? MIN_RELATION_ID : rel_id; MODIFY Y USING Y.RDB$RELATION_ID = --rel_id; @@ -4845,6 +4970,114 @@ static bool delete_index(thread_db* tdbb, SSHORT phase, DeferredWork* work, jrd_ } +// Delete an index for a Local Temporary Table. +static bool delete_ltt_index(thread_db* tdbb, SSHORT phase, DeferredWork* work, jrd_tra* transaction) +{ + switch (phase) + { + case 1: + case 2: + return true; + + case 3: + break; + + default: + return false; + } + + // The index ID is passed in dfw_id, and the relation ID is stored in dfw_ids + const USHORT indexId = work->dfw_id; + const auto& ids = work->dfw_ids; + + if (ids.isEmpty()) + return false; + + const USHORT relationId = ids.front(); + + const auto relation = MET_lookup_relation_id(tdbb, relationId, false); + if (!relation) + return false; + + { // scope + AutoSetRestoreFlag noDbPageSpace(&tdbb->tdbb_flags, TDBB_use_db_page_space, true); + + // Get the relation pages + const auto relPages = relation->getPages(tdbb, MAX_TRA_NUMBER, false); + if (!relPages || !relPages->rel_index_root) + return false; + + // Delete the index by its ID + IDX_delete_index(tdbb, relation, indexId); + } + + return false; +} + + +static bool modify_ltt_index(thread_db* tdbb, SSHORT phase, DeferredWork* work, jrd_tra* transaction) +{ +/************************************** + * + * m o d i f y _ l t t _ i n d e x + * + ************************************** + * + * Functional description + * Recalculate statistics for an index on a Local Temporary Table. + * + **************************************/ + + // Recalculate statistics for an index on a Local Temporary Table. + switch (phase) + { + case 1: + case 2: + return true; + + case 3: + break; + + default: + return false; + } + + const auto attachment = tdbb->getAttachment(); + + // Find the LTT by looking up the index + LocalTemporaryTable* ltt = nullptr; + LocalTemporaryTable::Index* lttIndex = nullptr; + + if (!MET_get_ltt_index(attachment, work->getQualifiedName(), <t, <tIndex)) + return false; // LTT or index no longer exists (rolled back?) + + const auto relation = ltt->relation; + if (!relation) + return false; + + // Skip inactive indexes + if (lttIndex->inactive) + return false; + + // Only recalculate statistics if the table has been instantiated + if (!ltt->relationId) + return false; + + { // scope + AutoSetRestoreFlag dbPageSpace(&tdbb->tdbb_flags, TDBB_use_db_page_space, true); + + // Recalculate index statistics + SelectivityList selectivity(*tdbb->getDefaultPool()); + IDX_statistics(tdbb, relation, lttIndex->id, selectivity); + + // The selectivity is stored directly in the index root page by IDX_statistics, + // so we don't need to update any system tables (LTT indexes don't have entries in RDB$INDICES) + } + + return false; +} + + static bool delete_parameter(thread_db* tdbb, SSHORT phase, DeferredWork*, jrd_tra*) { /************************************** @@ -4931,6 +5164,11 @@ static bool delete_relation(thread_db* tdbb, SSHORT phase, DeferredWork* work, j return false; case 1: + relation = MET_lookup_relation_id(tdbb, work->dfw_id, false); + + if (relation && (relation->rel_flags & REL_temp_ltt)) + return true; + // check if any views use this as a base relation request.reset(); @@ -4957,7 +5195,6 @@ static bool delete_relation(thread_db* tdbb, SSHORT phase, DeferredWork* work, j // Msg310: there are %ld dependencies } - relation = MET_lookup_relation_id(tdbb, work->dfw_id, false); if (!relation) return false; @@ -4991,8 +5228,9 @@ static bool delete_relation(thread_db* tdbb, SSHORT phase, DeferredWork* work, j if (relation->rel_use_count) MET_clear_cache(tdbb); - if (relation->rel_use_count || (relation->rel_existence_lock && - !LCK_convert(tdbb, relation->rel_existence_lock, LCK_EX, transaction->getLockWait()))) + if (relation->rel_use_count || + (relation->rel_existence_lock && + !LCK_convert(tdbb, relation->rel_existence_lock, LCK_EX, transaction->getLockWait()))) { if (adjusted) ++relation->rel_use_count; @@ -5018,8 +5256,9 @@ static bool delete_relation(thread_db* tdbb, SSHORT phase, DeferredWork* work, j case 4: relation = MET_lookup_relation_id(tdbb, work->dfw_id, true); - if (!relation) { - fb_assert(false); + if (!relation) + { + fb_assert(work->dfw_id >= MIN_LTT_ID && work->dfw_id <= MAX_LTT_ID); return false; } @@ -5059,41 +5298,41 @@ static bool delete_relation(thread_db* tdbb, SSHORT phase, DeferredWork* work, j } RelationPages* const relPages = relation->getBasePages(); - if (relPages->rel_index_root) { + if (relPages && relPages->rel_index_root) IDX_delete_indices(tdbb, relation, relPages); - } - if (relPages->rel_pages) { + if (relPages && relPages->rel_pages) DPM_delete_relation(tdbb, relation); - } // if this is a view (or even if we don't know), delete dependency lists - if (relation->rel_view_rse || !(relation->rel_flags & REL_scanned)) { - MET_delete_dependencies(tdbb, work->getQualifiedName(), obj_view, transaction); - } + if (!(relation->rel_flags & REL_temp_ltt)) + { + if (relation->rel_view_rse || !(relation->rel_flags & REL_scanned)) { + MET_delete_dependencies(tdbb, work->getQualifiedName(), obj_view, transaction); + } - // Now that the data, pointer, and index pages are gone, - // get rid of the relation itself + // Now that the data, pointer, and index pages are gone, + // get rid of the relation itself - request.reset(); + request.reset(); - FOR(REQUEST_HANDLE request) X IN RDB$FORMATS WITH - X.RDB$RELATION_ID EQ relation->rel_id - { - ERASE X; - } - END_FOR + FOR(REQUEST_HANDLE request) X IN RDB$FORMATS WITH + X.RDB$RELATION_ID EQ relation->rel_id + { + ERASE X; + } + END_FOR - // Release relation locks - if (relation->rel_existence_lock) { - LCK_release(tdbb, relation->rel_existence_lock); - } - if (relation->rel_partners_lock) { - LCK_release(tdbb, relation->rel_partners_lock); - } - if (relation->rel_rescan_lock) { - LCK_release(tdbb, relation->rel_rescan_lock); + // Release relation locks + if (relation->rel_existence_lock) + LCK_release(tdbb, relation->rel_existence_lock); + + if (relation->rel_partners_lock) + LCK_release(tdbb, relation->rel_partners_lock); + + if (relation->rel_rescan_lock) + LCK_release(tdbb, relation->rel_rescan_lock); } // Mark relation in the cache as dropped @@ -5723,7 +5962,8 @@ static Format* make_format(thread_db* tdbb, jrd_rel* relation, USHORT* version, format->fmt_length = offset; Format* old_format; - if (format->fmt_version && + if (!(relation->rel_flags & REL_temp_ltt) && + format->fmt_version && (old_format = MET_format(tdbb, relation, (format->fmt_version - 1))) && (formatsAreEqual(old_format, format))) { @@ -5738,6 +5978,9 @@ static Format* make_format(thread_db* tdbb, jrd_rel* relation, USHORT* version, vec::newVector(*relation->rel_pool, relation->rel_formats, format->fmt_version + 1); (*vector)[format->fmt_version] = format; + if (relation->rel_flags & REL_temp_ltt) + return format; + // Store format in system relation AutoCacheRequest request(tdbb, irq_format3, IRQ_REQUESTS); @@ -6292,6 +6535,127 @@ static bool make_version(thread_db* tdbb, SSHORT phase, DeferredWork* work, jrd_ } +// Make a new format version for a LTT. +static bool make_ltt_version(thread_db* tdbb, SSHORT phase, DeferredWork* work, jrd_tra* transaction) +{ + switch (phase) + { + case 1: + case 2: + return true; + + case 3: + break; + + default: + return false; + } + + const auto attachment = tdbb->getAttachment(); + const auto dbb = tdbb->getDatabase(); + auto& attPool = *attachment->att_pool; + + const auto lttPtr = attachment->att_local_temporary_tables.get(work->getQualifiedName()); + if (!lttPtr) + return false; + + const auto ltt = *lttPtr; + + if (!ltt->hasPendingChanges) + return false; + + fb_assert(ltt->pendingFields.hasData()); + + jrd_rel* relation = ltt->relation; + const bool isAlter = (relation != nullptr); + + if (!isAlter) + { + fb_assert(ltt->relationId != 0); + + // CREATE: Create a new relation + relation = FB_NEW_POOL(attPool) jrd_rel(attPool); + relation->rel_id = ltt->relationId; + relation->rel_name = ltt->name; + relation->rel_current_fmt = 1; + relation->rel_flags = REL_scanned | REL_sql_relation | REL_temp_ltt | + (ltt->relationType == rel_temp_preserve ? REL_temp_conn : REL_temp_tran); + + // Initialize base pages for LTT to act as a connection-wide blueprint for indexes. + relation->getBasePages()->rel_pg_space_id = dbb->dbb_page_manager.getTempPageSpaceID(tdbb); + DPM_create_relation_pages(tdbb, relation, relation->getBasePages()); + + // Note: LTTs should NOT be registered in att_relations. + // They are stored only in att_local_temporary_tables and accessed via MET_relation. + } + else + { + // ALTER: Bump the format version + if (relation->rel_current_fmt == MAX_TABLE_VERSIONS) + raiseTooManyVersionsError(obj_relation, work->getQualifiedName()); + + relation->rel_current_fmt++; + } + + // Update the fields vector - extend if needed for new fields + relation->rel_fields = vec::newVector(attPool, relation->rel_fields, ltt->pendingFields.getCount()); + + TemporaryField* tempFields = nullptr; + + for (FB_SIZE_T i = 0; i < ltt->pendingFields.getCount(); ++i) + { + const auto& lttField = ltt->pendingFields[i]; + auto& relField = (*relation->rel_fields)[i]; + + if (!relField) + relField = FB_NEW_POOL(attPool) jrd_fld(attPool); + + relField->fld_name = lttField.name; + relField->fld_not_null = nullptr; + relField->fld_validation = nullptr; + + if (lttField.notNullFlag) + { + Jrd::ContextPoolHolder context(tdbb, &attPool); + relField->fld_not_null = PAR_validation_blr(tdbb, &relation->rel_name.schema, relation, + nonnull_validation_blr, sizeof(nonnull_validation_blr), NULL, NULL, 0); + } + + const auto tempField = FB_NEW_POOL(attPool) TemporaryField(); + tempField->tfb_next = tempFields; + tempFields = tempField; + + tempField->tfb_desc = lttField.desc; + + // For text types, set the text type from charset/collation + if (DTYPE_IS_TEXT(lttField.desc.dsc_dtype)) + { + const SSHORT charSetId = lttField.charSetId.value_or(CS_NONE); + const SSHORT collationId = lttField.collationId.value_or(COLLATE_NONE); + tempField->tfb_desc.setTextType(INTL_CS_COLL_TO_TTYPE(charSetId, collationId)); + } + + memset(&tempField->tfb_default, 0, sizeof(tempField->tfb_default)); + tempField->tfb_id = lttField.id; + } + + relation->rel_current_format = make_format(tdbb, relation, &relation->rel_current_fmt, tempFields); + + // Store the runtime relation in the LTT (for CREATE case) + ltt->relation = relation; + + // Commit the changes: copy pendingFields to fields + ltt->fields = ltt->pendingFields; + ltt->pendingFields.clear(); + ltt->hasPendingChanges = false; + + // Now invalidate DSQL cache at commit time + METD_drop_relation(transaction, work->getQualifiedName()); + + return false; +} + + static bool modify_trigger(thread_db* tdbb, SSHORT phase, DeferredWork* work, jrd_tra* transaction) { /************************************** diff --git a/src/jrd/dpm.epp b/src/jrd/dpm.epp index 94c01d8564f..23c1e1bea3b 100644 --- a/src/jrd/dpm.epp +++ b/src/jrd/dpm.epp @@ -1026,6 +1026,8 @@ void DPM_delete_relation( thread_db* tdbb, jrd_rel* relation) * Get rid of an unloved, unwanted relation. * **************************************/ + fb_assert(!(relation->rel_flags & REL_ltt_created)); + SET_TDBB(tdbb); Jrd::Attachment* attachment = tdbb->getAttachment(); diff --git a/src/jrd/dyn_ut_proto.h b/src/jrd/dyn_ut_proto.h index 08d11d53269..cccb667b636 100644 --- a/src/jrd/dyn_ut_proto.h +++ b/src/jrd/dyn_ut_proto.h @@ -36,10 +36,9 @@ void DYN_UTIL_generate_index_name(Jrd::thread_db*, Jrd::jrd_tra*, Jrd::Qualified void DYN_UTIL_generate_field_position(Jrd::thread_db*, const Jrd::QualifiedName&, SLONG*); void DYN_UTIL_generate_field_name(Jrd::thread_db*, Jrd::QualifiedName&); void DYN_UTIL_generate_constraint_name(Jrd::thread_db*, Jrd::QualifiedName&); -bool DYN_UTIL_check_unique_name_nothrow(Jrd::thread_db* tdbb, Jrd::jrd_tra* transaction, - const Jrd::QualifiedName& object_name, int object_type, USHORT* errorCode = nullptr); -void DYN_UTIL_check_unique_name(Jrd::thread_db* tdbb, Jrd::jrd_tra* transaction, - const Jrd::QualifiedName& object_name, int object_type); +bool DYN_UTIL_check_unique_name_nothrow(Jrd::thread_db* tdbb, const Jrd::QualifiedName& object_name, int object_type, + USHORT* errorCode = nullptr); +void DYN_UTIL_check_unique_name(Jrd::thread_db* tdbb, const Jrd::QualifiedName& object_name, int object_type); SINT64 DYN_UTIL_gen_unique_id(Jrd::thread_db*, SSHORT, const char*); #endif // JRD_DYN_UT_PROTO_H diff --git a/src/jrd/dyn_util.epp b/src/jrd/dyn_util.epp index e67219ec7d5..720e17993f4 100644 --- a/src/jrd/dyn_util.epp +++ b/src/jrd/dyn_util.epp @@ -82,11 +82,15 @@ static const UCHAR gen_id_blr2[] = }; // Check if an object already exists. If yes, return false. -bool DYN_UTIL_check_unique_name_nothrow(thread_db* tdbb, jrd_tra* transaction, - const QualifiedName& object_name, int object_type, USHORT* errorCode) +bool DYN_UTIL_check_unique_name_nothrow(thread_db* tdbb, const QualifiedName& object_name, int object_type, + USHORT* errorCode) { SET_TDBB(tdbb); + const auto attachment = tdbb->getAttachment(); + // Use system transaction to see all committed metadata + const auto transaction = attachment->getSysTransaction(); + USHORT tempErrorCode; errorCode = errorCode ? errorCode : &tempErrorCode; *errorCode = 0; @@ -125,6 +129,14 @@ bool DYN_UTIL_check_unique_name_nothrow(thread_db* tdbb, jrd_tra* transaction, } END_FOR } + + if (!*errorCode) + { + // Check if a Local Temporary Table with the same name already exists + if (attachment->att_local_temporary_tables.exist(object_name)) + *errorCode = 132; // isc_dyn_dup_table + } + break; } @@ -142,6 +154,29 @@ bool DYN_UTIL_check_unique_name_nothrow(thread_db* tdbb, jrd_tra* transaction, } END_FOR + if (!*errorCode) + { + // Check if an index with the same name exists on any Local Temporary Table + for (const auto& lttEntry : attachment->att_local_temporary_tables) + { + const auto ltt = lttEntry.second; + if (ltt->name.schema != object_name.schema) + continue; + + for (const auto& index : ltt->indexes) + { + if (index.name == object_name) + { + *errorCode = 251; // isc_dyn_dup_index + break; + } + } + + if (*errorCode) + break; + } + } + break; } @@ -309,12 +344,11 @@ bool DYN_UTIL_check_unique_name_nothrow(thread_db* tdbb, jrd_tra* transaction, } // Check if an object already exists. If yes, throw error. -void DYN_UTIL_check_unique_name(thread_db* tdbb, jrd_tra* transaction, const QualifiedName& object_name, - int object_type) +void DYN_UTIL_check_unique_name(thread_db* tdbb, const QualifiedName& object_name, int object_type) { USHORT errorCode; - if (!DYN_UTIL_check_unique_name_nothrow(tdbb, transaction, object_name, object_type, &errorCode)) + if (!DYN_UTIL_check_unique_name_nothrow(tdbb, object_name, object_type, &errorCode)) status_exception::raise(Arg::PrivateDyn(errorCode) << object_name.toQuotedString()); } diff --git a/src/jrd/fields.h b/src/jrd/fields.h index 05e2885456a..49a47fd055a 100644 --- a/src/jrd/fields.h +++ b/src/jrd/fields.h @@ -237,3 +237,5 @@ FIELD(fld_sch_name , nam_sch_name , dtype_text , MAX_SQL_IDENTIFIER_LEN , dsc_text_type_metadata , NULL , true , ODS_14_0) FIELD(fld_text_max , nam_text_max , dtype_varying, MAX_VARY_COLUMN_SIZE / METADATA_BYTES_PER_CHAR * METADATA_BYTES_PER_CHAR, dsc_text_type_metadata, NULL, true, ODS_14_0) + + FIELD(fld_tab_type , nam_mon_tab_type , dtype_varying , 32 , dsc_text_type_ascii , NULL , true , ODS_14_0) diff --git a/src/jrd/idx.cpp b/src/jrd/idx.cpp index ae5a42e693e..4bd3bff0266 100644 --- a/src/jrd/idx.cpp +++ b/src/jrd/idx.cpp @@ -468,7 +468,7 @@ bool IndexCreateTask::handler(WorkItem& _item) Database* dbb = tdbb->getDatabase(); Attachment* attachment = tdbb->getAttachment(); jrd_rel* relation = MET_relation(tdbb, m_creation->relation->rel_id); - if (!(relation->rel_flags & REL_scanned)) + if (relation && !(relation->rel_flags & REL_scanned)) MET_scan_relation(tdbb, relation); index_desc* idx = &item->m_idx; @@ -1006,14 +1006,17 @@ IndexBlock* IDX_create_index_block(thread_db* tdbb, jrd_rel* relation, USHORT id index_block->idb_next = relation->rel_index_blocks; relation->rel_index_blocks = index_block; - // create a shared lock for the index, to coordinate - // any modification to the index so that the cached information - // about the index will be discarded - - Lock* lock = FB_NEW_RPT(*relation->rel_pool, 0) - Lock(tdbb, sizeof(SLONG), LCK_expression, index_block, index_block_flush); - index_block->idb_lock = lock; - lock->setKey((relation->rel_id << 16) | index_block->idb_id); + if (!(relation->rel_flags & REL_temp_ltt)) + { + // create a shared lock for the index, to coordinate + // any modification to the index so that the cached information + // about the index will be discarded + + Lock* lock = FB_NEW_RPT(*relation->rel_pool, 0) + Lock(tdbb, sizeof(SLONG), LCK_expression, index_block, index_block_flush); + index_block->idb_lock = lock; + lock->setKey((relation->rel_id << 16) | index_block->idb_id); + } return index_block; } @@ -2167,7 +2170,8 @@ static void release_index_block(thread_db* tdbb, IndexBlock* index_block) } index_block->idb_condition = nullptr; - LCK_release(tdbb, index_block->idb_lock); + if (index_block->idb_lock) + LCK_release(tdbb, index_block->idb_lock); } @@ -2210,13 +2214,14 @@ static void signal_index_deletion(thread_db* tdbb, jrd_rel* relation, USHORT id) lock = index_block->idb_lock; } - // signal other processes to clear out the index block + if (lock) + { + // signal other processes to clear out the index block - if (lock->lck_physical == LCK_SR) { - LCK_convert(tdbb, lock, LCK_EX, LCK_WAIT); - } - else { - LCK_lock(tdbb, lock, LCK_EX, LCK_WAIT); + if (lock->lck_physical == LCK_SR) + LCK_convert(tdbb, lock, LCK_EX, LCK_WAIT); + else + LCK_lock(tdbb, lock, LCK_EX, LCK_WAIT); } release_index_block(tdbb, index_block); diff --git a/src/jrd/irq.h b/src/jrd/irq.h index d659cc2401a..68877007124 100644 --- a/src/jrd/irq.h +++ b/src/jrd/irq.h @@ -31,8 +31,6 @@ enum irq_type_t { irq_s_pages, // store PAGES irq_r_pages, // read PAGES - irq_l_field, // lookup field name - irq_l_relation, // lookup relation name irq_c_relation, // create new relation irq_format1, // make a new format for a record irq_format2, // make a new format for a record @@ -86,7 +84,6 @@ enum irq_type_t irq_l_exp_index_blr, // lookup expression index BLR irq_l_cond_index, // lookup condition index - irq_l_rel_id, // lookup relation id irq_l_procedure, // lookup procedure name irq_l_proc_id, // lookup procedure id irq_r_params, // scan procedure parameters @@ -175,7 +172,6 @@ enum irq_type_t irq_grant17, // process grant option (database) irq_grant18, // process grant option (filters) irq_grant19, // process grant option (roles) - irq_l_curr_format, // lookup table's current format irq_c_relation3, // lookup relation in phase 0 to cleanup irq_linger, // get database linger value irq_dbb_ss_definer, // get database sql security value diff --git a/src/jrd/jrd.cpp b/src/jrd/jrd.cpp index b05b5ccbabb..c59d9f7efaa 100644 --- a/src/jrd/jrd.cpp +++ b/src/jrd/jrd.cpp @@ -7900,7 +7900,7 @@ void release_attachment(thread_db* tdbb, Jrd::Attachment* attachment, XThreadEns dbb->dbb_extManager->closeAttachment(tdbb, attachment); if (dbb->dbb_config->getServerMode() == MODE_SUPER) - attachment->releaseGTTs(tdbb); + attachment->releaseTempTables(tdbb); if (attachment->att_event_session) dbb->eventManager()->deleteSession(attachment->att_event_session); diff --git a/src/jrd/met.epp b/src/jrd/met.epp index 4ef5bd33973..4ecd2c97464 100644 --- a/src/jrd/met.epp +++ b/src/jrd/met.epp @@ -94,6 +94,7 @@ #include "../jrd/Function.h" #include "../jrd/trace/TraceJrdHelpers.h" #include "firebird/impl/msg_helper.h" +#include "../jrd/LocalTemporaryTable.h" #ifdef HAVE_CTYPE_H @@ -263,9 +264,48 @@ void MET_get_relation_field(thread_db* tdbb, MemoryPool& csbPool, const Qualifie bool found = false; MetaName sourceName; - AutoCacheRequest handle(tdbb, irq_l_relfield, IRQ_REQUESTS); + if (const auto lttPtr = attachment->att_local_temporary_tables.get(relationName)) + { + const auto ltt = *lttPtr; - FOR(REQUEST_HANDLE handle) + for (const auto& lttField : ltt->fields) + { + if (lttField.name == fieldName) + { + // Copy descriptor from LTT field + *desc = lttField.desc; + + // Set text type for text fields + if (DTYPE_IS_TEXT(lttField.desc.dsc_dtype)) + { + const SSHORT charSetId = lttField.charSetId.value_or(CS_NONE); + const SSHORT collationId = lttField.collationId.value_or(COLLATE_NONE); + desc->setTextType(INTL_CS_COLL_TO_TTYPE(charSetId, collationId)); + } + + sourceName = lttField.source.object; + found = true; + + if (fieldInfo) + { + fieldInfo->nullable = !lttField.notNullFlag; + fieldInfo->defaultValue = nullptr; // LTTs don't support defaults yet + fieldInfo->validationExpr = nullptr; + } + + return; + } + } + + ERR_post(Arg::Gds(isc_dyn_column_does_not_exist) << + fieldName.toQuotedString() << + relationName.toQuotedString()); + } + + static const CachedRequestId handle1CacheId; + AutoCacheRequest handle1(tdbb, handle1CacheId); + + FOR(REQUEST_HANDLE handle1) RFL IN RDB$RELATION_FIELDS CROSS FLD IN RDB$FIELDS WITH RFL.RDB$SCHEMA_NAME EQ relationName.schema.c_str() AND @@ -1236,9 +1276,10 @@ Format* MET_current(thread_db* tdbb, jrd_rel* relation) if (!(relation->rel_flags & REL_scanned)) { - AutoCacheRequest request(tdbb, irq_l_curr_format, IRQ_REQUESTS); + static const CachedRequestId request1CacheId; + AutoCacheRequest request1(tdbb, request1CacheId); - FOR(REQUEST_HANDLE request) + FOR(REQUEST_HANDLE request1) REL IN RDB$RELATIONS WITH REL.RDB$RELATION_ID EQ relation->rel_id { @@ -1423,6 +1464,7 @@ Format* MET_format(thread_db* tdbb, jrd_rel* relation, USHORT number) // System relations don't have their formats stored inside RDB$FORMATS, // so it's absolutely pointless trying to find one there fb_assert(!relation->isSystem()); + fb_assert(!(relation->rel_flags & REL_temp_ltt)); format = NULL; AutoCacheRequest request(tdbb, irq_r_format, IRQ_REQUESTS); @@ -2260,9 +2302,26 @@ int MET_lookup_field(thread_db* tdbb, jrd_rel* relation, const MetaName& name) if (relation->rel_flags & REL_deleted) return id; - AutoCacheRequest request(tdbb, irq_l_field, IRQ_REQUESTS); + if (const auto lttPtr = attachment->att_local_temporary_tables.get(relation->rel_name)) + { + const auto ltt = *lttPtr; - FOR(REQUEST_HANDLE request) + for (const auto& lttField : ltt->fields) + { + if (lttField.name == name) + { + id = lttField.id; + break; + } + } + + return id; + } + + static const CachedRequestId request1CacheId; + AutoCacheRequest request1(tdbb, request1CacheId); + + FOR(REQUEST_HANDLE request1) X IN RDB$RELATION_FIELDS WITH X.RDB$SCHEMA_NAME EQ relation->rel_name.schema.c_str() AND X.RDB$RELATION_NAME EQ relation->rel_name.object.c_str() AND @@ -2501,6 +2560,23 @@ void MET_lookup_index(thread_db* tdbb, index_name.clear(); + // Check for Local Temporary Table indexes first + if (const auto lttPtr = attachment->att_local_temporary_tables.get(relation_name)) + { + const auto ltt = *lttPtr; + + for (const auto& idx : ltt->indexes) + { + if (idx.id + 1 == number) + { + index_name = idx.name; + return; + } + } + + return; + } + AutoCacheRequest request(tdbb, irq_l_index, IRQ_REQUESTS); FOR(REQUEST_HANDLE request) @@ -2534,6 +2610,22 @@ SLONG MET_lookup_index_name(thread_db* tdbb, SET_TDBB(tdbb); Attachment* attachment = tdbb->getAttachment(); + for (const auto& lttEntry : attachment->att_local_temporary_tables) + { + for (const auto& lttIndex : lttEntry.second->indexes) + { + if (lttIndex.name == index_name) + { + fb_assert(lttEntry.second->relationId != 0); + *relation_id = lttEntry.second->relationId; + *status = lttIndex.inactive ? MET_object_inactive : MET_object_active; + return lttIndex.id; + } + } + + return -1; + } + AutoCacheRequest request(tdbb, irq_l_index_name, IRQ_REQUESTS); *status = MET_object_unknown; @@ -2640,7 +2732,8 @@ void MET_lookup_index_condition(thread_db* tdbb, jrd_rel* relation, index_desc* // If we can't get the lock, no big deal: just give up on caching the index info - if (!LCK_lock(tdbb, index_block->idb_lock, LCK_SR, LCK_NO_WAIT)) + if (!(relation->rel_flags & REL_temp_ltt) && + !LCK_lock(tdbb, index_block->idb_lock, LCK_SR, LCK_NO_WAIT)) { // clear lock error from status vector fb_utils::init_status(tdbb->tdbb_status_vector); @@ -2733,7 +2826,8 @@ void MET_lookup_index_expression(thread_db* tdbb, jrd_rel* relation, index_desc* // if we can't get the lock, no big deal: just give up on caching the index info - if (!LCK_lock(tdbb, index_block->idb_lock, LCK_SR, LCK_NO_WAIT)) + if (!(relation->rel_flags & REL_temp_ltt) && + !LCK_lock(tdbb, index_block->idb_lock, LCK_SR, LCK_NO_WAIT)) { // clear lock error from status vector fb_utils::init_status(tdbb->tdbb_status_vector); @@ -3045,13 +3139,22 @@ jrd_rel* MET_lookup_relation(thread_db* tdbb, const QualifiedName& name) // See if we already know the relation by name + if (const auto lttPtr = attachment->att_local_temporary_tables.get(name)) + { + const auto ltt = *lttPtr; + + if (ltt->relation) + return ltt->relation; + } + vec* relations = attachment->att_relations; jrd_rel* check_relation = NULL; + jrd_rel* relation = nullptr; vec::iterator ptr = relations->begin(); for (const vec::const_iterator end = relations->end(); ptr < end; ++ptr) { - jrd_rel* const relation = *ptr; + relation = *ptr; if (relation) { @@ -3086,42 +3189,45 @@ jrd_rel* MET_lookup_relation(thread_db* tdbb, const QualifiedName& name) // We need to look up the relation name in RDB$RELATIONS - jrd_rel* relation = NULL; - - AutoCacheRequest request(tdbb, irq_l_relation, IRQ_REQUESTS); + static const CachedRequestId request1CacheId; + AutoCacheRequest request1(tdbb, request1CacheId); - FOR(REQUEST_HANDLE request) + FOR(REQUEST_HANDLE request1) X IN RDB$RELATIONS WITH X.RDB$SCHEMA_NAME EQ name.schema.c_str() AND X.RDB$RELATION_NAME EQ name.object.c_str() { relation = MET_relation(tdbb, X.RDB$RELATION_ID); - if (relation->rel_name.object.isEmpty()) { + if (relation->rel_name.object.isEmpty()) relation->rel_name = name; - } relation->rel_flags |= get_rel_flags_from_FLAGS(X.RDB$FLAGS); if (!X.RDB$RELATION_TYPE.NULL) - { relation->rel_flags |= MET_get_rel_flags_from_TYPE(X.RDB$RELATION_TYPE); - } } END_FOR if (check_relation) { check_relation->rel_flags &= ~REL_check_existence; + if (check_relation != relation) { - LCK_release(tdbb, check_relation->rel_existence_lock); - if (!(check_relation->rel_flags & REL_check_partners)) + if (!(check_relation->rel_flags & REL_temp_ltt)) { - check_relation->rel_flags |= REL_check_partners; - LCK_release(tdbb, check_relation->rel_partners_lock); - check_relation->rel_flags &= ~REL_check_partners; + LCK_release(tdbb, check_relation->rel_existence_lock); + + if (!(check_relation->rel_flags & REL_check_partners)) + { + check_relation->rel_flags |= REL_check_partners; + LCK_release(tdbb, check_relation->rel_partners_lock); + check_relation->rel_flags &= ~REL_check_partners; + } + + LCK_release(tdbb, check_relation->rel_rescan_lock); } - LCK_release(tdbb, check_relation->rel_rescan_lock); + check_relation->rel_flags |= REL_deleted; } } @@ -3167,7 +3273,9 @@ jrd_rel* MET_lookup_relation_id(thread_db* tdbb, SLONG id, bool return_deleted) if (relation->rel_flags & REL_check_existence) { check_relation = relation; - LCK_lock(tdbb, check_relation->rel_existence_lock, LCK_SR, LCK_WAIT); + + if (!(check_relation->rel_flags & REL_temp_ltt)) + LCK_lock(tdbb, check_relation->rel_existence_lock, LCK_SR, LCK_WAIT); } else return relation; @@ -3175,40 +3283,60 @@ jrd_rel* MET_lookup_relation_id(thread_db* tdbb, SLONG id, bool return_deleted) // We need to look up the relation id in RDB$RELATIONS - relation = NULL; + relation = nullptr; - AutoCacheRequest request(tdbb, irq_l_rel_id, IRQ_REQUESTS); - - FOR(REQUEST_HANDLE request) - X IN RDB$RELATIONS - WITH X.RDB$RELATION_ID EQ id + for (const auto& lttEntry : attachment->att_local_temporary_tables) { - relation = MET_relation(tdbb, X.RDB$RELATION_ID); - if (relation->rel_name.object.isEmpty()) - relation->rel_name = QualifiedName(X.RDB$RELATION_NAME, X.RDB$SCHEMA_NAME); + const auto ltt = lttEntry.second; - relation->rel_flags |= get_rel_flags_from_FLAGS(X.RDB$FLAGS); + if (ltt->relationId == (ULONG) id && ltt->relation) + { + relation = ltt->relation; + break; + } + } - if (!X.RDB$RELATION_TYPE.NULL) + if (!relation) + { + static const CachedRequestId request1CacheId; + AutoCacheRequest request1(tdbb, request1CacheId); + + FOR(REQUEST_HANDLE request1) + X IN RDB$RELATIONS + WITH X.RDB$RELATION_ID EQ id { - relation->rel_flags |= MET_get_rel_flags_from_TYPE(X.RDB$RELATION_TYPE); + relation = MET_relation(tdbb, X.RDB$RELATION_ID); + if (relation->rel_name.object.isEmpty()) + relation->rel_name = QualifiedName(X.RDB$RELATION_NAME, X.RDB$SCHEMA_NAME); + + relation->rel_flags |= get_rel_flags_from_FLAGS(X.RDB$FLAGS); + + if (!X.RDB$RELATION_TYPE.NULL) + relation->rel_flags |= MET_get_rel_flags_from_TYPE(X.RDB$RELATION_TYPE); } + END_FOR } - END_FOR if (check_relation) { check_relation->rel_flags &= ~REL_check_existence; + if (check_relation != relation) { - LCK_release(tdbb, check_relation->rel_existence_lock); - if (!(check_relation->rel_flags & REL_check_partners)) + if (!(check_relation->rel_flags & REL_temp_ltt)) { - check_relation->rel_flags |= REL_check_partners; - LCK_release(tdbb, check_relation->rel_partners_lock); - check_relation->rel_flags &= ~REL_check_partners; + LCK_release(tdbb, check_relation->rel_existence_lock); + + if (!(check_relation->rel_flags & REL_check_partners)) + { + check_relation->rel_flags |= REL_check_partners; + LCK_release(tdbb, check_relation->rel_partners_lock); + check_relation->rel_flags &= ~REL_check_partners; + } + + LCK_release(tdbb, check_relation->rel_rescan_lock); } - LCK_release(tdbb, check_relation->rel_rescan_lock); + check_relation->rel_flags |= REL_deleted; } } @@ -3744,13 +3872,32 @@ jrd_rel* MET_relation(thread_db* tdbb, USHORT id) * * Functional description * Find or create a relation block for a given relation id. + * This function handles both normal relations and LTTs (Local Temporary Tables). + * For LTT IDs (>= MIN_LTT_ID), it searches att_local_temporary_tables. + * For normal relation IDs (< MIN_LTT_ID), it uses att_relations. * **************************************/ SET_TDBB(tdbb); Database* dbb = tdbb->getDatabase(); CHECK_DBB(dbb); - Attachment* attachment = tdbb->getAttachment(); + + // For LTT IDs, search att_local_temporary_tables + if (id >= MIN_LTT_ID) + { + for (const auto& lttEntry : attachment->att_local_temporary_tables) + { + const auto ltt = lttEntry.second; + if (ltt->relationId == id && ltt->relation) + return ltt->relation; + } + + // LTT not found - unlike normal relations, LTTs are not auto-created by this function + fb_assert(false); + status_exception::raise(Arg::PrivateDyn(61)); + } + + // For normal relations, use att_relations vec* vector = attachment->att_relations; MemoryPool* pool = attachment->att_pool; @@ -3817,8 +3964,12 @@ void MET_release_existence(thread_db* tdbb, jrd_rel* relation) if (!relation->rel_use_count) { - if (relation->rel_flags & REL_blocking) + if ((relation->rel_flags & REL_blocking) && + !(relation->rel_flags & REL_temp_ltt) && + relation->rel_existence_lock) + { LCK_re_post(tdbb, relation->rel_existence_lock); + } // release trigger requests relation->releaseTriggers(tdbb, false); @@ -3955,6 +4106,7 @@ static void scan_relation(thread_db* tdbb, jrd_rel* relation) try { + fb_assert(!(relation->rel_flags & REL_temp_ltt)); fb_assert(!(relation->rel_flags & (REL_scanned | REL_deleted))); relation->rel_flags |= REL_being_scanned; @@ -4054,13 +4206,13 @@ static void scan_relation(thread_db* tdbb, jrd_rel* relation) fb_assert(relation->rel_flags & REL_virtual); relation->rel_flags |= REL_virtual; break; - case rel_global_temp_preserve: - fb_assert(relation->rel_flags & REL_temp_conn); - relation->rel_flags |= REL_temp_conn; + case rel_temp_preserve: + fb_assert((relation->rel_flags & (REL_temp_conn | REL_temp_gtt)) == (REL_temp_conn | REL_temp_gtt)); + relation->rel_flags |= REL_temp_conn | REL_temp_gtt; break; - case rel_global_temp_delete: - fb_assert(relation->rel_flags & REL_temp_tran); - relation->rel_flags |= REL_temp_tran; + case rel_temp_delete: + fb_assert((relation->rel_flags & (REL_temp_tran | REL_temp_gtt)) == (REL_temp_tran | REL_temp_gtt)); + relation->rel_flags |= REL_temp_tran | REL_temp_gtt; break; default: fb_assert(false); @@ -4658,11 +4810,11 @@ ULONG MET_get_rel_flags_from_TYPE(USHORT type) case rel_virtual: ret |= REL_virtual; break; - case rel_global_temp_preserve: - ret |= REL_temp_conn; + case rel_temp_preserve: + ret |= REL_temp_conn | REL_temp_gtt; break; - case rel_global_temp_delete: - ret |= REL_temp_tran; + case rel_temp_delete: + ret |= REL_temp_tran | REL_temp_gtt; break; default: fb_assert(false); @@ -5151,6 +5303,9 @@ void scan_partners(thread_db* tdbb, jrd_rel* relation) * scan of primary dependencies on relation's primary key. * **************************************/ + if (relation->rel_flags & REL_temp_ltt) + return; + Attachment* attachment = tdbb->getAttachment(); while (relation->rel_flags & REL_check_partners) @@ -5317,6 +5472,8 @@ void MET_store_dependencies(thread_db* tdbb, SET_TDBB(tdbb); + fb_assert(!dep_rel || !(dep_rel->rel_flags & REL_temp_ltt)); + const Trigger* t = nullptr; const bool checkTableScope = (dependency_type == obj_computed) || @@ -5806,18 +5963,42 @@ std::optional MET_qualify_existing_name(thread_db* tdbb, QualifiedNa { fb_assert(name.package.isEmpty()); - static const CachedRequestId indexHandleId; - handle.reset(tdbb, indexHandleId); + // Check LTT indexes first + for (const auto& lttEntry : attachment->att_local_temporary_tables) + { + const auto ltt = lttEntry.second; - FOR (REQUEST_HANDLE handle) - IDX IN RDB$INDICES - WITH IDX.RDB$SCHEMA_NAME EQ searchSchema.c_str() AND - IDX.RDB$INDEX_NAME EQ name.object.c_str() + if (ltt->name.schema != searchSchema) + continue; + + for (const auto& index : ltt->indexes) + { + if (index.name.object == name.object) + { + found = true; + break; + } + } + + if (found) + break; + } + + if (!found) { - found = true; - break; + static const CachedRequestId indexHandleId; + handle.reset(tdbb, indexHandleId); + + FOR (REQUEST_HANDLE handle) + IDX IN RDB$INDICES + WITH IDX.RDB$SCHEMA_NAME EQ searchSchema.c_str() AND + IDX.RDB$INDEX_NAME EQ name.object.c_str() + { + found = true; + break; + } + END_FOR } - END_FOR break; } @@ -5865,18 +6046,33 @@ std::optional MET_qualify_existing_name(thread_db* tdbb, QualifiedNa case obj_view: if (name.package.isEmpty()) { - static const CachedRequestId relationHandleId; - handle.reset(tdbb, relationHandleId); + // Check LTT first + for (const auto& lttEntry : attachment->att_local_temporary_tables) + { + const auto ltt = lttEntry.second; - FOR (REQUEST_HANDLE handle) - REL IN RDB$RELATIONS - WITH REL.RDB$SCHEMA_NAME EQ searchSchema.c_str() AND - REL.RDB$RELATION_NAME EQ name.object.c_str() + if (ltt->name.schema == searchSchema && ltt->name.object == name.object) + { + found = true; + break; + } + } + + if (!found) { - found = true; - break; + static const CachedRequestId relationHandleId; + handle.reset(tdbb, relationHandleId); + + FOR (REQUEST_HANDLE handle) + REL IN RDB$RELATIONS + WITH REL.RDB$SCHEMA_NAME EQ searchSchema.c_str() AND + REL.RDB$RELATION_NAME EQ name.object.c_str() + { + found = true; + break; + } + END_FOR } - END_FOR } break; @@ -5956,3 +6152,50 @@ bool MET_check_schema_exists(thread_db* tdbb, const MetaName& name) return false; } + +bool MET_get_ltt_index(Attachment* attachment, const QualifiedName& indexName, + LocalTemporaryTable** outLtt, LocalTemporaryTable::Index** outIndex) +{ +/************************************** + * + * M E T _ g e t _ l t t _ i n d e x + * + ************************************** + * + * Functional description + * Look up an index by name in all LTTs of the current attachment. + * If found, returns true and sets outLtt to the owning LocalTemporaryTable + * and outIndex to point to the index. If not found, returns false. + * + **************************************/ + + if (outLtt) + *outLtt = nullptr; + + if (outIndex) + *outIndex = nullptr; + + // Search through all LTTs for an index with the given name + for (auto& lttEntry : attachment->att_local_temporary_tables) + { + const auto ltt = lttEntry.second; + + // Only search in LTTs with matching schema + if (ltt->name.schema != indexName.schema) + continue; + + for (auto& index : ltt->indexes) + { + if (index.name == indexName) + { + if (outLtt) + *outLtt = ltt; + if (outIndex) + *outIndex = &index; + return true; + } + } + } + + return false; +} diff --git a/src/jrd/met_proto.h b/src/jrd/met_proto.h index d178125b777..a82210cee40 100644 --- a/src/jrd/met_proto.h +++ b/src/jrd/met_proto.h @@ -50,6 +50,7 @@ namespace Jrd class DeferredWork; struct FieldInfo; class ExceptionItem; + class LocalTemporaryTable; // index status enum IndexStatus @@ -157,5 +158,7 @@ std::optional MET_qualify_existing_name(Jrd::thread_db* tdbb, Jrd::Q std::initializer_list objTypes, const Firebird::ObjectsArray* schemaSearchPath = nullptr); bool MET_check_schema_exists(Jrd::thread_db* tdbb, const Jrd::MetaName& name); +bool MET_get_ltt_index(Jrd::Attachment* attachment, const Jrd::QualifiedName& indexName, + Jrd::LocalTemporaryTable** outLtt = nullptr, Jrd::LocalTemporaryTable::Index** outIndex = nullptr); #endif // JRD_MET_PROTO_H diff --git a/src/jrd/names.h b/src/jrd/names.h index 661c7974b79..3e1908c0f3d 100644 --- a/src/jrd/names.h +++ b/src/jrd/names.h @@ -490,3 +490,5 @@ NAME("RDB$TABLE_SCHEMA_NAME", nam_tab_sch_name) NAME("RDB$CONST_SCHEMA_NAME_UQ", nam_con_sch_name_uq) NAME("MON$SEARCH_PATH", nam_mon_search_path) NAME("RDB$TEXT_MAX", nam_text_max) + +NAME("MON$TABLE_TYPE", nam_mon_tab_type) diff --git a/src/jrd/ods.h b/src/jrd/ods.h index 4c6e67b6e5a..137bbdf34b9 100644 --- a/src/jrd/ods.h +++ b/src/jrd/ods.h @@ -190,6 +190,14 @@ inline constexpr USHORT ODS_CURRENT_VERSION = ODS_14_0; // Current ODS version //const USHORT USER_REL_INIT_ID_ODS8 = 31; // ODS < 9 ( <= 8.2) 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; +static_assert(MIN_LTT_ID > MAX_RELATION_ID); + // Page types diff --git a/src/jrd/optimizer/Optimizer.cpp b/src/jrd/optimizer/Optimizer.cpp index 92a13b51f74..97278b7b183 100644 --- a/src/jrd/optimizer/Optimizer.cpp +++ b/src/jrd/optimizer/Optimizer.cpp @@ -3191,13 +3191,15 @@ string Optimizer::makeAlias(StreamType stream) const CompilerScratch::csb_repeat* csb_tail = &csb->csb_rpt[stream]; - if (csb_tail->csb_view || csb_tail->csb_alias) + // Check for view or explicit alias with actual content + // (csb_alias can be a non-null pointer to an empty string for blr_relation3) + if (csb_tail->csb_view || (csb_tail->csb_alias && csb_tail->csb_alias->hasData())) { ObjectsArray alias_list; while (csb_tail) { - if (csb_tail->csb_alias) + if (csb_tail->csb_alias && csb_tail->csb_alias->hasData()) alias_list.push(*csb_tail->csb_alias); else if (csb_tail->csb_relation) alias_list.push(csb_tail->csb_relation->rel_name.toQuotedString()); diff --git a/src/jrd/relations.h b/src/jrd/relations.h index 7aaaa4a88e8..32bcf510bed 100644 --- a/src/jrd/relations.h +++ b/src/jrd/relations.h @@ -748,6 +748,7 @@ RELATION(nam_mon_tab_stats, rel_mon_tab_stats, ODS_12_0, rel_virtual) FIELD(f_mon_tab_name, nam_mon_tab_name, fld_r_name, 0, ODS_12_0) FIELD(f_mon_tab_rec_stat_id, nam_mon_rec_stat_id, fld_stat_id, 0, ODS_12_0) FIELD(f_mon_tab_sch_name, nam_mon_sch_name, fld_sch_name, 0, ODS_14_0) + FIELD(f_mon_tab_type, nam_mon_tab_type, fld_tab_type, 0, ODS_14_0) END_RELATION // Relation 50 (RDB$TIME_ZONES) diff --git a/src/jrd/rlck.cpp b/src/jrd/rlck.cpp index 9fd8411b6d7..2fe6899fb26 100644 --- a/src/jrd/rlck.cpp +++ b/src/jrd/rlck.cpp @@ -51,8 +51,11 @@ Lock* RLCK_reserve_relation(thread_db* tdbb, jrd_tra* transaction, jrd_rel* rela SET_TDBB(tdbb); Database* const dbb = tdbb->getDatabase(); - if (transaction->tra_flags & TRA_system) - return NULL; + if ((transaction->tra_flags & TRA_system) || + (relation->rel_flags & REL_temp_ltt)) + { + return nullptr; + } // hvlad: virtual relations are always writable, see below for the other rules diff --git a/src/jrd/tra.cpp b/src/jrd/tra.cpp index d9935f022e2..abf2ba81db6 100644 --- a/src/jrd/tra.cpp +++ b/src/jrd/tra.cpp @@ -2488,10 +2488,13 @@ static void release_temp_tables(thread_db* tdbb, jrd_tra* transaction) ************************************** * * Functional description - * Release data of temporary tables with transaction lifetime + * Release (delete pages) temporary tables with transaction lifetime (ON COMMIT DELETE ROWS). + * This is called on full commit/rollback, not on commit retaining. * **************************************/ Attachment* att = tdbb->getAttachment(); + + // Release GTTs (Global Temporary Tables) with transaction lifetime vec& rels = *att->att_relations; for (FB_SIZE_T i = 0; i < rels.count(); i++) @@ -2501,6 +2504,15 @@ static void release_temp_tables(thread_db* tdbb, jrd_tra* transaction) if (relation && (relation->rel_flags & REL_temp_tran)) relation->delPages(tdbb, transaction->tra_number); } + + // Release LTTs (Local Temporary Tables) with transaction lifetime + for (const auto& lttEntry : att->att_local_temporary_tables) + { + const auto ltt = lttEntry.second; + + if (ltt && ltt->relation && (ltt->relation->rel_flags & REL_temp_tran)) + ltt->relation->delPages(tdbb, transaction->tra_number); + } } @@ -2514,10 +2526,13 @@ static void retain_temp_tables(thread_db* tdbb, jrd_tra* transaction, TraNumber * * Functional description * Reassign instance of temporary tables with transaction lifetime to the new - * transaction number (see retain_context). + * transaction number (see retain_context). This is called on commit retaining + * to preserve the data in ON COMMIT DELETE ROWS tables. * **************************************/ Attachment* att = tdbb->getAttachment(); + + // Retain GTTs (Global Temporary Tables) with transaction lifetime vec& rels = *att->att_relations; for (FB_SIZE_T i = 0; i < rels.count(); i++) @@ -2527,6 +2542,15 @@ static void retain_temp_tables(thread_db* tdbb, jrd_tra* transaction, TraNumber if (relation && (relation->rel_flags & REL_temp_tran)) relation->retainPages(tdbb, transaction->tra_number, new_number); } + + // Retain LTTs (Local Temporary Tables) with transaction lifetime + for (const auto& lttEntry : att->att_local_temporary_tables) + { + const auto ltt = lttEntry.second; + + if (ltt && ltt->relation && (ltt->relation->rel_flags & REL_temp_tran)) + ltt->relation->retainPages(tdbb, transaction->tra_number, new_number); + } } @@ -4160,11 +4184,9 @@ void jrd_tra::checkBlob(thread_db* tdbb, const bid* blob_id, jrd_fld* fld, bool if (!tra_blobs->locate(blob_id->bid_temp_id()) && !tra_fetched_blobs.locate(*blob_id)) { - vec* vector = tra_attachment->att_relations; - jrd_rel* blb_relation; + const auto blb_relation = MET_relation(tdbb, rel_id); - if ((rel_id < vector->count() && (blb_relation = (*vector)[rel_id])) || - (blb_relation = MET_relation(tdbb, rel_id))) + if (blb_relation) { auto security_name = (fld && fld->fld_security_name.hasData()) ? fld->fld_security_name : blb_relation->rel_security_name.object; diff --git a/src/jrd/tra.h b/src/jrd/tra.h index b55ed364dcf..7cb8251ce3f 100644 --- a/src/jrd/tra.h +++ b/src/jrd/tra.h @@ -474,7 +474,11 @@ enum dfw_t { dfw_create_relation, dfw_delete_relation, dfw_update_format, + dfw_update_ltt_format, dfw_create_index, + dfw_create_ltt_index, + dfw_modify_ltt_index, + dfw_delete_ltt_index, dfw_delete_index, dfw_compute_security, dfw_add_shadow, diff --git a/src/jrd/types.h b/src/jrd/types.h index 4d8956f18f4..c9698f97408 100644 --- a/src/jrd/types.h +++ b/src/jrd/types.h @@ -118,8 +118,8 @@ TYPE("PERSISTENT", rel_persistent, nam_r_type) TYPE("VIEW", rel_view, nam_r_type) TYPE("EXTERNAL", rel_external, nam_r_type) TYPE("VIRTUAL", rel_virtual, nam_r_type) -TYPE("GLOBAL_TEMPORARY_PRESERVE", rel_global_temp_preserve, nam_r_type) -TYPE("GLOBAL_TEMPORARY_DELETE", rel_global_temp_delete, nam_r_type) +TYPE("GLOBAL_TEMPORARY_PRESERVE", rel_temp_preserve, nam_r_type) +TYPE("GLOBAL_TEMPORARY_DELETE", rel_temp_delete, nam_r_type) TYPE("LEGACY", prc_legacy, nam_prc_type) TYPE("SELECTABLE", prc_selectable, nam_prc_type) diff --git a/src/utilities/gstat/dba.epp b/src/utilities/gstat/dba.epp index 53baa567a85..6eae03c9f54 100644 --- a/src/utilities/gstat/dba.epp +++ b/src/utilities/gstat/dba.epp @@ -827,9 +827,9 @@ int gstat(Firebird::UtilSvc* uSvc) if (!X.RDB$VIEW_BLR.NULL || !X.RDB$EXTERNAL_FILE.NULL) { continue; } - //rel_virtual, rel_global_temp_preserve, rel_global_temp_delete + //rel_virtual, rel_temp_preserve, rel_temp_delete if (!X.RDB$SYSTEM_FLAG.NULL && X.RDB$SYSTEM_FLAG == fb_sysflag_system && - X.RDB$RELATION_TYPE >= rel_virtual && X.RDB$RELATION_TYPE <= rel_global_temp_delete) + X.RDB$RELATION_TYPE >= rel_virtual && X.RDB$RELATION_TYPE <= rel_temp_delete) { continue; }