diff --git a/builds/posix/make.shared.variables b/builds/posix/make.shared.variables index c0e1cc0ab8c..d12ec2ac449 100644 --- a/builds/posix/make.shared.variables +++ b/builds/posix/make.shared.variables @@ -114,10 +114,10 @@ GFIX_Objects:= $(Svc_GFIX_Objects) $(call dirObjects,alice/main) AllObjects += $(GFIX_Objects) # gbak -Svc_GBAK_Objects:= $(call dirObjects,burp) -GBAK_Objects:= $(Svc_GBAK_Objects) $(call dirObjects,burp/main) +Svc_GBAK_Objects:= $(call dirObjects,burp) $(call dirObjects,burp/engine) +GBAK_Objects:= $(call dirObjects,burp) $(call dirObjects,burp/main) -AllObjects += $(GBAK_Objects) +AllObjects += $(GBAK_Objects) $(call dirObjects,burp/engine) # gsec Svc_GSEC_Objects:= $(call dirObjects,utilities/gsec) diff --git a/builds/win32/msvc15/burp.vcxproj b/builds/win32/msvc15/burp.vcxproj index ef564dc7f9c..cde4c981601 100644 --- a/builds/win32/msvc15/burp.vcxproj +++ b/builds/win32/msvc15/burp.vcxproj @@ -193,6 +193,7 @@ + @@ -211,10 +212,14 @@ + + + + diff --git a/builds/win32/msvc15/burp.vcxproj.filters b/builds/win32/msvc15/burp.vcxproj.filters index 93cc7a69136..e4a1a947a18 100644 --- a/builds/win32/msvc15/burp.vcxproj.filters +++ b/builds/win32/msvc15/burp.vcxproj.filters @@ -24,6 +24,9 @@ BURP files + + BURP files + BURP files @@ -70,6 +73,15 @@ Header files + + Header files + + + Header files + + + Header files + Header files @@ -82,8 +94,11 @@ Header files + + Header files + Header files - \ No newline at end of file + diff --git a/builds/win32/msvc15/engine_static.vcxproj b/builds/win32/msvc15/engine_static.vcxproj index 8c4feaad72f..cc9f09abc13 100644 --- a/builds/win32/msvc15/engine_static.vcxproj +++ b/builds/win32/msvc15/engine_static.vcxproj @@ -200,6 +200,9 @@ + + + diff --git a/builds/win32/msvc15/engine_static.vcxproj.filters b/builds/win32/msvc15/engine_static.vcxproj.filters index 9b99e9ebb8f..cc260a038df 100644 --- a/builds/win32/msvc15/engine_static.vcxproj.filters +++ b/builds/win32/msvc15/engine_static.vcxproj.filters @@ -432,6 +432,15 @@ Services + + Services + + + Services + + + Services + JRD files\GPRE cpp diff --git a/builds/win32/msvc15/engine_test.vcxproj b/builds/win32/msvc15/engine_test.vcxproj index 6f49efefbed..6410da0123c 100644 --- a/builds/win32/msvc15/engine_test.vcxproj +++ b/builds/win32/msvc15/engine_test.vcxproj @@ -264,6 +264,9 @@ + + + diff --git a/builds/win32/msvc15/engine_test.vcxproj.filters b/builds/win32/msvc15/engine_test.vcxproj.filters index 6dd45c98d06..54185852059 100644 --- a/builds/win32/msvc15/engine_test.vcxproj.filters +++ b/builds/win32/msvc15/engine_test.vcxproj.filters @@ -18,6 +18,9 @@ source + + source + source diff --git a/builds/win32/msvc15/gbak.vcxproj b/builds/win32/msvc15/gbak.vcxproj index 7dd90063bd5..40d3dc5e3ee 100644 --- a/builds/win32/msvc15/gbak.vcxproj +++ b/builds/win32/msvc15/gbak.vcxproj @@ -285,6 +285,9 @@ + + + diff --git a/builds/win32/msvc15/gbak.vcxproj.filters b/builds/win32/msvc15/gbak.vcxproj.filters index 3bbffdaef90..adf432c7e21 100644 --- a/builds/win32/msvc15/gbak.vcxproj.filters +++ b/builds/win32/msvc15/gbak.vcxproj.filters @@ -51,5 +51,14 @@ BURP files + + BURP files + + + BURP files + + + BURP files + - \ No newline at end of file + diff --git a/doc/README.gbak_fast_path.md b/doc/README.gbak_fast_path.md new file mode 100644 index 00000000000..66c95a74100 --- /dev/null +++ b/doc/README.gbak_fast_path.md @@ -0,0 +1,156 @@ +# gbak FAST_PATH mode + +`FAST_PATH` is an optional, non-default backup and restore mode available through the Services Manager. It is intended to make +data-heavy `gbak` operations faster by moving table data directly between the `gbak` service and the database engine internal layers, +with less per-record processing. + +The backup format is unchanged. A backup created with `FAST_PATH` can be restored normally, and an ordinary backup can be +restored with `FAST_PATH`. Database creation and metadata restore continue to use the normal restore process. + +## When to use it + +### Speeding up large operations + +`FAST_PATH` is most useful for large backups or restores where moving and processing many rows is significant. It can +reduce CPU use and elapsed time, but the improvement depends on the database, data types, storage, and number of +parallel workers. Normal restore already has batch and bulk paths, so measure `FAST_PATH` against the current normal +restore with a representative backup or restore before making it the default for all jobs. + +The mode is particularly suitable for data-heavy operations on persistent user tables. For restore, use a trusted backup +and plan appropriate post-restore data-quality checks, because the direct data path bypasses per-row validation performed +by some normal restore paths. + +### Recovering data when normal restore fails + +`FAST_PATH` restore can also be worth trying when a normal restore fails and the goal is to salvage the data. This works +only for a specific class of failures: normal restore rejects rows through its statement machinery — per-row `NOT NULL` +and domain validation expressions, and request/BLR compilation for the statement path. A backup whose rows no longer +satisfy the currently defined rules (for example, taken before a `NOT NULL` or domain constraint was added, or loaded +while enforcement was disabled) can therefore abort a normal restore while the stored values themselves are intact. + +`FAST_PATH` skips that validation pass and the statement machinery: it converts each backed-up message directly into an +engine record with the same conversion routines normal storage uses, then buffers it through bulk insert. Rows rejected +only by validation can load this way. + +Limits to keep in mind: the direct path still checks message lengths and field mappings and still converts data types, +and it is fail-fast — a conversion or engine error aborts the restore rather than skipping the row. It will not rescue +a corrupt or truncated backup stream, missing tables or columns, or unreadable blobs. Treat the recovered database as +containing potentially rule-violating rows: validation metadata itself is restored normally, so later DML is still +checked, but the loaded rows are not retrospectively validated. Inspect the data, fix or remove offending rows, and run +explicit post-restore checks before putting the database into production. + +## Restore semantics + +`FAST_PATH` changes the table-data transfer path, not the backup format or the normal metadata restore process. For eligible +persistent user tables, restore converts backed-up values directly into engine records (using the same conversion routines as +normal record storage) and buffers them through the engine's bulk-insert mechanism, bypassing normal restore statement +execution and its per-row validation processing. + +This must not be confused with replaying application `INSERT` statements. Ordinary `gbak` restore also restores explicit +stored values rather than regenerating column defaults or identity values, and does not replay user DML trigger side effects. +Table `CHECK` constraints implemented through triggers are not a per-row validation guarantee of ordinary restore either. + +The validation difference depends on the normal restore path being compared. Statement-based stores can evaluate field +validation expressions; the normal bulk-insert path has an explicit `NOT NULL` check but not the general store +validation-expression pass. `FAST_PATH` bypasses that `NOT NULL` check as well: the direct writer does not evaluate per-row +`NOT NULL` or domain validation expressions. + +Unless affected by other restore options, validation metadata is restored normally. Retaining that metadata does not +retrospectively validate rows loaded through the direct writer. + +Do not treat either restore mode as a complete audit of application data rules. If additional assurance is required, run +explicit post-restore checks for the relevant domain rules, table constraints, and application invariants. Ordinary restore +is not a way to replay trigger side effects. + +The direct path still checks message lengths and field mappings and performs required data-type conversions; it is not a +complete backup-integrity validator. + +`NO_VALIDITY` (`-no_validity`) is separate from `FAST_PATH`. It omits restored column and domain nullability flags and domain +validation BLR and source; it is not merely a temporary instruction to skip checks during data loading. Those omitted rules are +consequently unavailable for later enforcement unless recreated. `FAST_PATH` alone does not remove those definitions. Conversely, +`NO_VALIDITY` is not a blanket removal of all constraints: table `CHECK` trigger metadata and index-backed primary-key, unique, +and foreign-key constraints follow their normal restore paths. + +## Usage + +`FAST_PATH` requires the `-service` option (Services Manager). It works both via `gbak -service ... -fast` (or `-fast_path`) and via +`fbsvcmgr` / Services API options. + +With `fbsvcmgr`, use `bkp_fast_path` for backup and `res_fast_path` for restore. Paths in these examples are server-side. +For example: + +```text +fbsvcmgr localhost:service_mgr \ + -user SYSDBA -password \ + action_backup dbname /path/source.fdb \ + bkp_file /path/source.fbk bkp_fast_path +``` + +```text +fbsvcmgr localhost:service_mgr \ + -user SYSDBA -password \ + action_restore bkp_file /path/source.fbk \ + dbname /path/restored.fdb res_fast_path +``` + +The same mode is available through `gbak -service`, which forwards its switches via the service command line: + +```text +gbak -service localhost:service_mgr -user SYSDBA -password \ + -backup /path/source.fdb /path/source.fbk -fast_path +``` + +```text +gbak -service localhost:service_mgr -user SYSDBA -password \ + -create_database /path/source.fbk /path/restored.fdb -fast_path +``` + +Services API clients set the corresponding bit in the service-start `isc_spb_options` value: + +| Operation | `fbsvcmgr` option | Services API option bit | +|-----------|-------------------|-------------------------| +| Backup | `bkp_fast_path` | `isc_spb_bkp_fast_path` (`0x040000`) | +| Restore | `res_fast_path` | `isc_spb_res_fast_path` (`0x040000`; backup and restore option bits live in separate per-action namespaces) | + +The option can be combined with the usual backup, restore, and parallel worker options. `FAST_PATH` does not change the normal +table-data selection rules. With `METADATA_ONLY`, backup omits table rows, and restore consumes but does not load any table rows +present in the backup; the direct row-transfer path is therefore not exercised. + +## Scope and eligibility + +The direct data path targets persistent user tables. Normal `gbak` selection happens before `FAST_PATH` eligibility is tested: +system relations with `RDB$SYSTEM_FLAG = 1` are excluded from the backed-up relation list, views do not have their rows backed up, +and external-table rows are omitted unless external-table conversion is requested. Temporary-table contents are not persistent +database data. + +Restore uses the direct data path only for backups in format 8 or newer; tables from older backups are silently loaded through +the normal restore path. + +Privileges are the same as for a normal backup: besides the `USE_GBAK_UTILITY` requirement for the attachment, the backup +checks schema `USAGE`, table `SELECT` and column `SELECT` for every table it reads directly. + +Because `FAST_PATH` uses the engine provider directly instead of the provider list, database names must be local to the server +(a path or an alias). Names with a remote prefix such as `host:path` or `inet://host/path` are not routed and fail. + +## Architecture + +`FAST_PATH` selects the in-process engine provider. Direct VIO access needs an engine `JAttachment`, which the +Services Manager yvalve handle is not, so backup workers always use a private engine attachment - including with a +single worker. Those worker transactions join the main backup snapshot with `isc_tpb_at_snapshot_number` (the same +mechanism as `-parallel` workers). +The reader scans records with `VIO_next_record()` and fills the normal backup message layout; downstream backup serialization +remains unchanged. + +Restore retains normal metadata processing, backup decoding, and blob and array handling. Eligible table-data workers convert each +message into an engine record and buffer it through the transaction's bulk-insert page images, the same mechanism the normal +restore batch path uses to reserve and fill data pages. The direct path is fail-fast: a conversion or engine error aborts the restore +rather than skipping a row. This is also true when `-incremental` is specified; normal-mode relations retain their existing +incremental error handling. Rows buffered in the bulk-insert images are flushed at table end and at commit. In-memory batches use an aligned +stride distinct from the serialized message length so packed records keep field alignment; the backup format itself is unpadded. + +## Verification + +`FAST_PATH` is a trade-off for trusted backups. Restore-test backups, check relevant data rules with application-specific queries, and +benchmark representative scalar-heavy, blob-heavy, and array-heavy workloads with one and multiple workers before enabling the option in +production jobs. CPU, elapsed time, and peak memory should all be compared, since the direct path changes allocation and batching behaviour +as well as per-row CPU. diff --git a/doc/sql.extensions/README.decimal_and_non_decimal_literals.txt b/doc/sql.extensions/README.decimal_and_non_decimal_literals.txt new file mode 100644 index 00000000000..2b971acb15c --- /dev/null +++ b/doc/sql.extensions/README.decimal_and_non_decimal_literals.txt @@ -0,0 +1,154 @@ +============================================== +Decimal integer literals +Non-decimal integer literals (SQL:2023 T661) +Underscores in numeric literals (SQL:2023 T662) +============================================== + +Supports unsigned hexadecimal integers, unsigned octal integers, and unsigned binary integers. +Also support for underscores in numeric and non-decimal literals + +Authors: + Alexey Chudaykin + +Syntax rules: + + ::= + + !! U+002B + + ::= + - !! U+002D + + ::= + . !! U+002E + + ::= + _ !! U+005F + + ::= + 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F | a | b | c | d | e | f + + ::= + 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 + + ::= + 0 | 1 + + ::= + [ ] + + ::= + | + + ::= + | [ ] | + + + ::= + | + + ::= + E + + ::= + [ [ ] ] | + + + ::= + + + ::= + [ ] + + ::= + [ ] + + ::= + | | | + + + ::= + [ { [ ] }... ] + + ::= + 0X { [ ] }... + + ::= + 0O { [ ] }... + + ::= + 0B { [ ] }... + +The letters X, O, B and E may be written in either case. + +Notes (non-decimal integer literals): + 1. The standard allows non-decimal literals in the mantissa of an exponential number entry. + For example (0xAAAe10), but there may be a conflict (0xEEE10). Therefore, non-decimal + literals are not allowed in the mantissa (see above): 0xAAAe10 is the + hexadecimal integer 0xAAAE10 = 11185680. + 2. The value is a numeric value defined by applying the usual + mathematical interpretation of positional hexadecimal notation to a string that is an + unsigned hexadecimal integer. Similarly for an unsigned octal integer and an + unsigned binary integer. + 3. To represent negative values, place a minus sign in front of an unsigned hexadecimal literal. + Similarly for an unsigned octal integer and an unsigned binary integer. + 4. There is no non-decimal literal of type SMALLINT: even 0x1 evaluates to INTEGER. However, + a value within the range 0x0000 (decimal zero) to 0x7FFF (decimal 32767) is converted to + SMALLINT transparently when it is assigned to a SMALLINT column, variable or parameter. + Similarly for an unsigned octal integer and an unsigned binary integer. + 5. The data type depends on the value of the literal, not on the number of digits + (leading zeros do not matter): + 0 .. 0x7FFFFFFF - INTEGER; + 0x80000000 .. 0x7FFFFFFFFFFFFFFF - BIGINT; + 0x8000000000000000 .. 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF - INT128. + A greater value raises an error. Since the literal itself is unsigned, the minimal + INT128 value cannot be written as -0x80000000000000000000000000000000, use the decimal + literal -170141183460469231731687303715884105728 instead. + 6. Compatibility. Previously (see README.hex_literals) a hexadecimal literal was a signed + two's complement value, and its data type depended on the number of . Now the value + is never negative (see note 2) and the data type depends on the value (see note 5): + 0xF0000000 was -268435456 (INTEGER), now 4026531840 (BIGINT); + 0xFFFFFFFFFFFFFFFF was -1 (BIGINT), now 18446744073709551615 (INT128). + Use a minus sign to write negative values, e.g. -0x10000000 instead of 0xF0000000. + +Notes (decimal literals): + 1. Data types of decimal literals are not changed by this feature. Underscores do not affect + the data type and the scale: 1_000.5 is NUMERIC(18, 1), 12_345_678_901_234_567_890 is INT128. + +Notes (underscores in numeric literals): + 1. Limitations for non-decimal integer literals: + 1.1. It is considered unacceptable for there to be two or more consecutive underscores; + 1.2. Underscores are not allowed after the last character. + An underscore is allowed right after the prefix: 0x_FF. + 2. Limitations for decimal literals: + 2.1. Underscores before the first character and after the last character are not allowed; + 2.2. It is considered unacceptable for there to be two or more consecutive underscores; + 2.3. Underscores are not permitted before or after the symbol; + 2.4. Underscores are not allowed before or after the character and the exponent sign. + +Examples (non-decimal integer literals): + 1. Unsigned binary integer: + 1.1. select 0b11010100, 0B11010100 from rdb$database; --> 212; + 1.2. select 0b0000000 from rdb$database; --> 0; + 1.3. select -0b11010100, -0B11010100 from rdb$database; --> -212. + 2. Unsigned octal integer: + 2.1. select 0o12345670, 0O12345670 from rdb$database; --> 2739128; + 2.2. select 0o00000000 from rdb$database; --> 0; + 2.3. select -0o12345670, -0O12345670 from rdb$database; --> -2739128. + 3. Unsigned hexadecimal integer: + 3.1. select 0xABC123, 0XABC123 from rdb$database; --> 11256099; + 3.2. select 0x00000000 from rdb$database; --> 0; + 3.3. select -0xABC123, -0XABC123 from rdb$database; --> -11256099. + 3.4. select 0x7FFFFFFF from rdb$database; --> 2147483647 type INTEGER + 3.5. select 0x80000000 from rdb$database; --> 2147483648 type BIGINT + 3.6. select 0x7FFFFFFFFFFFFFFF from rdb$database; --> 9223372036854775807 type BIGINT + 3.7. select 0x8000000000000000 from rdb$database; --> 9223372036854775808 type INT128 + 3.8. select -0x80000000 from rdb$database; --> -2147483648 type INTEGER + 3.9. select -0x8000000000000000 from rdb$database; --> -9223372036854775808 type BIGINT + +Examples (underscores in numeric literals): + 1. For non-decimal integer literals: + 1.1 Permitted: select 0x_FFFF, 0xFF_FF, 0x_FF_FF from rdb$database; + 1.2 Forbidden: select 0x_FF__FF, 0xFFFFFF_, 0x_FF_FF_FF_ from rdb$database; + 2. For decimal literals: + 2.1 Permitted: select 10_10, 10_10.10_10, 10.10E-10_0 from rdb$database; + 2.2 Forbidden: select _1010, 100_, 10__10, 1010._1010, 1010_.1, 10.10E_-100_ from rdb$database; diff --git a/doc/sql.extensions/README.hex_literals.txt b/doc/sql.extensions/README.hex_literals.txt index add4a1b3acd..67093ae60b2 100644 --- a/doc/sql.extensions/README.hex_literals.txt +++ b/doc/sql.extensions/README.hex_literals.txt @@ -7,6 +7,7 @@ Support for hexadecimal numeric and binary string literals. Authors: Bill Oliver Adriano dos Santos Fernandes + Alexey Chudaykin Syntax: @@ -35,3 +36,8 @@ Notes (binary string literal): Example: select 0x10, cast('0x0F0000000' as bigint) from rdb$database; select x'deadbeef' from rdb$database; + +Notes by Alexey Chudaykin: + ISO/IEC 9075-2:2023(E) introduces rules for hexadecimal numbers, so this document should be + considered obsolete in the part. Refer to the new document + README.decimal_and_non_decimal_literals. diff --git a/extern/icu/tzdata/be.zip b/extern/icu/tzdata/be.zip index d43f06a56b2..272bc6857ea 100644 Binary files a/extern/icu/tzdata/be.zip and b/extern/icu/tzdata/be.zip differ diff --git a/extern/icu/tzdata/le.zip b/extern/icu/tzdata/le.zip index b7f5130a4e5..93e20c8f9f7 100644 Binary files a/extern/icu/tzdata/le.zip and b/extern/icu/tzdata/le.zip differ diff --git a/extern/icu/tzdata/version.txt b/extern/icu/tzdata/version.txt index 9217a2da674..b62e2687afb 100644 --- a/extern/icu/tzdata/version.txt +++ b/extern/icu/tzdata/version.txt @@ -1 +1 @@ -2026c +2026d diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 13059dc5ebe..899fc183bac 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -323,6 +323,7 @@ add_library (alice ${alice_src} ${alice_generated_src_master}) ######################################## file(GLOB burp_src "burp/*.cpp" "burp/*.h") +file(GLOB burp_engine_src "burp/engine/*.cpp" "burp/engine/*.h") set(burp_generated_src burp/backup.epp @@ -459,6 +460,7 @@ set(engine_src ${engine_src} utilities/gsec/gsec.cpp utilities/gstat/ppg.cpp utilities/nbackup/nbackup.cpp + ${burp_engine_src} # parse ${GENERATED_DIR}/dsql/parse.cpp ) @@ -761,11 +763,17 @@ target_link_libraries (gfix alice common yvalve) # EXECUTABLE boot_gbak ######################################## -file(GLOB gbak_include "burp/*.h") +file(GLOB gbak_include "burp/*.h" "burp/engine/*.h") + +set(gbak_stub_src + burp/main/FastPathBurpProviderStub.cpp + burp/main/FastPathRecordReaderStub.cpp + burp/main/FastPathRecordWriterStub.cpp +) if (NOT CMAKE_CROSSCOMPILING) -add_executable (boot_gbak burp/main/burpMain.cpp ${gbak_include} ${VERSION_RC}) +add_executable (boot_gbak burp/main/burpMain.cpp ${gbak_stub_src} ${gbak_include} ${VERSION_RC}) target_link_libraries (boot_gbak boot_burp common boot_yvalve) set_output_directory (boot_gbak . CURRENT_DIR) project_group (boot_gbak Boot) @@ -777,7 +785,7 @@ endif() # if (NOT CMAKE_CROSSCOMPILING) # EXECUTABLE gbak ######################################## -add_executable (gbak burp/main/burpMain.cpp ${gbak_include} ${VERSION_RC}) +add_executable (gbak burp/main/burpMain.cpp ${gbak_stub_src} ${gbak_include} ${VERSION_RC}) target_link_libraries (gbak burp common yvalve) diff --git a/src/burp/BurpTasks.cpp b/src/burp/BurpTasks.cpp index 2235328f102..c4345d6723b 100644 --- a/src/burp/BurpTasks.cpp +++ b/src/burp/BurpTasks.cpp @@ -31,6 +31,7 @@ #include "../common/classes/SafeArg.h" #include "../burp/burp_proto.h" #include "../burp/mvol_proto.h" +#include "../burp/FastPathBurpProvider.h" using MsgFormat::SafeArg; using namespace Firebird; @@ -141,7 +142,7 @@ BackupRelationTask::BackupRelationTask(BurpGlobals* tdgbl) : BurpTask(tdgbl), m_items.add(item); item = FB_NEW_POOL(*pool) Item(this, false); - item->m_ownAttach = false; // will use attach from main thread + item->m_ownAttach = m_masterGbl->gbl_fast_path; // Direct VIO requires an engine attachment. m_items.add(item); for (int i = 1; i < workers; i++) @@ -182,7 +183,11 @@ void BackupRelationTask::SetRelation(burp_rel* relation) m_readDone = false; m_nextPP = 0; - m_metadata.setRelation(m_relation, getMaxWorkers() > 2); + const bool useFastPath = m_masterGbl->gbl_fast_path && + !relation->rel_system && !(relation->rel_flags & (REL_view | REL_external)) && + relation->rel_type == rel_persistent; + + m_metadata.setRelation(m_relation, getMaxWorkers() > 2, useFastPath); } bool BackupRelationTask::handler(WorkItem& _item) @@ -505,14 +510,33 @@ void BackupRelationTask::initItem(BurpGlobals* tdgbl, Item& item) if (!item.m_att) { FbLocalStatus status; - DispatcherPtr provider; + FastPathBurpProvider provider(m_masterGbl->gbl_fast_path); + + // Propagate the database crypt callback so encrypted databases + // requiring per-attachment keys (useOnlyOwnKeys) work on workers. + if (m_masterGbl->gbl_fast_path) + { + ICryptKeyCallback* cryptCb = nullptr; + + if (m_masterGbl->gbl_sw_keyholder) + cryptCb = MVOL_get_crypt(m_masterGbl); + else + cryptCb = m_masterGbl->uSvc->getCryptCallback(); + + if (cryptCb) + { + provider.get()->setDbCryptCallback(&status, cryptCb); + if (status->getState() & IStatus::STATE_ERRORS) + BURP_abort(&status); + } + } // attach, start tran, etc const unsigned char* dpbBuffer = m_masterGbl->gbl_dpb_data.begin(); const unsigned int dpbLength = m_masterGbl->gbl_dpb_data.getCount(); - item.m_att = provider->attachDatabase(&status, tdgbl->gbl_database_file_name, + item.m_att = provider.get()->attachDatabase(&status, tdgbl->gbl_database_file_name, dpbLength, dpbBuffer); if (status->getState() & IStatus::STATE_ERRORS) BURP_abort(&status); @@ -520,10 +544,14 @@ void BackupRelationTask::initItem(BurpGlobals* tdgbl, Item& item) ClumpletWriter tpb(ClumpletReader::Tpb, 128, isc_tpb_version3); tpb.insertTag(isc_tpb_concurrency); tpb.insertTag(isc_tpb_read); + if (tdgbl->gbl_sw_ignore_limbo) tpb.insertTag(isc_tpb_ignore_limbo); + tpb.insertTag(isc_tpb_no_auto_undo); + // add snapshot id + fb_assert(m_masterGbl->tr_snapshot); tpb.insertBigInt(isc_tpb_at_snapshot_number, m_masterGbl->tr_snapshot); item.m_tra = item.m_att->startTransaction(&status, @@ -570,11 +598,13 @@ void BackupRelationTask::freeItem(Item& item) void BackupRelationTask::stopItems() { - MutexLockGuard guard(m_mutex, FB_FUNCTION); - for (Item** p = m_items.begin(); p < m_items.end(); p++) + { + MutexLockGuard guard((*p)->m_mutex, FB_FUNCTION); (*p)->m_cleanCond.notifyAll(); + } + MutexLockGuard guard(m_mutex, FB_FUNCTION); m_dirtyCond.notifyAll(); } @@ -703,7 +733,11 @@ void RestoreRelationTask::SetRelation(BurpGlobals* tdgbl, burp_rel* relation) m_lastRecord = rec_relation_data; m_records = 0; - m_verbRecs = 0; + + { // scope + MutexLockGuard guard(m_verbMutex, FB_FUNCTION); + m_verbRecs = 0; + } m_metadata.setRelation(tdgbl, m_relation); } @@ -732,6 +766,7 @@ bool RestoreRelationTask::handler(WorkItem& _item) } catch (const LongJump&) { + MutexLockGuard guard(m_mutex, FB_FUNCTION); m_stop = true; m_error = true; m_dirtyCond.notifyAll(); @@ -747,6 +782,7 @@ bool RestoreRelationTask::handler(WorkItem& _item) BURP_print_status(&st, true); } + MutexLockGuard guard(m_mutex, FB_FUNCTION); m_stop = true; m_error = true; m_dirtyCond.notifyAll(); @@ -831,6 +867,9 @@ void RestoreRelationTask::verbRecs(FB_UINT64& records, bool total) records = 0; const FB_UINT64 newVerb = (newRecs / m_masterGbl->verboseInterval) * m_masterGbl->verboseInterval; + + MutexLockGuard guard(m_verbMutex, FB_FUNCTION); + if (newVerb > m_verbRecs) { m_verbRecs = newVerb; @@ -841,6 +880,8 @@ void RestoreRelationTask::verbRecs(FB_UINT64& records, bool total) void RestoreRelationTask::verbRecsFinal() { + MutexLockGuard guard(m_verbMutex, FB_FUNCTION); + if (m_verbRecs < static_cast(m_records)) { m_verbRecs = m_records; @@ -890,6 +931,7 @@ void RestoreRelationTask::initItem(BurpGlobals* tdgbl, Item& item) tdgbl->runtimeODS = m_masterGbl->runtimeODS; tdgbl->gbl_use_no_auto_undo = m_masterGbl->gbl_use_no_auto_undo; tdgbl->gbl_use_auto_release_temp_blobid = m_masterGbl->gbl_use_auto_release_temp_blobid; + tdgbl->gbl_fast_path = m_masterGbl->gbl_fast_path; if (item.m_ownAttach) { @@ -897,7 +939,6 @@ void RestoreRelationTask::initItem(BurpGlobals* tdgbl, Item& item) { // attach, start tran, etc FbLocalStatus status; - DispatcherPtr provider; ClumpletWriter dpb(ClumpletReader::dpbList, 128, m_masterGbl->gbl_dpb_data.begin(), @@ -906,8 +947,24 @@ void RestoreRelationTask::initItem(BurpGlobals* tdgbl, Item& item) const UCHAR* dpbBuffer = dpb.getBuffer(); const USHORT dpbLength = dpb.getBufferLength(); - item.m_att = provider->attachDatabase(&status, tdgbl->gbl_database_file_name, - dpbLength, dpbBuffer); + if (tdgbl->gbl_fast_path) + { + ICryptKeyCallback* cryptCb = nullptr; + + if (m_masterGbl->gbl_sw_keyholder) + cryptCb = MVOL_get_crypt(m_masterGbl); + else + cryptCb = m_masterGbl->uSvc->getCryptCallback(); + + item.m_att = FastPathBurpProvider::attachWorker(&status, + tdgbl->gbl_database_file_name, dpbLength, dpbBuffer, cryptCb); + } + else + { + DispatcherPtr provider; + item.m_att = provider->attachDatabase(&status, tdgbl->gbl_database_file_name, + dpbLength, dpbBuffer); + } if (status->getState() & IStatus::STATE_ERRORS) BURP_abort(&status); @@ -960,18 +1017,21 @@ bool RestoreRelationTask::freeItem(Item& item, bool commit) // more detailed message required ? BURP_print_status(&status); } - item.m_tra = nullptr; + else + item.m_tra = nullptr; } if (item.m_tra) { - item.m_tra->rollback(&status); + FbLocalStatus rollbackStatus; + item.m_tra->rollback(&rollbackStatus); item.m_tra = nullptr; } if (item.m_att) { - item.m_att->detach(&status); + FbLocalStatus detachStatus; + item.m_att->detach(&detachStatus); item.m_att = nullptr; } } diff --git a/src/burp/BurpTasks.h b/src/burp/BurpTasks.h index 545716c08d9..9daff507fc9 100644 --- a/src/burp/BurpTasks.h +++ b/src/burp/BurpTasks.h @@ -37,19 +37,34 @@ #include "../common/classes/auto.h" #include "../common/classes/condition.h" #include "../common/classes/fb_atomic.h" +#include "../burp/RestoreMessageLayout.h" +#include "../burp/FastPathRecordWriter.h" +#include "../burp/FastPathRecordReader.h" +#include "../burp/FastPathBurpProvider.h" namespace Burp { +constexpr unsigned FAST_PATH_BACKUP_BATCH_MAX_RECORDS = 1000; +constexpr FB_SIZE_T FAST_PATH_BACKUP_BATCH_MAX_BYTES = 1024 * 1024; +// Staging buffer for the restore direct writer: messages are accumulated here +// so one engine attachment sync covers many rows. The BulkInsert object itself +// stays alive across staging batches (it auto-flushes full extents) and is +// finished only at table end, so staging size bounds memory/latency +// without stranding a partial-extent tail per batch. +constexpr unsigned FAST_PATH_RESTORE_BATCH_MAX_RECORDS = 8000; +constexpr FB_SIZE_T FAST_PATH_RESTORE_BATCH_MAX_BYTES = 8 * 1024 * 1024; + class ReadRelationMeta { public: ReadRelationMeta() noexcept : - m_blr(*getDefaultMemoryPool()) + m_blr(*getDefaultMemoryPool()), + m_layout(*getDefaultMemoryPool()) { clear(); } - void setRelation(const burp_rel* relation, bool partition); + void setRelation(burp_rel* relation, bool partition, bool fastPath); void clear(); bool haveInputs() const noexcept @@ -57,12 +72,24 @@ class ReadRelationMeta return m_inMgsNum != m_outMgsNum; } + bool isFastPath() const noexcept + { + return m_fastPath; + } + + const RestoreMessageLayout& getLayout() const noexcept + { + return m_layout; + } + //private: const burp_rel* m_relation; SSHORT m_fldCount; SSHORT m_inMgsNum; SSHORT m_outMgsNum; Firebird::HalfStaticArray m_blr; + RestoreMessageLayout m_layout; + bool m_fastPath = false; RCRD_LENGTH m_outMsgLen; RCRD_LENGTH m_outRecLen; RCRD_OFFSET m_outEofOffset; @@ -72,7 +99,8 @@ class ReadRelationReq { public: ReadRelationReq() noexcept : - m_outMsg(*getDefaultMemoryPool()) + m_outMsg(*getDefaultMemoryPool()), + m_fastPathBatch(*getDefaultMemoryPool()) { } @@ -86,7 +114,8 @@ class ReadRelationReq void compile(Firebird::CheckStatusWrapper* status, Firebird::IAttachment* db); void setParams(ULONG loPP, ULONG hiPP); - void start(Firebird::CheckStatusWrapper* status, Firebird::ITransaction* tran); + void start(Firebird::CheckStatusWrapper* status, Firebird::IAttachment* att, + Firebird::ITransaction* tran); void receive(Firebird::CheckStatusWrapper* status); void release(Firebird::CheckStatusWrapper* status); @@ -116,8 +145,13 @@ class ReadRelationReq const ReadRelationMeta* m_meta = nullptr; InMsg m_inMgs{}; Firebird::Array m_outMsg; + Firebird::Array m_fastPathBatch; SSHORT* m_eof = nullptr; Firebird::IRequest* m_request = nullptr; + FastPathRecordReader m_fastPathReader; + unsigned m_fastPathBatchRecords = 0; + unsigned m_fastPathBatchPosition = 0; + bool m_usingFastPath = false; }; @@ -125,12 +159,13 @@ class WriteRelationMeta { public: WriteRelationMeta() noexcept : + m_messageLayout(*getDefaultMemoryPool()), m_blr(*getDefaultMemoryPool()) { clear(); } - void setRelation(BurpGlobals* tdgbl, const burp_rel* relation); + void setRelation(BurpGlobals* tdgbl, burp_rel* relation); void clear(); Firebird::IBatch* createBatch(BurpGlobals* tdgbl, Firebird::IAttachment* att); @@ -138,13 +173,22 @@ class WriteRelationMeta //private: bool prepareBatch(BurpGlobals* tdgbl); void prepareRequest(BurpGlobals* tdgbl); + void prepareMessageLayout(BurpGlobals* tdgbl); + void buildNonFastPathMode(BurpGlobals* tdgbl); - const burp_rel* m_relation; + const RestoreMessageLayout& getMessageLayout() const noexcept + { + return m_messageLayout; + } + + burp_rel* m_relation; Firebird::Mutex m_mutex; bool m_batchMode; + bool m_fastPathMode; bool m_batchOk; - ULONG m_inMsgLen; + RCRD_LENGTH m_inMsgLen; ULONG m_blobCount; + RestoreMessageLayout m_messageLayout; // batch mode Firebird::string m_sqlStatement; @@ -162,7 +206,8 @@ class WriteRelationReq public: WriteRelationReq() noexcept : m_inMsg(*getDefaultMemoryPool()), - m_batchMsg(*getDefaultMemoryPool()) + m_batchMsg(*getDefaultMemoryPool()), + m_fastPathBatch(*getDefaultMemoryPool()) { } @@ -174,12 +219,22 @@ class WriteRelationReq void reset(WriteRelationMeta* meta); void clear(); + void initFastPathWriter(Firebird::IAttachment* att, Firebird::ITransaction* tra, + const burp_rel* relation, const RestoreMessageLayout& layout); + void execFastPathBatch(); + + // Flush the bulk insert used by the fast-path writer and destroy the + // writer (it caches the raw engine transaction, which is replaced by + // commit_relation_data() in incremental restore). Runs in get_data() + // before that commit; safe to call when no writer exists. + void finishFastPathBulk(); + void compile(BurpGlobals* tdgbl, Firebird::IAttachment* att); void send(BurpGlobals* tdgbl, Firebird::ITransaction* tran, bool lastRec); void execBatch(BurpGlobals * tdgbl); void release(); - ULONG getDataLength() const noexcept + RCRD_LENGTH getDataLength() const noexcept { return m_inMsg.getCount(); } @@ -214,8 +269,11 @@ class WriteRelationReq WriteRelationMeta* m_meta = nullptr; Firebird::Array m_inMsg; Firebird::Array m_batchMsg; + Firebird::Array m_fastPathBatch; Firebird::IBatch* m_batch = nullptr; Firebird::IRequest* m_request = nullptr; + Firebird::AutoPtr m_fastPathWriter; + unsigned m_fastPathBatchRecs = 0; int m_recs = 0; // total records sent int m_batchRecs = 0; // records in current batch bool m_resync = true; @@ -395,8 +453,8 @@ class BackupRelationTask : public BurpTask Firebird::Mutex m_mutex; Firebird::HalfStaticArray m_items; - volatile bool m_stop; - bool m_error; + std::atomic_bool m_stop; + std::atomic_bool m_error; Firebird::HalfStaticArray m_buffers; Firebird::HalfStaticArray m_dirtyBuffers; @@ -516,9 +574,10 @@ class RestoreRelationTask : public BurpTask Firebird::Mutex m_mutex; Firebird::HalfStaticArray m_items; - volatile bool m_stop; - bool m_error; + std::atomic_bool m_stop; + std::atomic_bool m_error; Firebird::AtomicCounter m_records; // records restored for the current relation + Firebird::Mutex m_verbMutex; FB_UINT64 m_verbRecs; // last records count reported Firebird::HalfStaticArray m_buffers; diff --git a/src/burp/FastPathBurpProvider.h b/src/burp/FastPathBurpProvider.h new file mode 100644 index 00000000000..264f566df0a --- /dev/null +++ b/src/burp/FastPathBurpProvider.h @@ -0,0 +1,62 @@ +/* + * 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) 2026 Adriano dos Santos Fernandes + * and all contributors signed below. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + */ + +#ifndef BURP_FAST_PATH_BURP_PROVIDER_H +#define BURP_FAST_PATH_BURP_PROVIDER_H + +#include "firebird/Interface.h" +#include "../common/classes/ImplementHelper.h" +#include "../common/classes/auto.h" + +namespace Burp { + +class FastPathBurpProvider +{ +private: + struct EngineHolder; + +public: + explicit FastPathBurpProvider(bool useFastPath); + ~FastPathBurpProvider(); + + FastPathBurpProvider(const FastPathBurpProvider&) = delete; + FastPathBurpProvider& operator=(const FastPathBurpProvider&) = delete; + +public: + Firebird::IProvider* get() noexcept + { + return provider; + } + + static Firebird::IAttachment* attachWorker(Firebird::CheckStatusWrapper* status, + const char* dbName, unsigned dpbLen, const unsigned char* dpb, + Firebird::ICryptKeyCallback* cryptCallback); + +private: + Firebird::DispatcherPtr dispatcher; + Firebird::AutoPtr engineHolder; + Firebird::IProvider* provider; +}; + +} // namespace Burp + +#endif // BURP_FAST_PATH_BURP_PROVIDER_H diff --git a/src/burp/FastPathRecordReader.h b/src/burp/FastPathRecordReader.h new file mode 100644 index 00000000000..e1d867e5c07 --- /dev/null +++ b/src/burp/FastPathRecordReader.h @@ -0,0 +1,68 @@ +/* + * 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) 2026 Adriano dos Santos Fernandes + * and all contributors signed below. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + */ + +#ifndef BURP_FAST_PATH_RECORD_READER_H +#define BURP_FAST_PATH_RECORD_READER_H + +#include "../common/common.h" +#include "../common/classes/auto.h" + +struct burp_rel; + +namespace Firebird { + class IAttachment; + class ITransaction; +} + +namespace Burp { + +class RestoreMessageLayout; + +class FastPathRecordReader +{ +private: + struct Imp; + +public: + FastPathRecordReader(); + ~FastPathRecordReader(); + + FastPathRecordReader(const FastPathRecordReader&) = delete; + FastPathRecordReader& operator=(const FastPathRecordReader&) = delete; + +public: + void init(Firebird::IAttachment* att, Firebird::ITransaction* tra, + const burp_rel* relation, const RestoreMessageLayout& layout, bool partition); + + void start(ULONG loPP, ULONG hiPP); + bool readRecord(UCHAR* messageBuffer, RCRD_LENGTH msgLength); + unsigned readRecords(UCHAR* messageBuffer, RCRD_LENGTH msgLength, unsigned maxRecords); + + void clear(); + +private: + Firebird::AutoPtr imp; +}; + +} // namespace Burp + +#endif // BURP_FAST_PATH_RECORD_READER_H diff --git a/src/burp/FastPathRecordWriter.h b/src/burp/FastPathRecordWriter.h new file mode 100644 index 00000000000..4fdd72bdce8 --- /dev/null +++ b/src/burp/FastPathRecordWriter.h @@ -0,0 +1,81 @@ +/* + * 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) 2026 Adriano dos Santos Fernandes + * and all contributors signed below. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + */ + +#ifndef BURP_FAST_PATH_RECORD_WRITER_H +#define BURP_FAST_PATH_RECORD_WRITER_H + +#include "../common/common.h" +#include "../common/classes/auto.h" + +struct burp_rel; + +namespace Firebird { + class IAttachment; + class ITransaction; +} + +namespace Burp { + +class RestoreMessageLayout; + +class FastPathRecordWriter +{ +public: + FastPathRecordWriter(); + ~FastPathRecordWriter(); + + FastPathRecordWriter(const FastPathRecordWriter&) = delete; + FastPathRecordWriter& operator=(const FastPathRecordWriter&) = delete; + +public: + // The caller must route only persistent user tables to direct VIO. Other + // problems raise an error. + void init(Firebird::IAttachment* att, Firebird::ITransaction* tra, + const burp_rel* relation, const RestoreMessageLayout& layout); + + // Convert and buffer a batch of messages using a single engine attachment + // sync. Any failure aborts the direct restore; there is no per-record + // recovery because BulkInsert has no rollback for blob/array side effects. + void writeRecords(const UCHAR* messageBuffer, RCRD_LENGTH msgLength, unsigned recordCount); + + // Flush buffered pages and unregister the bulk insert from the + // transaction. The bulk object auto-flushes full extents internally and + // stays alive across message batches, so this runs at table end and at + // commit; the next row transparently registers a fresh one when needed. + // Safe to call when no bulk insert is registered. + void finishBulk(); + + // Release ArrayFields left behind by records in the last batch. Must only be + // called after a whole VIO batch has been processed: future batches have not + // created their arrays yet. + void releaseRequestlessArrays(); + + void clear(); + +private: + struct Imp; + Firebird::AutoPtr imp; +}; + +} // namespace Burp + +#endif // BURP_FAST_PATH_RECORD_WRITER_H diff --git a/src/burp/RestoreMessageLayout.cpp b/src/burp/RestoreMessageLayout.cpp new file mode 100644 index 00000000000..ee920e34a1f --- /dev/null +++ b/src/burp/RestoreMessageLayout.cpp @@ -0,0 +1,172 @@ +/* + * The contents of this file are subject to the Interbase 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.Inprise.com/IPL.html + * + * Software distributed under the License is distributed on an + * "AS IS" basis, 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 Inprise Corporation + * and its predecessors. Portions created by Inprise Corporation are + * Copyright (C) Inprise Corporation. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + * + * Adriano dos Santos Fernandes + */ + +#include "firebird.h" + +// burp.h and ini.h both define struct gfld with different layouts +#define gfld burp_gfld +#include "../burp/RestoreMessageLayout.h" +#include "../burp/burp.h" +#undef gfld + +#include "../common/classes/SafeArg.h" +#include "../common/dsc_proto.h" +#include "../jrd/align.h" +#include "../jrd/val.h" + +using namespace Firebird; + +namespace Burp { + + +static bool makeMessageDescriptor(dsc& desc, const burp_fld* field, bool fixFssData, CSetId fixFssDataId) +{ + USHORT blrType = field->fld_type; + FLD_LENGTH length = field->fld_length; + + if (field->fld_flags & FLD_array) + blrType = blr_quad; + else + { + // Normalize extended text opcodes: charset/collation are passed + // separately to DSC_make_descriptor(). + switch (blrType) + { + case blr_text2: + blrType = blr_text; + break; + case blr_varying2: + blrType = blr_varying; + break; + case blr_cstring2: + blrType = blr_cstring; + break; + default: + break; + } + } + + if (blrType != blr_blob && blrType > DTYPE_BLR_MAX) + return false; + + SSHORT subType = field->fld_sub_type; + CSetId charSet = field->fld_character_set_id; + + if (field->fld_flags & FLD_array) + { + subType = 0; + charSet = CS_NONE; + } + else if (fixFssData && charSet == CS_UNICODE_FSS && + (blrType == blr_text || blrType == blr_varying || + (blrType == blr_blob && subType == isc_blob_text))) + { + // Same condition as the normal restore message builders: the stored data + // is in the given charset and is converted to UNICODE_FSS on store. + charSet = fixFssDataId; + } + + return DSC_make_descriptor(&desc, blrType, field->fld_scale, length, + subType, charSet, field->fld_collation_id); +} + +[[noreturn]] static void raiseInvalidLength(const burp_rel* relation) +{ + (Arg::Gds(isc_gbak_inv_record_length) << + Arg::Str(relation->rel_name.toQuotedString())).raise(); +} + +static RCRD_OFFSET alignField(const burp_rel* relation, RCRD_OFFSET offset, + USHORT alignment, USHORT length) +{ + if (offset > MAX_RECORD_SIZE) + raiseInvalidLength(relation); + + if (alignment) + offset = FB_ALIGN(offset, alignment); + + if (offset > MAX_RECORD_SIZE || length > MAX_RECORD_SIZE - offset) + raiseInvalidLength(relation); + + return offset; +} + + +void RestoreMessageLayout::build(burp_rel* relation, bool fixFssData, CSetId fixFssDataId) +{ + fb_assert(relation); + + clear(); + + RCRD_OFFSET offset = 0; + USHORT peakAlignment = sizeof(SSHORT); + + for (burp_fld* field = relation->rel_fields; field; field = field->fld_next) + { + if (field->fld_flags & FLD_computed) + continue; + + dsc descriptor; + if (!makeMessageDescriptor(descriptor, field, fixFssData, fixFssDataId)) + (Arg::Gds(isc_gbak_unk_type) << Arg::Num(field->fld_type)).raise(); + + const auto alignment = type_alignments[descriptor.dsc_dtype]; + + if (alignment > peakAlignment) + peakAlignment = alignment; + + const auto fieldOffset = alignField(relation, offset, alignment, descriptor.dsc_length); + field->fld_offset = fieldOffset; + offset = fieldOffset + descriptor.dsc_length; + + fields.add({field, descriptor, alignment, fieldOffset, 0}); + } + + SSHORT count = 0; + for (auto& field : fields) + { + const auto nullOffset = alignField(relation, offset, sizeof(SSHORT), sizeof(SSHORT)); + field.nullOffset = nullOffset; + field.metadata->fld_missing_parameter = count++; + field.metadata->fld_missing_offset = nullOffset; + offset = nullOffset + sizeof(SSHORT); + } + + length = offset; + maxAlignment = fields.hasData() ? peakAlignment : 0; +} + +RCRD_LENGTH RestoreMessageLayout::getStride() const noexcept +{ + if (!maxAlignment) + return length; + return FB_ALIGN(length, maxAlignment); +} + +void RestoreMessageLayout::clear() noexcept +{ + fields.clear(); + length = 0; + maxAlignment = 0; +} + + +} // namespace Burp diff --git a/src/burp/RestoreMessageLayout.h b/src/burp/RestoreMessageLayout.h new file mode 100644 index 00000000000..c0b27defd41 --- /dev/null +++ b/src/burp/RestoreMessageLayout.h @@ -0,0 +1,96 @@ +/* + * 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) 2026 Adriano dos Santos Fernandes + * and all contributors signed below. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + */ + +#ifndef BURP_RESTORE_MESSAGE_LAYOUT_H +#define BURP_RESTORE_MESSAGE_LAYOUT_H + +#include "../common/classes/array.h" +#include "../common/dsc.h" + +struct burp_fld; +struct burp_rel; + +namespace Burp { + +class RestoreMessageLayout +{ +public: + struct Field + { + burp_fld* metadata; + dsc descriptor; + USHORT alignment; + RCRD_OFFSET offset; + RCRD_OFFSET nullOffset; + }; + +public: + explicit RestoreMessageLayout(Firebird::MemoryPool& pool) + : fields(pool) + { + } + + RestoreMessageLayout(const RestoreMessageLayout&) = delete; + RestoreMessageLayout& operator=(const RestoreMessageLayout&) = delete; + +public: + // Computes message offsets for all non-computed fields and writes them back + // into the field metadata (fld_offset, fld_missing_*) - the same convention + // as the BLR message builders. + void build(burp_rel* relation, bool fixFssData, CSetId fixFssDataId); + + void clear() noexcept; + + RCRD_LENGTH getLength() const noexcept + { + return length; + } + + USHORT getMaxAlignment() const noexcept + { + return maxAlignment; + } + + // In-memory batch stride: message length rounded up to the maximum field + // alignment so consecutive packed records keep descriptor alignment. + // The serialized backup format still uses getLength(). + RCRD_LENGTH getStride() const noexcept; + + const Field* begin() const noexcept + { + return fields.begin(); + } + + const Field* end() const noexcept + { + return fields.end(); + } + +private: + Firebird::HalfStaticArray fields; + RCRD_LENGTH length = 0; + USHORT maxAlignment = 0; +}; + +} // namespace Burp + +#endif // BURP_RESTORE_MESSAGE_LAYOUT_H diff --git a/src/burp/backup.epp b/src/burp/backup.epp index 6fa5b06f812..c1a8cc0ebac 100644 --- a/src/burp/backup.epp +++ b/src/burp/backup.epp @@ -255,9 +255,11 @@ int BACKUP_backup(const TEXT* dbb_file, const TEXT* file_name) EXEC SQL SET TRANSACTION NAME gds_trans; } - // get shared snapshot number if asked for parallel backup + // Readers that use a private attachment must join this snapshot via + // isc_tpb_at_snapshot_number. FAST_PATH always does (VIO needs a + // JAttachment, not the yvalve master handle), as does -parallel > 1. tdgbl->tr_snapshot = 0; - if (gds_trans && tdgbl->gbl_sw_par_workers > 1) + if (gds_trans && (tdgbl->gbl_fast_path || tdgbl->gbl_sw_par_workers > 1)) { UCHAR in_buf[] = {fb_info_tra_snapshot_number, isc_info_end}; UCHAR out_buf[16] = {0}; @@ -268,25 +270,37 @@ int BACKUP_backup(const TEXT* dbb_file, const TEXT* file_name) UCHAR* p = out_buf, *e = out_buf + sizeof(out_buf); while (p < e) { - SSHORT len; - switch (*p++) - { - case isc_info_error: - case isc_info_end: - p = e; + const UCHAR item = *p++; + if (item == isc_info_end || item == isc_info_error || p + 2 > e) break; - case fb_info_tra_snapshot_number: - len = isc_portable_integer(p, 2); - p += 2; - tdgbl->tr_snapshot = isc_portable_integer(p, len); - p += len; + const USHORT len = isc_portable_integer(p, 2); + p += 2; + if (p + len > e) break; + + switch (item) + { + case fb_info_tra_snapshot_number: + tdgbl->tr_snapshot = isc_portable_integer(p, len); + break; } + + p += len; } } + if (tdgbl->tr_snapshot == 0) - tdgbl->gbl_sw_par_workers = 1; + { + if (tdgbl->gbl_fast_path) + { + // msg 432 cannot obtain transaction snapshot number for FAST_PATH backup + BURP_error(432, true); + } + + if (tdgbl->gbl_sw_par_workers > 1) + tdgbl->gbl_sw_par_workers = 1; + } } // detect if MAKE_DBKEY is supported and decide kind of read relation query @@ -1565,7 +1579,7 @@ void put_data(burp_rel* relation, ReadRelationReq* request) count++; // eof FbLocalStatus status; - request->start(&status, gds_trans); + request->start(&status, DB, gds_trans); if (status->getState() & IStatus::STATE_ERRORS) { BURP_error_redirect(&status, 28); @@ -4211,6 +4225,9 @@ void write_relations() relation->rel_next = tdgbl->relations; tdgbl->relations = relation; relation->rel_id = X.RDB$RELATION_ID; + relation->rel_type = X.RDB$RELATION_TYPE.NULL ? + (!X.RDB$EXTERNAL_FILE.NULL ? rel_external : rel_persistent) : + static_cast(X.RDB$RELATION_TYPE); if (!X.RDB$SCHEMA_NAME.NULL) relation->rel_name.schema = X.RDB$SCHEMA_NAME; @@ -4270,6 +4287,7 @@ void write_relations() relation->rel_next = tdgbl->relations; tdgbl->relations = relation; relation->rel_id = X.RDB$RELATION_ID; + relation->rel_type = !X.RDB$EXTERNAL_FILE.NULL ? rel_external : rel_persistent; relation->rel_name = name; relation->rel_flags |= flags; put_relation (relation); @@ -4933,9 +4951,13 @@ namespace Burp { /// class ReadRelationMeta -void ReadRelationMeta::setRelation(const burp_rel* relation, bool partition) +void ReadRelationMeta::setRelation(burp_rel* relation, bool partition, bool fastPath) { m_relation = relation; + m_fastPath = fastPath; + + if (m_fastPath) + m_layout.build(relation, false, CS_NONE); // Build request BLR. There could be two kind of requests : // a) partition == true @@ -5004,6 +5026,16 @@ void ReadRelationMeta::setRelation(const burp_rel* relation, bool partition) if (field->fld_flags & FLD_array) blrType = blr_blob; + // Normalize text2 family like RestoreMessageLayout does, so tables + // using text2/varying2/cstring2 don't abort BLR building before + // fast-path gets a chance (VIO normalizes the same way). + if (blrType == blr_text2) + blrType = blr_text; + else if (blrType == blr_varying2) + blrType = blr_varying; + else if (blrType == blr_cstring2) + blrType = blr_cstring; + if (blrType <= DTYPE_BLR_MAX) { const auto dtype = gds_cvt_blr_dtype[blrType]; @@ -5014,7 +5046,8 @@ void ReadRelationMeta::setRelation(const burp_rel* relation, bool partition) { case blr_text: case blr_varying: - add_byte(blr, field->fld_type); + case blr_cstring: + add_byte(blr, blrType); add_word(blr, field->fld_length); if (blrType == blr_varying) length += sizeof(USHORT); @@ -5073,6 +5106,7 @@ void ReadRelationMeta::setRelation(const burp_rel* relation, bool partition) add_byte(blr, 0); offset = FB_ALIGN(offset, sizeof(SSHORT)); field->fld_missing_parameter = count++; + field->fld_missing_offset = offset; offset += sizeof(SSHORT); } @@ -5086,6 +5120,31 @@ void ReadRelationMeta::setRelation(const burp_rel* relation, bool partition) // To be used later for the buffer size to receive data m_outMsgLen = m_outEofOffset + sizeof(SSHORT); + // The fast-path and BLR paths must describe identical message + // layouts: VIO fills buffers at layout offsets while put_data() locates + // blob/array quads via fld_offset. Drift between the two computations + // would silently corrupt backups, so fail closed if they disagree. + if (m_fastPath) + { + bool layoutOk = m_layout.getLength() == m_outRecLen && + m_outMsgLen == m_layout.getLength() + sizeof(SSHORT); + + for (const auto& layoutField : m_layout) + { + layoutOk = layoutOk && + layoutField.metadata->fld_offset == layoutField.offset && + layoutField.metadata->fld_missing_offset == layoutField.nullOffset; + } + + if (!layoutOk) + { + fb_assert(false); + (Arg::Gds(isc_random) << + Arg::Str("FAST_PATH produced an inconsistent message layout") << + Arg::Str(relation->rel_name.toQuotedString())).raise(); + } + } + if (partition) { add_byte(blr, blr_receive); @@ -5209,6 +5268,8 @@ void ReadRelationMeta::clear() m_relation = NULL; m_fldCount = 0; m_blr.clear(); + m_layout.clear(); + m_fastPath = false; m_inMgsNum = m_outMgsNum = 0; m_outMsgLen = m_outRecLen = m_outEofOffset = 0; } @@ -5230,13 +5291,19 @@ void ReadRelationReq::reset(const ReadRelationMeta* meta) m_relation = m_meta->m_relation; BurpGlobals* tdgbl = BurpGlobals::getSpecific(); - FbLocalStatus status; - compile(&status, DB); - if (status->getState() & IStatus::STATE_ERRORS) + + if (!m_meta->isFastPath()) { - BURP_error_redirect(&status, 27); - // msg 27 isc_compile_request failed - fb_print_blr(m_meta->m_blr.begin(), m_meta->m_blr.getCount(), NULL, NULL, 0); + FbLocalStatus status; + + compile(&status, DB); + + if (status->getState() & IStatus::STATE_ERRORS) + { + BURP_error_redirect(&status, 27); + // msg 27 isc_compile_request failed + fb_print_blr(m_meta->m_blr.begin(), m_meta->m_blr.getCount(), NULL, NULL, 0); + } } UCHAR* data = m_outMsg.getBuffer(m_meta->m_outMsgLen); @@ -5254,10 +5321,15 @@ void ReadRelationReq::reset(const ReadRelationMeta* meta) void ReadRelationReq::clear() { + m_fastPathReader.clear(); + m_usingFastPath = false; + m_fastPathBatchRecords = 0; + m_fastPathBatchPosition = 0; m_relation = NULL; m_meta = NULL; m_eof = NULL; m_outMsg.clear(); + m_fastPathBatch.clear(); if (m_request) { @@ -5273,6 +5345,12 @@ void ReadRelationReq::compile(CheckStatusWrapper* status, IAttachment* att) void ReadRelationReq::release(CheckStatusWrapper* status) { + if (m_usingFastPath) + { + clear(); + return; + } + if (m_request) { m_request->free(status); @@ -5291,8 +5369,18 @@ void ReadRelationReq::setParams(ULONG loPP, ULONG hiPP) m_inMgs.hiPP = hiPP; } -void ReadRelationReq::start(CheckStatusWrapper* status, ITransaction* tran) +void ReadRelationReq::start(CheckStatusWrapper* status, IAttachment* att, ITransaction* tran) { + if (m_meta->isFastPath()) + { + m_fastPathReader.init(att, tran, m_relation, m_meta->getLayout(), m_meta->haveInputs()); + m_usingFastPath = true; + m_fastPathBatchRecords = 0; + m_fastPathBatchPosition = 0; + m_fastPathReader.start(m_inMgs.loPP, m_inMgs.hiPP); + return; + } + if (m_meta->haveInputs()) m_request->startAndSend(status, tran, 0, m_meta->m_inMgsNum, sizeof(m_inMgs), &m_inMgs); else @@ -5301,7 +5389,50 @@ void ReadRelationReq::start(CheckStatusWrapper* status, ITransaction* tran) void ReadRelationReq::receive(CheckStatusWrapper* status) { - m_request->receive(status, 0, m_meta->m_outMgsNum, m_meta->m_outMsgLen, m_outMsg.begin()); + if (!m_usingFastPath) + { + m_request->receive(status, 0, m_meta->m_outMgsNum, m_meta->m_outMsgLen, m_outMsg.begin()); + return; + } + + memset(m_outMsg.begin(), 0, m_meta->m_outMsgLen); + + // Aligned stride keeps descriptor alignment between packed records; + // serialization still uses m_outMsgLen. + FB_SIZE_T stride = m_meta->m_outMsgLen; + + if (const USHORT maxAlign = m_meta->getLayout().getMaxAlignment()) + stride = FB_ALIGN(stride, maxAlign); + + if (m_fastPathBatchPosition == m_fastPathBatchRecords) + { + unsigned batchRecords = FAST_PATH_BACKUP_BATCH_MAX_RECORDS; + + if (stride) + { + batchRecords = MIN(batchRecords, + static_cast(FAST_PATH_BACKUP_BATCH_MAX_BYTES / stride)); + } + + batchRecords = MAX(batchRecords, 1); + + m_fastPathBatchRecords = m_fastPathReader.readRecords( + m_fastPathBatch.getBuffer(static_cast(batchRecords) * stride, false), + static_cast(stride), batchRecords); + m_fastPathBatchPosition = 0; + } + + if (m_fastPathBatchPosition < m_fastPathBatchRecords) + { + memcpy(m_outMsg.begin(), + m_fastPathBatch.begin() + static_cast(m_fastPathBatchPosition) * stride, + m_meta->m_outMsgLen); + + ++m_fastPathBatchPosition; + *m_eof = 1; + } + else + *m_eof = 0; } diff --git a/src/burp/burp.cpp b/src/burp/burp.cpp index c02b5892969..33daec8c3b5 100644 --- a/src/burp/burp.cpp +++ b/src/burp/burp.cpp @@ -1042,6 +1042,11 @@ int gbak(Firebird::UtilSvc* uSvc) BURP_error(334, true, SafeArg() << in_sw_tab->in_sw_name); tdgbl->gbl_sw_deactivate_indexes = true; break; + case IN_SW_BURP_FAST_PATH: + if (tdgbl->gbl_fast_path) + BURP_error(334, true, SafeArg() << in_sw_tab->in_sw_name); + tdgbl->gbl_fast_path = true; + break; case IN_SW_BURP_IG: if (ignoreDamaged) BURP_error(334, true, SafeArg() << in_sw_tab->in_sw_name); @@ -1299,6 +1304,16 @@ int gbak(Firebird::UtilSvc* uSvc) if (!sw_replace) sw_replace = IN_SW_BURP_B; + if (tdgbl->gbl_fast_path) + { + // Single enforcement point for FAST_PATH: it requires -service. + if (!uSvc->isService()) + { + // msg 430 FAST_PATH requires the -service option + BURP_error(430, true); + } + } + if (sw_replace == IN_SW_BURP_B) { for (burp_fil* f = tdgbl->gbl_sw_files; f; f = f->fil_next) diff --git a/src/burp/burp.h b/src/burp/burp.h index e21e2c69613..bf57720c840 100644 --- a/src/burp/burp.h +++ b/src/burp/burp.h @@ -817,7 +817,9 @@ struct burp_rel burp_rel* rel_next; burp_fld* rel_fields; SSHORT rel_flags; + bool rel_system; // set on restore; backup relies on BURP_alloc_zero (false = user table) SSHORT rel_id; + SSHORT rel_type; // RDB$RELATION_TYPE (rel_persistent, rel_view, ...); zero == persistent Firebird::QualifiedMetaString rel_name; GDS_NAME rel_owner; // relation owner, if not us ULONG rel_max_pp; // max pointer page sequence number @@ -1305,6 +1307,7 @@ class BurpGlobals : public Firebird::ThreadData, public GblPool bool gbl_use_no_auto_undo = true; bool gbl_use_auto_release_temp_blobid = true; + bool gbl_fast_path = false; }; // CVC: This aux routine declared here to not force inclusion of burp.h with burp_proto.h diff --git a/src/burp/burpswi.h b/src/burp/burpswi.h index 10d67fd7ac5..60901a490b6 100644 --- a/src/burp/burpswi.h +++ b/src/burp/burpswi.h @@ -104,6 +104,7 @@ inline constexpr int IN_SW_BURP_DIRECT_IO = 55; // direct IO for backup files inline constexpr int IN_SW_BURP_SKIP_SCHEMA_DATA = 56; // skip data from schema inline constexpr int IN_SW_BURP_INCLUDE_SCHEMA_DATA = 57; // backup data from schemas +inline constexpr int IN_SW_BURP_FAST_PATH = 58; // fast-path backup/restore via VIO /**************************************************************************/ @@ -133,6 +134,8 @@ static inline constexpr Switches::in_sw_tab_t reference_burp_in_sw_table[] = // msg 97: @1EXPAND no data compression {IN_SW_BURP_FA, isc_spb_bkp_factor, "FACTOR", 0, 0, 0, false, false, 181, 2, NULL, boBackup}, /* msg 181; @1FACTOR blocking factor */ + {IN_SW_BURP_FAST_PATH, isc_spb_bkp_fast_path, "FAST_PATH", 0, 0, 0, false, true, 431, 4, NULL, boGeneral}, + // msg 431: @1FAST(_PATH) fast-path data transfer {IN_SW_BURP_FETCHPASS, 0, "FETCH_PASSWORD", 0, 0, 0, false, false, 306, 2, NULL, boGeneral}, // msg 306: @1FE(TCH_PASSWORD) fetch password from file {IN_SW_BURP_FIX_FSS_DATA, isc_spb_res_fix_fss_data, @@ -249,4 +252,3 @@ static inline constexpr const char* burp_repl_mode_sw_table[] = const int BURP_MAX_PARALLEL_WORKERS = 64; #endif // BURP_BURP_H - diff --git a/src/burp/engine/FastPathBurpUtil.cpp b/src/burp/engine/FastPathBurpUtil.cpp new file mode 100644 index 00000000000..3f53ab9160c --- /dev/null +++ b/src/burp/engine/FastPathBurpUtil.cpp @@ -0,0 +1,69 @@ +/* + * 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) 2026 Adriano dos Santos Fernandes + * and all contributors signed below. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + */ + +#include "firebird.h" + +#include "../burp/FastPathBurpProvider.h" +#include "../common/classes/alloc.h" +#include "../common/classes/GetPlugins.h" +#include "../jrd/EngineInterface.h" + +using namespace Firebird; +using namespace Jrd; + +namespace Burp { + + +struct FastPathBurpProvider::EngineHolder +{ + AutoPlugin provider{JProvider::getInstance()}; +}; + + +FastPathBurpProvider::FastPathBurpProvider(bool useFastPath) + : provider(dispatcher) +{ + if (useFastPath) + { + engineHolder = FB_NEW EngineHolder(); + provider = engineHolder->provider; + } +} + +FastPathBurpProvider::~FastPathBurpProvider() = default; + +IAttachment* FastPathBurpProvider::attachWorker(CheckStatusWrapper* status, const char* dbName, + unsigned dpbLen, const unsigned char* dpb, ICryptKeyCallback* cryptCallback) +{ + AutoPlugin engineProvider(JProvider::getInstance()); + + if (cryptCallback) + engineProvider->setDbCryptCallback(status, cryptCallback); + + if (status->getState() & IStatus::STATE_ERRORS) + return nullptr; + + return engineProvider->attachDatabase(status, dbName, dpbLen, dpb); +} + + +} // namespace Burp diff --git a/src/burp/engine/FastPathRecordReader.cpp b/src/burp/engine/FastPathRecordReader.cpp new file mode 100644 index 00000000000..5ea7462d12d --- /dev/null +++ b/src/burp/engine/FastPathRecordReader.cpp @@ -0,0 +1,402 @@ +/* + * The contents of this file are subject to the Interbase 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.Inprise.com/IPL.html + * + * Software distributed under the License is distributed on an + * "AS IS" basis, 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 Inprise Corporation + * and its predecessors. Portions created by Inprise Corporation are + * Copyright (C) Inprise Corporation. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + * + * Adriano dos Santos Fernandes + */ + +#include "firebird.h" + +// burp.h and ini.h both define struct gfld with different layouts +#define gfld burp_gfld +#include "../burp/FastPathRecordReader.h" +#include "../burp/RestoreMessageLayout.h" +#include "../burp/burp.h" +#undef gfld + +#include "../burp/burp_proto.h" +#include "../common/dsc_proto.h" +#include "../jrd/EngineInterface.h" +#include "../jrd/Attachment.h" +#include "../jrd/Database.h" +#include "../jrd/jrd.h" +#include "../jrd/Record.h" +#include "../jrd/Relation.h" +#include "../jrd/met.h" +#include "../jrd/mov_proto.h" +#include "../jrd/req.h" +#include "../jrd/rlck_proto.h" +#include "../jrd/scl.h" +#include "../jrd/scl_proto.h" +#include "../jrd/tdbb.h" +#include "../jrd/tra.h" +#include "../jrd/vio_proto.h" +#include "../jrd/cch_proto.h" +#include "../jrd/blb.h" +#include "../jrd/evl_proto.h" + +using namespace Firebird; +using namespace Jrd; + +namespace Burp { + + +struct FastPathRecordReader::Imp +{ + struct Field + { + dsc target; + RCRD_OFFSET offset; + RCRD_OFFSET nullOffset; + USHORT fieldId; + bool array; + }; + + IAttachment* att = nullptr; + ITransaction* tra = nullptr; + const burp_rel* burpRelation = nullptr; + JAttachment* jAtt = nullptr; + Attachment* attachment = nullptr; + jrd_tra* transaction = nullptr; + jrd_rel* relation = nullptr; + const RestoreMessageLayout* layout = nullptr; + RCRD_LENGTH messageLength = 0; + + HalfStaticArray fields; + record_param rpb; + RecordNumber upper; + bool partition = false; + bool scanActive = false; + bool initialized = false; + + Imp() + : fields(*getDefaultMemoryPool()), + rpb() + {} + + // record_param is a plain struct: VIO_record() allocates rpb_record from + // the default pool on first use (see VIO_data) and reuses it afterwards, + // but nothing frees it (sweep code deletes it explicitly too). + // Always use this instead of assigning a fresh record_param. + void resetRpb() + { + delete rpb.rpb_record; + rpb = record_param(); + } + + // The direct scan does not compile a request, so check the same privileges + // that a BLR FOR over the relation would verify: schema USAGE, relation + // SELECT and SELECT on every read column. + void checkAccess(thread_db* tdbb) + { + const auto& relName = relation->getName(); + const auto& securityName = relation->getSecurityName(); + + SCL_check_schema(tdbb, relName.schema, SCL_usage); + + if (const auto sClass = SCL_get_class(tdbb, securityName.object)) + SCL_check_access(tdbb, sClass, 0, {}, SCL_select, obj_relations, false, relName); + + const auto relFields = relation->rel_fields; + + for (const auto& field : fields) + { + const jrd_fld* const jrdField = (relFields && field.fieldId < relFields->count()) ? + (*relFields)[field.fieldId] : nullptr; + + if (!jrdField) + continue; + + if (const auto sClass = SCL_get_class(tdbb, jrdField->fld_security_name)) + { + SCL_check_access(tdbb, sClass, 0, {}, SCL_select, obj_column, false, + relName, jrdField->fld_name); + } + } + } + + // Balance the rel_scan_count increment made in start(). Called on natural + // EOF and from clear(), so a finished scan does not keep the relation + // registered as actively scanned across pointer-page slices or until the + // reader is destroyed. + void finishScan() + { + if (!scanActive) + return; + + scanActive = false; + + if (relation && relation->getPermanent()->rel_scan_count) + --relation->getPermanent()->rel_scan_count; + } +}; + + +[[noreturn]] static void raiseInvalidField(const burp_rel* relation, const burp_fld* field, + const char* reason) +{ + (Arg::Gds(isc_gbak_inv_column) << Arg::Str(field->fld_name) << + Arg::Str(relation->rel_name.toQuotedString()) << Arg::Str(reason)).raise(); +} + + +FastPathRecordReader::FastPathRecordReader() + : imp(FB_NEW_POOL(*getDefaultMemoryPool()) Imp()) +{ +} + +FastPathRecordReader::~FastPathRecordReader() +{ + clear(); +} + +void FastPathRecordReader::init(IAttachment* att, ITransaction* tra, const burp_rel* relation, + const RestoreMessageLayout& layout, bool partition) +{ + fb_assert(att && tra && relation && imp); + + clear(); + imp->att = att; + imp->tra = tra; + imp->burpRelation = relation; + imp->layout = &layout; + imp->messageLength = layout.getLength(); + imp->partition = partition; + imp->jAtt = static_cast(att); + imp->attachment = imp->jAtt->getHandle(); + fb_assert(imp->attachment); + + FbLocalStatus status; + EngineContextHolder tdbb(&status, imp->jAtt, FB_FUNCTION); + + imp->transaction = imp->jAtt->getEngineTransaction(&status, tra); + + if (status->hasData()) + BURP_abort(&status); + + if (!imp->transaction) + Arg::Gds(isc_bad_trans_handle).raise(); + + tdbb->setTransaction(imp->transaction); + + imp->relation = MetadataCache::getVersioned(tdbb, relation->rel_id, + CacheFlag::AUTOCREATE); + + if (!imp->relation) + { + (Arg::Gds(isc_gbak_tbl_missing) << + Arg::Str(relation->rel_name.toQuotedString())).raise(); + } + + if (relation->rel_type != rel_persistent || + (relation->rel_flags & (::REL_view | ::REL_external)) || + imp->relation->isSystem() || imp->relation->isView() || + imp->relation->isVirtual() || imp->relation->isTemporary() || + imp->relation->getExtFile()) + { + fb_assert(false); + (Arg::Gds(isc_random) << + Arg::Str("FAST_PATH selected an ineligible relation") << + Arg::Str(relation->rel_name.toQuotedString())).raise(); + } + + const Format* const format = imp->relation->currentFormat(tdbb); + if (!format) + { + fb_assert(false); + (Arg::Gds(isc_random) << + Arg::Str("FAST_PATH relation has no current format") << + Arg::Str(relation->rel_name.toQuotedString())).raise(); + } + + for (const auto& layoutField : layout) + { + const burp_fld* field = layoutField.metadata; + if (field->fld_id < 0 || field->fld_id >= format->fmt_count) + raiseInvalidField(relation, field, "field is not present in the source table"); + + const dsc source = format->fmt_desc[field->fld_id]; + if (source.isUnknown() || (source.dsc_flags & DSC_computed)) + raiseInvalidField(relation, field, "invalid source descriptor"); + + Imp::Field mapping; + mapping.target = layoutField.descriptor; + mapping.offset = layoutField.offset; + mapping.nullOffset = layoutField.nullOffset; + mapping.fieldId = static_cast(field->fld_id); + mapping.array = (field->fld_flags & FLD_array) != 0; + imp->fields.add(mapping); + } + + imp->checkAccess(tdbb); + + RLCK_reserve_relation(tdbb, imp->transaction, imp->relation->getPermanent(), false); + imp->initialized = true; +} + +void FastPathRecordReader::start(ULONG loPP, ULONG hiPP) +{ + fb_assert(imp && imp->initialized); + + FbLocalStatus status; + EngineContextHolder tdbb(&status, imp->jAtt, FB_FUNCTION); + tdbb->setTransaction(imp->transaction); + + imp->resetRpb(); + imp->finishScan(); + imp->rpb.rpb_relation = imp->relation; + imp->rpb.rpb_stream_flags = 0; + imp->rpb.getWindow(tdbb).win_flags = WIN_large_scan; + imp->rpb.rpb_org_scans = imp->relation->getPermanent()->rel_scan_count++; + imp->rpb.rpb_number.setValue(BOF_NUMBER); + + if (imp->partition) + { + const auto dbb = tdbb->getDatabase(); + + imp->rpb.rpb_number.compose(dbb->dbb_max_records, dbb->dbb_dp_per_pp, + 0, 0, loPP); + imp->rpb.rpb_number.decrement(); + + imp->upper.compose(dbb->dbb_max_records, dbb->dbb_dp_per_pp, + 0, 0, hiPP); + imp->upper.decrement(); + } + + imp->scanActive = true; +} + +bool FastPathRecordReader::readRecord(UCHAR* messageBuffer, RCRD_LENGTH msgLength) +{ + fb_assert(imp && imp->initialized && messageBuffer); + return readRecords(messageBuffer, msgLength, 1) != 0; +} + +unsigned FastPathRecordReader::readRecords(UCHAR* messageBuffer, RCRD_LENGTH msgLength, unsigned maxRecords) +{ + fb_assert(imp && imp->initialized && messageBuffer); + + // A scan ended (EOF reached) and the caller asks again for records, e.g. + // when the last batch was partial. Report EOF without touching the + // finished scan. + if (!imp->scanActive) + return 0; + + if (!maxRecords) + return 0; + + if (msgLength < imp->messageLength) + { + // Internal caller invariant: the stride is derived from the same layout. + fb_assert(false); + (Arg::Gds(isc_random) << + Arg::Str("FAST_PATH message stride is smaller than the layout") << + Arg::Str(imp->burpRelation->rel_name.toQuotedString())).raise(); + } + + FbLocalStatus status; + // The attachment sync stays held for the whole batch; JRD_reschedule() + // inside the loop keeps shutdown/AST handling responsive. + EngineContextHolder tdbb(&status, imp->jAtt, FB_FUNCTION); + tdbb->setTransaction(imp->transaction); + + const auto scope = imp->partition ? DPM_next_pointer_page : DPM_next_all; + const RecordNumber* const upper = imp->partition ? &imp->upper : nullptr; + unsigned records = 0; + + while (records < maxRecords) + { + JRD_reschedule(tdbb); + + // With a pool given, VIO_next_record -> VIO_data copies the record out + // and releases the page window, so no CCH_RELEASE is needed here. + if (!VIO_next_record(tdbb, &imp->rpb, imp->transaction, tdbb->getDefaultPool(), scope, upper)) + { + imp->rpb.rpb_number.setValid(false); + imp->finishScan(); + break; + } + + UCHAR* const recordBuffer = messageBuffer + static_cast(records) * msgLength; + memset(recordBuffer, 0, msgLength); + + for (const auto& field : imp->fields) + { + dsc source; + const bool present = EVL_field(imp->relation, imp->rpb.rpb_record, field.fieldId, &source); + const SSHORT nullFlag = present ? 0 : -1; + memcpy(recordBuffer + field.nullOffset, &nullFlag, sizeof(nullFlag)); + + if (!present) + continue; + + dsc target = field.target; + target.dsc_address = recordBuffer + field.offset; + + if (field.array || DTYPE_IS_BLOB_OR_QUAD(target.dsc_dtype)) + blb::move(tdbb, &source, &target, nullptr, nullptr, 0); + else + MOV_move(tdbb, &source, &target); + } + + ++records; + } + + return records; +} + +void FastPathRecordReader::clear() +{ + if (!imp) + return; + + // Touching rel_scan_count requires the engine sync, also in this + // (possibly destructor) path. The attachment may already be shut down; + // never let that escape from here. + if (imp->scanActive && imp->jAtt && imp->relation) + { + try + { + FbLocalStatus status; + EngineContextHolder tdbb(&status, imp->jAtt, FB_FUNCTION); + imp->finishScan(); + } + catch (const Exception&) + { + imp->scanActive = false; + } + } + + imp->att = nullptr; + imp->tra = nullptr; + imp->burpRelation = nullptr; + imp->jAtt = nullptr; + imp->attachment = nullptr; + imp->transaction = nullptr; + imp->relation = nullptr; + imp->layout = nullptr; + imp->messageLength = 0; + imp->fields.clear(); + imp->resetRpb(); + imp->upper = RecordNumber(); + imp->partition = false; + imp->scanActive = false; + imp->initialized = false; +} + + +} // namespace Burp diff --git a/src/burp/engine/FastPathRecordWriter.cpp b/src/burp/engine/FastPathRecordWriter.cpp new file mode 100644 index 00000000000..5b862259a4f --- /dev/null +++ b/src/burp/engine/FastPathRecordWriter.cpp @@ -0,0 +1,390 @@ +/* + * The contents of this file are subject to the Interbase 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.Inprise.com/IPL.html + * + * Software distributed under the License is distributed on an + * "AS IS" basis, 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 Inprise Corporation + * and its predecessors. Portions created by Inprise Corporation are + * Copyright (C) Inprise Corporation. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + * + * Adriano dos Santos Fernandes + */ + +#include "firebird.h" + +// burp.h and ini.h both define struct gfld with different layouts +#define gfld burp_gfld +#include "../burp/FastPathRecordWriter.h" +#include "../burp/RestoreMessageLayout.h" +#include "../burp/burp.h" +#undef gfld + +#include "../burp/burp_proto.h" +#include "../common/dsc_proto.h" +#include "../common/msg_encode.h" +#include "../jrd/EngineInterface.h" +#include "../jrd/Attachment.h" +#include "../jrd/jrd.h" +#include "../jrd/Relation.h" +#include "../jrd/met.h" +#include "../jrd/met_proto.h" +#include "../jrd/align.h" +#include "../jrd/blb.h" +#include "../jrd/BulkInsert.h" +#include "../jrd/evl_proto.h" +#include "../jrd/mov_proto.h" +#include "../jrd/obj.h" +#include "../jrd/req.h" +#include "../jrd/rlck_proto.h" +#include "../jrd/tdbb.h" +#include "../jrd/tra.h" +#include "../jrd/vio_proto.h" +#include "../jrd/val.h" + +using namespace Firebird; +using namespace Jrd; + +namespace Burp { + + +struct FastPathRecordWriter::Imp +{ + struct Field + { + dsc source; + dsc target; + RCRD_OFFSET sourceOffset; + RCRD_OFFSET nullOffset; + USHORT targetId; + bool array; + }; + + IAttachment* att = nullptr; + ITransaction* tra = nullptr; + const burp_rel* burpRelation = nullptr; + ::JAttachment* jAtt = nullptr; + Attachment* attachment = nullptr; + jrd_tra* transaction = nullptr; + jrd_rel* relation = nullptr; + const Format* format = nullptr; + AutoPtr record; + + HalfStaticArray fields; + RCRD_LENGTH messageLength = 0; + record_param rpb; + bool initialized = false; + + Imp() + : fields(*getDefaultMemoryPool()), + rpb() + {} + + void setupRecord(thread_db* tdbb) + { + rpb.rpb_relation = relation; + // VIO_record() allocates rpb_record when null and returns it. + record = VIO_record(tdbb, &rpb, format, tdbb->getDefaultPool()); + rpb.rpb_format_number = format->fmt_version; + } + + void writeRecord(thread_db* tdbb, const UCHAR* messageBuffer) + { + // Register the bulk insert before field conversion so blobs/arrays + // use the batched secondary-page path (bulk=true) instead of + // row-at-a-time DPM_store_blob. The object persists across records and + // message batches; it is finished at table end (finishBulk) and at + // commit. + const auto bulk = transaction->getBulkInsert(tdbb, relation, true); + if (!bulk) + { + fb_assert(false); + (Arg::Gds(isc_random) << + Arg::Str("FAST_PATH could not start bulk insert") << + Arg::Str(burpRelation->rel_name.toQuotedString())).raise(); + } + + rpb.rpb_number.setValue(BOF_NUMBER); + rpb.rpb_record->nullify(); + + for (const auto& field : fields) + { + SSHORT nullFlag; + memcpy(&nullFlag, messageBuffer + field.nullOffset, sizeof(nullFlag)); + + if (nullFlag) + continue; + + rpb.rpb_record->clearNull(field.targetId); + + dsc src = field.source; + src.dsc_address = const_cast(messageBuffer + field.sourceOffset); + + dsc dst = field.target; + dst.dsc_address = rpb.rpb_record->getData() + (IPTR) field.target.dsc_address; + + if (DTYPE_IS_BLOB(dst.dsc_dtype) || field.array) + blb::move(tdbb, &src, &dst, relation, rpb.rpb_record, field.targetId, true); + else + MOV_move(tdbb, &src, &dst); + } + + rpb.rpb_address = rpb.rpb_record->getData(); + rpb.rpb_length = format->fmt_length; + rpb.rpb_record->setTransactionNumber(transaction->tra_number); + + // Buffer the record into the transaction's bulk-insert page images. + // BulkInsert performs no validation, matching the FAST_PATH + // design of trusting trusted backups. REPL_store() is + // intentionally not called: this path only populates freshly created + // databases. + bulk->putRecord(tdbb, &rpb, transaction); + } +}; + + +[[noreturn]] static void raiseInvalidField(const burp_rel* relation, const burp_fld* field, + const char* reason) +{ + (Arg::Gds(isc_gbak_inv_column) << Arg::Str(field->fld_name) << + Arg::Str(relation->rel_name.toQuotedString()) << Arg::Str(reason)).raise(); +} + + +FastPathRecordWriter::FastPathRecordWriter() + : imp(FB_NEW_POOL(*getDefaultMemoryPool()) Imp()) +{ +} + +FastPathRecordWriter::~FastPathRecordWriter() +{ + clear(); +} + +void FastPathRecordWriter::init(IAttachment* att, ITransaction* tra, const burp_rel* relation, + const RestoreMessageLayout& layout) +{ + fb_assert(att && tra && relation && imp); + + imp->initialized = false; + imp->fields.clear(); + + // Drop the record of a previous init(): VIO_record() would otherwise reuse + // rpb_record and assigning it back to the owning AutoPtr would free it. + imp->record = nullptr; + imp->rpb = record_param(); + imp->messageLength = layout.getLength(); + imp->att = att; + imp->tra = tra; + imp->burpRelation = relation; + imp->jAtt = static_cast<::JAttachment*>(att); + imp->attachment = imp->jAtt->getHandle(); + fb_assert(imp->attachment); + + FbLocalStatus status; + EngineContextHolder tdbb(&status, imp->jAtt, FB_FUNCTION); + imp->transaction = imp->jAtt->getEngineTransaction(&status, tra); + + if (status->hasData()) + BURP_abort(&status); + + if (!imp->transaction) + Arg::Gds(isc_bad_trans_handle).raise(); + + tdbb->setTransaction(imp->transaction); + + QualifiedName qualifiedName(relation->rel_name); + // Resolve old unqualified backup names through the target attachment's + // default schema; explicitly qualified names are left unchanged. + imp->attachment->qualifyExistingName(tdbb, qualifiedName, {obj_relation}); + + imp->relation = MetadataCache::getVersioned(tdbb, qualifiedName, CacheFlag::AUTOCREATE); + + if (!imp->relation) + { + (Arg::Gds(isc_gbak_tbl_missing) << + Arg::Str(qualifiedName.toQuotedString())).raise(); + } + + if (relation->rel_type != rel_persistent || + (relation->rel_flags & (::REL_view | ::REL_external)) || + imp->relation->isSystem() || imp->relation->isView() || + imp->relation->isVirtual() || imp->relation->isTemporary() || + imp->relation->getExtFile()) + { + fb_assert(false); + (Arg::Gds(isc_random) << + Arg::Str("FAST_PATH selected an ineligible relation") << + Arg::Str(relation->rel_name.toQuotedString())).raise(); + } + + imp->format = imp->relation->currentFormat(tdbb); + + if (!imp->format) + { + fb_assert(false); + (Arg::Gds(isc_random) << + Arg::Str("FAST_PATH relation has no current format") << + Arg::Str(relation->rel_name.toQuotedString())).raise(); + } + + imp->setupRecord(tdbb); + + for (const auto& layoutField : layout) + { + const burp_fld* field = layoutField.metadata; + const dsc source = layoutField.descriptor; + + const auto fieldId = MET_lookup_field(tdbb, imp->relation, MetaName(field->fld_name)); + if (fieldId < 0 || fieldId >= imp->format->fmt_count) + raiseInvalidField(relation, field, "field is not present in the target table"); + + for (const auto& mapping : imp->fields) + { + if (mapping.targetId == fieldId) + raiseInvalidField(relation, field, "field is mapped more than once"); + } + + const dsc target = imp->format->fmt_desc[fieldId]; + const auto targetOffset = (IPTR) target.dsc_address; + + // Computed fields are excluded from the message, so a computed/unknown descriptor here + // means inconsistent backup/target metadata. + if (target.isUnknown() || (target.dsc_flags & DSC_computed) || targetOffset <= 0 || + static_cast(targetOffset) > imp->format->fmt_length || + target.dsc_length > imp->format->fmt_length - static_cast(targetOffset)) + { + raiseInvalidField(relation, field, "invalid target descriptor"); + } + + Imp::Field mapping; + mapping.source = source; + mapping.target = target; + mapping.sourceOffset = layoutField.offset; + mapping.nullOffset = layoutField.nullOffset; + mapping.targetId = static_cast(fieldId); + mapping.array = (field->fld_flags & FLD_array) != 0; + imp->fields.add(mapping); + } + + RLCK_reserve_relation(tdbb, imp->transaction, imp->relation->getPermanent(), true); + + // Note: the transaction's bulk insert is registered lazily by the first + // stored row and finished at table end (see finishBulk), so + // nothing is set up here. + imp->initialized = true; +} + +void FastPathRecordWriter::writeRecords(const UCHAR* messageBuffer, RCRD_LENGTH msgLength, + unsigned recordCount) +{ + fb_assert(imp && imp->initialized && messageBuffer); + + if (!recordCount) + return; + + // msgLength is the in-memory stride and may exceed the serialized message + // length to preserve descriptor alignment between packed records. + if (msgLength < imp->messageLength) + { + // Internal caller invariant: the stride is derived from the same layout. + fb_assert(false); + (Arg::Gds(isc_random) << + Arg::Str("FAST_PATH message stride is smaller than the layout") << + Arg::Str(imp->burpRelation->rel_name.toQuotedString())).raise(); + } + + // One engine attachment sync processes the whole batch. The direct path is + // fail-fast: BulkInsert has no rollback for blob/array side effects, so + // continuing after a failed record could leave partial row data. + FbLocalStatus status; + EngineContextHolder tdbb(&status, imp->jAtt, FB_FUNCTION); + tdbb->setTransaction(imp->transaction); + + for (unsigned record = 0; record < recordCount; ++record) + { + const UCHAR* const recordBuffer = messageBuffer + static_cast(record) * msgLength; + imp->writeRecord(tdbb, recordBuffer); + } +} + +void FastPathRecordWriter::releaseRequestlessArrays() +{ + if (!imp || !imp->initialized || !imp->transaction) + return; + + // Touching the transaction's array list requires the engine sync. + FbLocalStatus status; + EngineContextHolder tdbb(&status, imp->jAtt, FB_FUNCTION); + tdbb->setTransaction(imp->transaction); + + blb::releaseRequestlessArrays(imp->transaction); +} + +void FastPathRecordWriter::finishBulk() +{ + if (!imp || !imp->initialized || !imp->transaction) + return; + + FbLocalStatus status; + EngineContextHolder tdbb(&status, imp->jAtt, FB_FUNCTION); + tdbb->setTransaction(imp->transaction); + + // Flush buffered pages and unregister from the transaction. Called on + // table end and at commit (as a safety net). Not called at every + // message-batch boundary: the bulk object auto-flushes full + // extents internally and its memory stays bounded (one extent plus blob + // pages), while forcing a flush per batch would strand a partial-extent + // tail each time and bloat the restored database. The next stored row + // transparently registers a fresh bulk object when needed. + imp->transaction->finiBulkInsert(tdbb, true); +} + +void FastPathRecordWriter::clear() +{ + if (!imp) + return; + + // Touching the transaction's array list requires the engine sync, also in + // this (possibly destructor) path. The attachment may already be shut + // down; never let that escape from here. + if (imp->initialized && imp->transaction && imp->jAtt) + { + try + { + FbLocalStatus status; + EngineContextHolder tdbb(&status, imp->jAtt, FB_FUNCTION); + tdbb->setTransaction(imp->transaction); + + blb::releaseRequestlessArrays(imp->transaction); + } + catch (const Exception&) + { + } + } + + imp->att = nullptr; + imp->tra = nullptr; + imp->burpRelation = nullptr; + imp->jAtt = nullptr; + imp->attachment = nullptr; + imp->transaction = nullptr; + imp->relation = nullptr; + imp->format = nullptr; + imp->record = nullptr; + imp->fields.clear(); + imp->messageLength = 0; + imp->rpb = record_param(); + imp->initialized = false; +} + + +} // namespace Burp diff --git a/src/burp/main/FastPathBurpProviderStub.cpp b/src/burp/main/FastPathBurpProviderStub.cpp new file mode 100644 index 00000000000..11880803b7d --- /dev/null +++ b/src/burp/main/FastPathBurpProviderStub.cpp @@ -0,0 +1,60 @@ +/* + * 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) 2026 Adriano dos Santos Fernandes + * and all contributors signed below. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + */ + +#include "firebird.h" + +#include "../burp/FastPathBurpProvider.h" +#include "../common/utils_proto.h" + +using namespace Firebird; + +namespace Burp { + + +// Complete type for Firebird::AutoPtr. The real definition lives in +// burp/engine/FastPathBurpUtil.cpp, which is not linked into standalone gbak. +struct FastPathBurpProvider::EngineHolder +{ +}; + + +FastPathBurpProvider::FastPathBurpProvider(bool useFastPath) + : provider(dispatcher) +{ + if (useFastPath) + { + (Arg::Gds(isc_random) << + Arg::Str("FAST_PATH mode requires service manager")).raise(); + } +} + +FastPathBurpProvider::~FastPathBurpProvider() = default; + +IAttachment* FastPathBurpProvider::attachWorker(CheckStatusWrapper* status, const char* dbName, + unsigned dpbLen, const unsigned char* dpb, ICryptKeyCallback*) +{ + (Arg::Gds(isc_random) << + Arg::Str("FAST_PATH mode requires service manager")).raise(); +} + + +} // namespace Burp diff --git a/src/burp/main/FastPathRecordReaderStub.cpp b/src/burp/main/FastPathRecordReaderStub.cpp new file mode 100644 index 00000000000..18413201a50 --- /dev/null +++ b/src/burp/main/FastPathRecordReaderStub.cpp @@ -0,0 +1,71 @@ +/* + * 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) 2026 Adriano dos Santos Fernandes + * and all contributors signed below. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + */ + +#include "firebird.h" +#include "../burp/FastPathRecordReader.h" +#include "../burp/burp_proto.h" + +namespace Burp { + + +// Complete type for Firebird::AutoPtr. The real definition lives in +// burp/engine/FastPathRecordReader.cpp, which is not linked into standalone gbak. +struct FastPathRecordReader::Imp +{ +}; + +FastPathRecordReader::FastPathRecordReader() + : imp(nullptr) +{ +} + +FastPathRecordReader::~FastPathRecordReader() = default; + +void FastPathRecordReader::init(Firebird::IAttachment*, Firebird::ITransaction*, const burp_rel*, + const RestoreMessageLayout&, bool) +{ + BURP_error(430, true); +} + +void FastPathRecordReader::start(ULONG, ULONG) +{ + fb_assert(false); +} + +bool FastPathRecordReader::readRecord(UCHAR*, RCRD_LENGTH) +{ + fb_assert(false); + return false; +} + +unsigned FastPathRecordReader::readRecords(UCHAR*, RCRD_LENGTH, unsigned) +{ + fb_assert(false); + return 0; +} + +void FastPathRecordReader::clear() +{ +} + + +} // namespace Burp diff --git a/src/burp/main/FastPathRecordWriterStub.cpp b/src/burp/main/FastPathRecordWriterStub.cpp new file mode 100644 index 00000000000..99d6465a455 --- /dev/null +++ b/src/burp/main/FastPathRecordWriterStub.cpp @@ -0,0 +1,66 @@ +/* + * 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) 2026 Adriano dos Santos Fernandes + * and all contributors signed below. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + */ + +#include "firebird.h" +#include "../burp/FastPathRecordWriter.h" +#include "../burp/burp_proto.h" + +namespace Burp { + + +// Complete type for Firebird::AutoPtr. The real definition lives in +// burp/engine/FastPathRecordWriter.cpp, which is not linked into standalone gbak. +struct FastPathRecordWriter::Imp +{ +}; + + +FastPathRecordWriter::FastPathRecordWriter() = default; + +FastPathRecordWriter::~FastPathRecordWriter() = default; + +void FastPathRecordWriter::init(Firebird::IAttachment*, Firebird::ITransaction*, const burp_rel*, + const RestoreMessageLayout&) +{ + BURP_error(430, true); +} + +void FastPathRecordWriter::writeRecords(const UCHAR*, RCRD_LENGTH, unsigned) +{ + fb_assert(false); +} + +void FastPathRecordWriter::releaseRequestlessArrays() +{ +} + +void FastPathRecordWriter::finishBulk() +{ + fb_assert(false); +} + +void FastPathRecordWriter::clear() +{ +} + + +} // namespace Burp diff --git a/src/burp/restore.epp b/src/burp/restore.epp index 6b1e98e4bc7..ced03f6093d 100644 --- a/src/burp/restore.epp +++ b/src/burp/restore.epp @@ -49,6 +49,7 @@ #include "../burp/mvol_proto.h" #include "../burp/resto_proto.h" #include "../burp/BurpTasks.h" +#include "../burp/FastPathRecordWriter.h" #include "../common/gdsassert.h" #include "../jrd/constants.h" #include "../remote/protocol.h" @@ -66,6 +67,7 @@ #include "../common/ThreadStart.h" #include "../common/msg_encode.h" #include "../common/classes/BatchCompletionState.h" +#include "../burp/FastPathBurpProvider.h" using MsgFormat::SafeArg; using namespace Firebird; @@ -352,8 +354,26 @@ int RESTORE_restore (const TEXT* file_name, const TEXT* database_name) Firebird::IRequest* req_handle3 = nullptr; QualifiedMetaString indexName; - Firebird::DispatcherPtr provider; BurpGlobals* tdgbl = BurpGlobals::getSpecific(); + FastPathBurpProvider burpProvider(tdgbl->gbl_fast_path); + + IProvider* provider = burpProvider.get(); + + // The fast-path provider bypasses the y-valve, so the crypt callback held by + // the service (normally installed as y-valve TLS state by fillDpb) is not + // picked up automatically. Set it explicitly, unless the backup itself + // supplied the keys (KEYHOLDER). + if (tdgbl->gbl_fast_path && !tdgbl->gbl_sw_keyholder) + { + if (const auto cryptCb = tdgbl->uSvc->getCryptCallback()) + { + FbLocalStatus status; + provider->setDbCryptCallback(&status, cryptCb); + if (status->getState() & IStatus::STATE_ERRORS) + BURP_abort(&status); + } + } + tdgbl->gbl_sw_transportable = tdgbl->gbl_sw_compress = false; if (!restore(tdgbl, provider, file_name, database_name)) @@ -3500,11 +3520,25 @@ void get_data(BurpGlobals* tdgbl, burp_rel* relation, WriteRelationReq* req) if (batch) req->execBatch(tdgbl); } + + req->execFastPathBatch(); + + // Flush and destroy the fast-path writer while the transaction is + // still valid. Must run before the incremental commit below: the + // commit destroys the engine transaction held by the writer. + req->finishFastPathBulk(); } catch (const FbException& ex) { BURP_abort(ex.getStatus()); } + catch (const status_exception& ex) + { + // Engine errors raised by the FAST_PATH direct writer. + FbLocalStatus status; + ex.stuffException(&status); + BURP_abort(&status); + } if (tdgbl->gbl_sw_incremental) commit_relation_data(tdgbl, relation); @@ -8258,6 +8292,9 @@ bool get_relation(BurpGlobals* tdgbl, Coordinator* coord, RestoreRelationTask* t } } + relation->rel_system = (sys_flag != fb_sysflag_user); + relation->rel_type = (SSHORT) type; + // If this is a view and there is a global transaction then use it Firebird::ITransaction* local_trans; if (view_blr_null || !tdgbl->global_trans) @@ -14050,18 +14087,38 @@ void WriteRelationMeta::clear() { m_relation = nullptr; m_batchMode = false; + m_fastPathMode = false; m_batchOk = false; m_batchMeta = nullptr; m_sqlStatement.clear(); m_blr.clear(); + m_messageLayout.clear(); m_inMgsNum = 0; m_inMsgLen = 0; m_blobCount = 0; } -void WriteRelationMeta::setRelation(BurpGlobals* tdgbl, const burp_rel* relation) +void WriteRelationMeta::setRelation(BurpGlobals* tdgbl, burp_rel* relation) { m_relation = relation; + + if (tdgbl->gbl_fast_path && tdgbl->RESTORE_format >= 8 && + !relation->rel_system && + !(relation->rel_flags & (REL_view | REL_external)) && + relation->rel_type == rel_persistent) + { + m_fastPathMode = true; + m_batchMode = false; + prepareMessageLayout(tdgbl); + return; + } + + buildNonFastPathMode(tdgbl); +} + +void WriteRelationMeta::buildNonFastPathMode(BurpGlobals* tdgbl) +{ + m_fastPathMode = false; m_batchMode = true; m_batchOk = false; @@ -14079,7 +14136,7 @@ void WriteRelationMeta::setRelation(BurpGlobals* tdgbl, const burp_rel* relation bool sysDomFlag = false; burp_fld* field; - for (field = relation->rel_fields; field && !sysDomFlag; field = field->fld_next) + for (field = m_relation->rel_fields; field && !sysDomFlag; field = field->fld_next) { if (field->fld_flags & FLD_computed) continue; @@ -14347,6 +14404,16 @@ bool WriteRelationMeta::prepareBatch(BurpGlobals* tdgbl) return true; } +void WriteRelationMeta::prepareMessageLayout(BurpGlobals* tdgbl) +{ + m_batchMode = false; + m_inMgsNum = 0; + + m_messageLayout.build(m_relation, tdgbl->gbl_sw_fix_fss_data, + CSetId(tdgbl->gbl_sw_fix_fss_data_id)); + m_inMsgLen = m_messageLayout.getLength(); +} + void WriteRelationMeta::prepareRequest(BurpGlobals* tdgbl) { const bool use_bulk_insert = (tdgbl->runtimeODS >= DB_VERSION_DDL14); @@ -14610,6 +14677,7 @@ void WriteRelationReq::reset(WriteRelationMeta* meta) m_request = 0; m_recs = 0; m_batchRecs = 0; + m_fastPathBatchRecs = 0; m_resync = true; } } @@ -14620,6 +14688,7 @@ void WriteRelationReq::clear() m_meta = NULL; m_recs = 0; m_batchRecs = 0; + m_fastPathBatchRecs = 0; m_resync = true; if (m_batch) @@ -14633,10 +14702,76 @@ void WriteRelationReq::clear() release(); m_request = nullptr; } + + m_fastPathWriter = nullptr; + m_fastPathBatch.clear(); +} + +void WriteRelationReq::initFastPathWriter(IAttachment* att, ITransaction* tra, + const burp_rel* relation, const RestoreMessageLayout& layout) +{ + if (!m_fastPathWriter) + m_fastPathWriter = FB_NEW_POOL(*getDefaultMemoryPool()) FastPathRecordWriter(); + + m_fastPathWriter->init(att, tra, relation, layout); +} + +void WriteRelationReq::execFastPathBatch() +{ + if (!m_fastPathWriter || !m_fastPathBatchRecs) + return; + + const unsigned recordCount = m_fastPathBatchRecs; + const RCRD_LENGTH recordLength = m_inMsg.getCount(); + + // In-memory stride preserves descriptor alignment between packed records; + // the serialized backup format still uses recordLength. + RCRD_LENGTH stride = recordLength; + + if (m_meta) + { + const RCRD_LENGTH layoutStride = m_meta->getMessageLayout().getStride(); + if (layoutStride > stride) + stride = layoutStride; + } + + const UCHAR* const batchBuffer = m_fastPathBatch.begin(); + m_fastPathBatchRecs = 0; + + // The direct writer is fail-fast. Any conversion or engine error aborts + // the restore instead of continuing with a partially isolated row. + m_fastPathWriter->writeRecords(batchBuffer, stride, recordCount); + + // Do not finish the bulk insert here: the BulkInsert object auto-flushes + // full extents internally and stays alive across message batches. Forcing + // a flush per staging batch would strand a partial-extent tail each time + // (thousands of empty pages on large tables). Flushing happens at table end + // via finishFastPathBulk() and at commit. Arrays are released eagerly in blb::move; + // releaseRequestlessArrays() handles any remaining request-less arrays. + m_fastPathWriter->releaseRequestlessArrays(); + + m_fastPathBatch.clear(); +} + +void WriteRelationReq::finishFastPathBulk() +{ + if (m_fastPathWriter) + { + m_fastPathWriter->finishBulk(); + + // Destroy the writer after flushing: it caches the raw engine + // transaction, which gets replaced by commit_relation_data() in + // incremental restore. initFastPathWriter() creates a fresh writer for + // every table. + m_fastPathWriter = nullptr; + } } void WriteRelationReq::compile(BurpGlobals* tdgbl, IAttachment* att) { + if (m_meta->m_fastPathMode) + return; + if (m_meta->m_batchMode) m_batch = m_meta->createBatch(tdgbl, att); @@ -14657,6 +14792,40 @@ void WriteRelationReq::send(BurpGlobals* tdgbl, ITransaction* tran, bool lastRec { m_recs++; + if (m_meta->m_fastPathMode) + { + fb_assert(m_fastPathWriter); + + const RCRD_LENGTH recordLength = m_inMsg.getCount(); + RCRD_LENGTH stride = recordLength; + const RCRD_LENGTH layoutStride = m_meta->getMessageLayout().getStride(); + if (layoutStride > stride) + stride = layoutStride; + + if (m_fastPathBatchRecs && + (m_fastPathBatchRecs >= FAST_PATH_RESTORE_BATCH_MAX_RECORDS || + m_fastPathBatch.getCount() + stride > FAST_PATH_RESTORE_BATCH_MAX_BYTES)) + { + execFastPathBatch(); + } + + const FB_SIZE_T base = m_fastPathBatch.getCount(); + m_fastPathBatch.add(m_inMsg.begin(), recordLength); + // Pad to the aligned stride so the next record keeps field alignment. + // Array::grow zero-fills the new tail. + if (stride > recordLength) + m_fastPathBatch.grow(base + stride); + ++m_fastPathBatchRecs; + + if (lastRec || m_fastPathBatchRecs >= FAST_PATH_RESTORE_BATCH_MAX_RECORDS || + m_fastPathBatch.getCount() >= FAST_PATH_RESTORE_BATCH_MAX_BYTES) + { + execFastPathBatch(); + } + + return; + } + if (m_batch) { m_batch->add(&tdgbl->throwStatus, 1, m_batchMsg.begin()); @@ -14707,6 +14876,9 @@ void WriteRelationReq::execBatch(BurpGlobals * tdgbl) void WriteRelationReq::release() { + // Release fast-path record while the engine attachment is still valid. + m_fastPathWriter = nullptr; + if (m_batch) { m_batch->release(); @@ -15129,6 +15301,11 @@ bool RestoreRelationTask::tableWriter(BurpGlobals* tdgbl, Item& item) { item.m_request.reset(&m_metadata); + if (m_metadata.m_fastPathMode) + { + item.m_request.initFastPathWriter(tdgbl->db_handle, tdgbl->tr_handle, + item.m_relation, m_metadata.getMessageLayout()); + } get_data(tdgbl, item.m_relation, &item.m_request); item.getRestoreTask()->releaseBuffer(item); diff --git a/src/common/TimeZones.h b/src/common/TimeZones.h index e0002527c27..cdb60110a99 100644 --- a/src/common/TimeZones.h +++ b/src/common/TimeZones.h @@ -1,6 +1,6 @@ // The content of this file is generated with help of update-ids utility Do not edit. -static const char* BUILTIN_TIME_ZONE_VERSION = "2026c"; +static const char* BUILTIN_TIME_ZONE_VERSION = "2026d"; // Do not change order of items in this array! The index corresponds to a TimeZone ID, which must be fixed! static const char* BUILTIN_TIME_ZONE_LIST[] = { diff --git a/src/common/classes/alloc.h b/src/common/classes/alloc.h index ec82044d027..87a16731635 100644 --- a/src/common/classes/alloc.h +++ b/src/common/classes/alloc.h @@ -481,7 +481,19 @@ namespace Firebird using void_pointer = void* ; using const_void_pointer = const void*; using difference_type = std::ptrdiff_t; - using is_always_equal = std::true_type; + + // We are using stateful allocator, so they are not always equal by definition. + using is_always_equal = std::false_type; + + // Do not propagate allocator on any type of assignment or swap (but remember, swap on two objects with + // different allocators is UB if swap propagation is false, so be careful with it), because it can lead + // to accidental misuse or undesired behavior, e.g.: + // `std::string str1(default_pool, std::move(std::string(req_pool, "some string")));` + // Where `str1` will borrow a pointer from `req_pool`, but `str1` can live longer then `req_pool`, so + // when `req_pool` is gone, we will have a dead pointer. + using propagate_on_container_copy_assignment = std::false_type; + using propagate_on_container_move_assignment = std::false_type; + using propagate_on_container_swap = std::false_type; template struct rebind diff --git a/src/common/classes/array.h b/src/common/classes/array.h index 897474fbb27..c6ac9a268fe 100644 --- a/src/common/classes/array.h +++ b/src/common/classes/array.h @@ -169,7 +169,6 @@ class Array : public Storage count = 0; } -protected: const T& getElement(size_type index) const noexcept { fb_assert(index < count); @@ -182,6 +181,7 @@ class Array : public Storage return data[index]; } +protected: void freeData() noexcept { // CVC: Warning, after this call, "data" is an invalid pointer, be sure to reassign it diff --git a/src/dsql/BoolNodes.h b/src/dsql/BoolNodes.h index bdf0301f654..35c76088c66 100644 --- a/src/dsql/BoolNodes.h +++ b/src/dsql/BoolNodes.h @@ -39,6 +39,12 @@ class BinaryBoolNode final : public TypedNodesetSecDbContext(att, tra); } - // run all statements under savepoint control - string savePoint; - savePoint.printf("ExecInSecurityDb%d", secDbContext->savePoint++); - secDbContext->att->execute(&statusWrapper, secDbContext->tra, 0, ("SAVEPOINT " + savePoint).c_str(), - SQL_DIALECT_V6, NULL, NULL, NULL, NULL); - check(&statusWrapper); - try { runInSecurityDb(secDbContext); - secDbContext->att->execute(&statusWrapper, secDbContext->tra, 0, - ("RELEASE SAVEPOINT " + savePoint).c_str(), - SQL_DIALECT_V6, NULL, NULL, NULL, NULL); - savePoint.erase(); - check(&statusWrapper); + secDbContext->releaseSavePoint(); } catch (const Exception&) { - if (savePoint.hasData()) - { - LocalStatus tmp; - CheckStatusWrapper tmpCheckStatusWrapper(&tmp); - secDbContext->att->execute(&tmpCheckStatusWrapper, secDbContext->tra, 0, - ("ROLLBACK TO SAVEPOINT " + savePoint).c_str(), - SQL_DIALECT_V6, NULL, NULL, NULL, NULL); - } + secDbContext->rollbackSavePoint(); throw; } @@ -13152,7 +13134,7 @@ void StoreIndexNode::step2(thread_db* tdbb, jrd_tra* transaction) auto id = expressionIndex ? createExpression(tdbb, transaction) : create(tdbb, transaction); if (id < tdbb->getDatabase()->dbb_max_idx) - indexRelation->newIndexVersion(tdbb, id, CacheFlag::MINISCAN); + indexRelation->newIndexVersion(tdbb, id); } @@ -13179,8 +13161,11 @@ MetaId StoreIndexNode::create(thread_db* tdbb, jrd_tra* transaction) index_desc idx; idx.idx_count = 0; + idx.idx_code = nullptr; int key_count = 0; + Cleanup idxCode([&]() {if (idx.idx_code) IndexCode::release(tdbb, idx.idx_code);}); + SET_TDBB(tdbb); Attachment* attachment = tdbb->getAttachment(); Database* dbb = tdbb->getDatabase(); @@ -13253,9 +13238,6 @@ MetaId StoreIndexNode::create(thread_db* tdbb, jrd_tra* transaction) END_FOR } - idx.idx_condition_node = nullptr; - idx.idx_condition_statement = nullptr; - if (!IDX.RDB$CONDITION_BLR.NULL) { // Allocate a new pool to contain the expression tree @@ -13271,8 +13253,9 @@ MetaId StoreIndexNode::create(thread_db* tdbb, jrd_tra* transaction) nullptr, &csb, indexName, obj_index_condition, 0, transaction); - idx.idx_condition_statement = Statement::makeBoolExpression(tdbb, - idx.idx_condition_node, csb, false); + idx.idx_code = FB_NEW_POOL(*dbb->dbb_permanent) IndexCode; + idx.idx_code->condition_statement = Statement::makeBoolExpression(tdbb, + idx.idx_code->condition, csb, false); idx.idx_flags |= idx_condition; } @@ -13449,6 +13432,7 @@ MetaId StoreIndexNode::create(thread_db* tdbb, jrd_tra* transaction) fb_assert(idxId <= dbb->dbb_max_idx); idx.idx_id = idxId; SelectivityList selectivity(*tdbb->getDefaultPool()); + IDX_create_index(tdbb, concurrently ? IdxCreate::Concurrently : IdxCreate::ForRollback, relation, &idx, indexName, &idxId, transaction, selectivity); @@ -13463,9 +13447,6 @@ MetaId StoreIndexNode::create(thread_db* tdbb, jrd_tra* transaction) else fb_assert(idxId == dbb->dbb_max_idx); - if (idx.idx_condition_statement) - idx.idx_condition_statement->release(tdbb); - if (partnerRelation) { indexRelation->checkPartners(tdbb); @@ -13508,6 +13489,9 @@ MetaId StoreIndexNode::createExpression(thread_db* tdbb, jrd_tra* transaction) CompilerScratch* csb = nullptr; const auto dbb = tdbb->getDatabase(); + idx.idx_code = FB_NEW_POOL(*dbb->dbb_permanent) IndexCode; + Cleanup idxCode([&]() {IndexCode::release(tdbb, idx.idx_code);}); + const auto attachment = tdbb->getAttachment(); MetaId idxId = dbb->dbb_max_idx; bool active = false; @@ -13561,8 +13545,8 @@ MetaId StoreIndexNode::createExpression(thread_db* tdbb, jrd_tra* transaction) nullptr, &csb, indexName, obj_index_expression, 0, transaction); - idx.idx_expression_statement = Statement::makeValueExpression(tdbb, - idx.idx_expression_node, idx.idx_expression_desc, csb, false); + idx.idx_code->expression_statement = Statement::makeValueExpression(tdbb, + idx.idx_code->expression, idx.idx_code->expression_desc, csb, false); // fake a description of the index @@ -13570,8 +13554,8 @@ MetaId StoreIndexNode::createExpression(thread_db* tdbb, jrd_tra* transaction) idx.idx_flags |= idx_expression; idx.idx_rpt[0].idx_itype = DFW_assign_index_type(tdbb, indexName, - idx.idx_expression_desc.dsc_dtype, - idx.idx_expression_desc.getTextType()); + idx.idx_code->expression_desc.dsc_dtype, + idx.idx_code->expression_desc.getTextType()); idx.idx_rpt[0].idx_selectivity = 0; } catch (const Exception&) @@ -13594,8 +13578,8 @@ MetaId StoreIndexNode::createExpression(thread_db* tdbb, jrd_tra* transaction) nullptr, &csb, indexName, obj_index_condition, 0, transaction); - idx.idx_condition_statement = Statement::makeBoolExpression(tdbb, - idx.idx_condition_node, csb, false); + idx.idx_code->condition_statement = Statement::makeBoolExpression(tdbb, + idx.idx_code->condition, csb, false); idx.idx_flags |= idx_condition; } @@ -13638,6 +13622,7 @@ MetaId StoreIndexNode::createExpression(thread_db* tdbb, jrd_tra* transaction) { fb_assert(idxId <= dbb->dbb_max_idx); idx.idx_id = idxId; + IDX_create_index(tdbb, concurrently ? IdxCreate::Concurrently : IdxCreate::ForRollback, relation, &idx, indexName, &idxId, transaction, selectivity); @@ -13647,9 +13632,7 @@ MetaId StoreIndexNode::createExpression(thread_db* tdbb, jrd_tra* transaction) catch (const Exception&) { // Get rid of the expression/condition statements - idx.idx_expression_statement->release(tdbb); - if (idx.idx_condition_statement) - idx.idx_condition_statement->release(tdbb); + IndexCode::release(tdbb, idx.idx_code); throw; } @@ -13664,9 +13647,7 @@ MetaId StoreIndexNode::createExpression(thread_db* tdbb, jrd_tra* transaction) fb_assert(idxId == dbb->dbb_max_idx); // Get rid of the expression/condition statements - idx.idx_expression_statement->release(tdbb); - if (idx.idx_condition_statement) - idx.idx_condition_statement->release(tdbb); + IndexCode::release(tdbb, idx.idx_code); return idxId; } @@ -14514,7 +14495,7 @@ void AlterIndexNode::step2(thread_db* tdbb, jrd_tra* transaction) else IDX_mark_index(tdbb, indexRelation, idxId.value()); - indexRelation->newIndexVersion(tdbb, idxId.value(), CacheFlag::MINISCAN); + indexRelation->newIndexVersion(tdbb, idxId.value()); } // Alter an index on a Local Temporary Table (ACTIVE/INACTIVE). @@ -15689,6 +15670,8 @@ void MappingNode::runInSecurityDb(SecDbContext* secDbContext) { try { + secDbContext->setSavePoint(); + // try direct access to rdb$auth_mapping table in secure db LocalStatus s2; CheckStatusWrapper statusWrapper2(&s2); @@ -16369,6 +16352,8 @@ void GrantRevokeNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, void GrantRevokeNode::runInSecurityDb(SecDbContext* secDbContext) { + secDbContext->setSavePoint(); + for (unsigned n = 0; n < createDbJobs.getCount(); ++n) { CreateDbJob& j = createDbJobs[n]; diff --git a/src/dsql/DsqlRequests.cpp b/src/dsql/DsqlRequests.cpp index 54e13e8064a..231b9b25106 100644 --- a/src/dsql/DsqlRequests.cpp +++ b/src/dsql/DsqlRequests.cpp @@ -553,6 +553,8 @@ void DsqlDmlRequest::doExecute(thread_db* tdbb, jrd_tra** traHandle, firstRowFetched = false; const dsql_msg* message = dsqlStatement->getSendMsg(); + req_transaction->processUpdates(tdbb); + if (!message) { JRD_start(tdbb, request, req_transaction); @@ -977,6 +979,15 @@ void DsqlDdlRequest::execute(thread_db* tdbb, jrd_tra** traHandle, //// Doing it in DFW_perform_work to avoid problems with DDL+DML in the same transaction. /// req_dbb->dbb_attachment->att_dsql_instance->dbb_statement_cache->purgeAllAttachments(tdbb); + // Check for presence of user savepoints + for (Savepoint::Iterator iter(req_transaction->tra_save_point); *iter; ++iter) + { + Savepoint* const current = *iter; + + if (!current->isSystem()) + (Arg::Gds(isc_user_savepoint) << current->getName().c_str()).raise(); + } + node->executeDdl(tdbb, internalScratch, req_transaction); const bool isInternalRequest = diff --git a/src/dsql/Parser.cpp b/src/dsql/Parser.cpp index 6a2c9db5fa4..1e6da087deb 100644 --- a/src/dsql/Parser.cpp +++ b/src/dsql/Parser.cpp @@ -292,7 +292,7 @@ int Parser::yylex() bool Parser::yylexSkipSpaces() { - UCHAR tok_class; + USHORT tok_class; SSHORT c; // Find end of white space and skip comments @@ -399,7 +399,7 @@ int Parser::yylexAux() MemoryPool& pool = *tdbb->getDefaultPool(); SSHORT c = lex.ptr[-1]; - const UCHAR tok_class = classes(c); + const USHORT tok_class = classes(c); char string[MAX_TOKEN_LEN]; // Depending on tok_class of token, parse token @@ -411,6 +411,10 @@ int Parser::yylexAux() if (lex.ptr >= lex.end) return -1; + // restriction for underscores before numeric literals + if ((classes(*lex.ptr) & CHR_DIGIT) || *lex.ptr == '.') + exceptionNumericLiterals(Firebird::string(lex.last_token, lex.ptr - lex.last_token + 1)); + if (classes(*lex.ptr) & (CHR_IDENT | CHR_QUOTE)) return TOK_INTRODUCER; @@ -782,416 +786,385 @@ int Parser::yylexAux() lex.ptr = lex.last_token + 1; } - // Hexadecimal numeric constants - 0xBBBBBB - // - // where the '0' and the 'X' (or 'x') are literal, followed - // by a set of nibbles, using 0-9, a-f, or A-F. Odd numbers - // of nibbles assume a leading '0'. The result is converted - // to an integer, and the result returned to the caller. The - // token is identified as a NUMBER32BIT if it's a 32-bit or less - // value, or a NUMBER64INT if it requires a 64-bit number. - if (c == '0' && lex.ptr + 1 < lex.end && (*lex.ptr == 'x' || *lex.ptr == 'X') && - (classes(lex.ptr[1]) & CHR_HEX)) - { - bool hexerror = false; - - // Remember where we start from, to rescan later. - // Also we'll need to know the length of the buffer. - - ++lex.ptr; // Skip the 'X' and point to the first digit - const char* hexstring = lex.ptr; - int charlen = 0; + // Non-decimal integer literals (SQL:2023 T661) + // Underscores in numeric literal support (SQL:2023 T662) + // See README.decimal_and_non_decimal_literals - // Time to scan the string. Make sure the characters are legal, - // and find out how long the hex digit string is. + if (c == '0' && lex.ptr + 1 < lex.end) + { + auto base = 0; + SSHORT currExpcChar = 0; - while (lex.ptr < lex.end) + if (*lex.ptr == 'x' || *lex.ptr == 'X') { - c = *lex.ptr; - - if (!(classes(c) & CHR_HEX)) // End of digit string - break; - - ++charlen; // Okay, just count 'em - ++lex.ptr; // and advance... - - if (charlen > 32) // Too many digits... - { - hexerror = true; - break; - } + base = 4; // 2^4 0b1111 + currExpcChar = CHR_HEX; } - - // we have a valid hex token. Now give it back, either as - // an NUMBER32BIT or NUMBER64BIT. - if (!hexerror) + else if (*lex.ptr == 'o' || *lex.ptr == 'O') { - if (charlen > 16) - { - // we deal with int128 - fb_assert(charlen <= 32); // charlen is always <= 32, see 10-15 lines upper - - Firebird::string sbuff(hexstring, charlen); - sbuff.insert(0, "0X"); + base = 3; // 2^3 0b111 + currExpcChar = CHR_OCT; + } + else if (*lex.ptr == 'b' || *lex.ptr == 'B') + { + base = 1; // 2^1 0b1 + currExpcChar = CHR_BIN; + } - yylval.lim64ptr = newLim64String(sbuff, 0); + if (base) + { + const auto decimalConversion = 10; + auto isLastIntroducer = false; + Int128 value128; + value128.set(0.0); - return TOK_NUM128; - } + const CInt128 MAX_VALUE(MAX_Int128 >> base); - // if charlen > 8 (something like FFFF FFFF 0, w/o the spaces) - // then we have to return a NUMBER64BIT. We'll make a string - // node here, and let make.cpp worry about converting the - // string to a number and building the node later. - else if (charlen > 8) + // Skip the 'X' or 'O' or 'B' and point to the first digit + for (++lex.ptr; lex.ptr < lex.end; lex.ptr++) { - char cbuff[32]; - fb_assert(charlen <= 16); // charlen is always <= 16, see 10-15 lines upper - cbuff[0] = 'X'; - fb_utils::copy_terminate(&cbuff[1], hexstring, charlen + 1); - - char* p = &cbuff[1]; - UCHAR byte = 0; - bool nibble = strlen(p) & 1; - - yylval.scaledNumber.number = 0; - yylval.scaledNumber.scale = 0; - yylval.scaledNumber.hex = true; + c = *lex.ptr; - while (*p) + if ((classes(c) & CHR_INTRODUCER)) { - if ((*p >= 'a') && (*p <= 'f')) - *p = UPPER(*p); - - // Now convert the character to a nibble - SSHORT c; - - if (*p >= 'A') - c = (*p - 'A') + 10; - else - c = (*p - '0'); - - if (nibble) - { - byte = (byte << 4) + (UCHAR) c; - nibble = false; - yylval.scaledNumber.number = (yylval.scaledNumber.number << 8) + byte; - } - else + if (isLastIntroducer) { - byte = c; - nibble = true; + exceptionNumericLiterals( + Firebird::string(lex.last_token, lex.ptr - lex.last_token + 1)); } - ++p; + isLastIntroducer = true; } + else if ((classes(c) & currExpcChar)) + { + // check overflow + if (value128 > MAX_VALUE) + exceptionNumericLiterals(Firebird::string("Overflow of the number")); - // The return value can be a negative number. - return TOK_NUMBER64BIT; - } - else - { - // we have an integer value. we'll return NUMBER32BIT. - // but we have to make a number value to be compatible - // with existing code. - - // See if the string length is odd. If so, - // we'll assume a leading zero. Then figure out the length - // of the actual resulting hex string. Allocate a second - // temporary buffer for it. - - bool nibble = (charlen & 1); // IS_ODD(temp.length) - - // Re-scan over the hex string we got earlier, converting - // adjacent bytes into nibble values. Every other nibble, - // write the saved byte to the temp space. At the end of - // this, the temp.space area will contain the binary - // representation of the hex constant. + auto ch = UPPER(c); + if (ch >= 'A') + ch = (ch - 'A') + decimalConversion; + else + ch = (ch - '0'); - UCHAR byte = 0; - SINT64 value = 0; + value128 *= 1 << base; + value128 += ch; - for (int i = 0; i < charlen; i++) + isLastIntroducer = false; + } + else if ((classes(c) & CHR_IDENT) && !(classes(c) & CHR_BRACE)) { - c = UPPER(hexstring[i]); - - // Now convert the character to a nibble + exceptionNumericLiterals( + Firebird::string(lex.last_token, lex.ptr - lex.last_token + 1)); + } + else // We have reached the separator + break; + } - if (c >= 'A') - c = (c - 'A') + 10; - else - c = (c - '0'); + // Error 0x + const auto minimalLength = 3U; + if ((lex.ptr - lex.last_token) < minimalLength) + { + exceptionNumericLiterals( + Firebird::string(lex.last_token, lex.ptr - lex.last_token)); + } - if (nibble) - { - byte = (byte << 4) + (UCHAR) c; - nibble = false; - value = (value << 8) + byte; - } - else - { - byte = c; - nibble = true; - } - } + // Error of having '_' at the end + if (isLastIntroducer) + { + exceptionNumericLiterals( + Firebird::string(lex.last_token, lex.ptr - lex.last_token)); + } - yylval.int32Val = (SLONG) value; - return TOK_NUMBER32BIT; - } // integer value - } // if (!hexerror)... + Int128 tmp; + tmp.set(MAX_SINT64, 0); + if (value128 > tmp) + { + Firebird::string strValue; + value128.toString(0, strValue); + yylval.lim64ptr = newLim64String(strValue, 0); - // If we got here, there was a parsing error. Set the - // position back to where it was before we messed with - // it. Then fall through to the next thing we might parse. + // Special case - exactly (MAX_SINT64 + 1), see decimal literals below + tmp += 1U; + return (value128 == tmp) ? TOK_LIMIT64_INT : TOK_NUM128; + } - c = *lex.last_token; - lex.ptr = lex.last_token + 1; - } // headecimal numeric constants + tmp.set(MAX_SLONG); + if (value128 > tmp) + { + yylval.scaledNumber.number = value128.toInt64(0); + yylval.scaledNumber.scale = 0; + yylval.scaledNumber.hex = false; + return TOK_NUMBER64BIT; + } + else + { + yylval.int32Val = (SLONG)value128.toInteger(0); + return TOK_NUMBER32BIT; + } + } + } if ((tok_class & CHR_DIGIT) || ((c == '.') && (lex.ptr < lex.end) && (classes(*lex.ptr) & CHR_DIGIT))) { - // The following variables are used to recognize kinds of numbers. - - bool have_error = false; // syntax error or value too large - bool have_digit = false; // we've seen a digit - bool have_decimal = false; // we've seen a '.' - bool have_exp = false; // digit ... [eE] - bool have_exp_sign = false; // digit ... [eE] {+-] - bool have_exp_digit = false; // digit ... [eE] ... digit - bool have_overflow = false; // value of digits > MAX_SINT64 - bool positive_overflow = false; // number is exactly (MAX_SINT64 + 1) - bool have_128_over = false; // value of digits > MAX_INT128 - FB_UINT64 number = 0; - Int128 num128; - int expVal = 0; - constexpr FB_UINT64 limit_by_10 = MAX_SINT64 / 10; + Firebird::string pureString; + auto isLastIntroducer = false; int scale = 0; - int expSign = 1; + auto exponentValue = 0; + auto isOverExponent64b = false; + auto isOverMantisa64b = false; + auto isOverMantisa128b = false; + auto signExponent = 0; + auto hasExponentDigit = false; + + Int128 mantisaValue; + mantisaValue.set(0.0); + + const auto decimalConversion = 10; + static const CInt128 MAX_MANTISA_128(MAX_Int128 / decimalConversion); + static const CInt128 MAX_MANTISA_64(MAX_SINT64 / decimalConversion); + + enum + { + state_mantisa = 0, + state_precision, + state_exponent, + } state = state_mantisa; for (--lex.ptr; lex.ptr < lex.end; lex.ptr++) { c = *lex.ptr; - if (have_exp_digit && (! (classes(c) & CHR_DIGIT))) - // First non-digit after exponent and digit terminates the token. - break; - if (have_exp_sign && (! (classes(c) & CHR_DIGIT))) + if (classes(c) & CHR_INTRODUCER) { - // only digits can be accepted after "1E-" - have_error = true; - break; - } - - if (have_exp) - { - // We've seen e or E, but nothing beyond that. - if ( ('-' == c) || ('+' == c) ) + if (isLastIntroducer) { - have_exp_sign = true; - if ('-' == c) - expSign = -1; + exceptionNumericLiterals( + Firebird::string(lex.last_token, lex.ptr - lex.last_token + 1)); } - else if ( classes(c) & CHR_DIGIT ) - { - // We have a digit: we haven't seen a sign yet, but it's too late now. - have_exp_digit = have_exp_sign = true; - if (!have_overflow) - { - expVal = expVal * 10 + (c - '0'); - if (expVal > DBL_MAX_10_EXP) - have_overflow = true; - } - } - else - { - // end of the token - have_error = true; - break; - } - } - else if ('.' == c) - { - if (!have_decimal) - have_decimal = true; - else + + const char lastSymbol = *(lex.ptr - 1); + if ((lastSymbol == '.') || (UPPER(lastSymbol) == 'E') || (lastSymbol == '-') || + (lastSymbol == '+')) { - have_error = true; - break; + exceptionNumericLiterals( + Firebird::string(lex.last_token, lex.ptr - lex.last_token + 1)); } + + isLastIntroducer = true; + continue; } - else if (classes(c) & CHR_DIGIT) + if (classes(c) & CHR_DIGIT) { - // Before computing the next value, make sure there will be no overflow. + pureString += static_cast(c); + auto ch = (c - '0'); - if (!have_overflow) + if (state == state_exponent) { - have_digit = true; + hasExponentDigit = true; + + if (signExponent == 0) + signExponent = 1; + + exponentValue *= decimalConversion; - if (number >= limit_by_10) + if (signExponent == 1) + exponentValue += ch; + else + exponentValue -= ch; + + if (!isOverExponent64b) { - // possibility of an overflow - if ((number > limit_by_10) || (c >= '8')) - { - have_overflow = true; - fb_assert(number <= MAX_SINT64); - num128.set((SINT64)number, 0); - if ((number == limit_by_10) && (c == '8')) - positive_overflow = true; - } + if (exponentValue > DBL_MAX_10_EXP || exponentValue < -DBL_MAX_10_EXP) + isOverExponent64b = true; } + else if (exponentValue > DECQUAD_Emax || exponentValue < DECQUAD_Emin) + exceptionNumericLiterals(Firebird::string("Overflow of the exponent")); } else { - positive_overflow = false; - if (!have_128_over) + if (!isOverMantisa64b) { - static const CInt128 MAX_BY10(MAX_Int128 / 10); - if ((num128 >= MAX_BY10) && ((num128 > MAX_BY10) || (c >= '8'))) - have_128_over = true; + if (mantisaValue >= MAX_MANTISA_64 && + ((mantisaValue > MAX_MANTISA_64) || (c >= '8'))) + { + isOverMantisa64b = true; + } + } + else if (!isOverMantisa128b) + { + if ((mantisaValue >= MAX_MANTISA_128) && + ((mantisaValue > MAX_MANTISA_128) || (c >= '8'))) + { + isOverMantisa128b = true; + isOverExponent64b = true; + } } - } - if (!have_overflow) - number = number * 10 + (c - '0'); - else if (!have_128_over) - { - num128 *= 10; - num128 += (c - '0'); - } + if (!isOverMantisa64b || !isOverMantisa128b) + { + mantisaValue *= decimalConversion; + mantisaValue += ch; + } - if (have_decimal) - --scale; + if (state == state_precision) + --scale; + } } - else if ( (('E' == c) || ('e' == c)) && have_digit ) - have_exp = true; - else - // Unexpected character: this is the end of the number. - break; - } + else if (c == '.') + { + if (isLastIntroducer) + { + exceptionNumericLiterals( + Firebird::string(lex.last_token, lex.ptr - lex.last_token + 1)); + } - // We're done scanning the characters: now return the right kind - // of number token, if any fits the bill. + pureString += static_cast(c); - if (!have_error) - { - fb_assert(have_digit); + if (state == state_mantisa) + state = state_precision; + else + { + exceptionNumericLiterals( + Firebird::string(lex.last_token, lex.ptr - lex.last_token + 1)); + } + } + else if (UPPER(c) == 'E') + { + if (isLastIntroducer) + { + exceptionNumericLiterals( + Firebird::string(lex.last_token, lex.ptr - lex.last_token + 1)); + } - if (positive_overflow) - have_overflow = false; + pureString += static_cast(c); - if (scale < MIN_SCHAR || scale > MAX_SCHAR) + if (state != state_exponent) + state = state_exponent; + else + { + exceptionNumericLiterals( + Firebird::string(lex.last_token, lex.ptr - lex.last_token + 1)); + } + } + else if ((classes(c) & CHR_IDENT) && !(classes(c) & CHR_BRACE)) { - have_overflow = true; - positive_overflow = false; - have_128_over = true; + exceptionNumericLiterals( + Firebird::string(lex.last_token, lex.ptr - lex.last_token + 1)); } - - // check for a more complex overflow case - if ((!have_overflow) && (expSign > 0) && (expVal > -scale)) + else // We have reached the separator { - expVal += scale; - double maxNum = DBL_MAX / pow(10.0, expVal); - if (double(number) > maxNum) + if ((c == '-') || (c == '+')) { - have_overflow = true; - positive_overflow = false; - have_128_over = true; + if (state == state_exponent && signExponent == 0) + { + pureString += static_cast(c); + + if (c == '-') + signExponent = -1; + else + signExponent = 1; + continue; + } } + break; } - // Special case - on the boarder of positive number - if (positive_overflow) - { - yylval.lim64ptr = newLim64String( - Firebird::string(lex.last_token, lex.ptr - lex.last_token), scale); - lex.last_token_bk = lex.last_token; - lex.line_start_bk = lex.line_start; - lex.lines_bk = lex.lines; + isLastIntroducer = false; + } + // We have reached the separator or the end of the line - return scale ? TOK_LIMIT64_NUMBER : TOK_LIMIT64_INT; - } + if (isLastIntroducer) + exceptionNumericLiterals(Firebird::string(lex.last_token, lex.ptr - lex.last_token)); - // Should we use floating point type? - if (have_exp_digit || have_128_over) - { - yylval.stringPtr = newString( - Firebird::string(lex.last_token, lex.ptr - lex.last_token)); - lex.last_token_bk = lex.last_token; - lex.line_start_bk = lex.line_start; - lex.lines_bk = lex.lines; - - return have_overflow ? TOK_DECIMAL_NUMBER : TOK_FLOAT_NUMBER; - } + // The exponent must contain at least one digit: 1e, 1e+, 1e- are invalid + if (state == state_exponent && !hasExponentDigit) + exceptionNumericLiterals(Firebird::string(lex.last_token, lex.ptr - lex.last_token)); - // May be 128-bit integer? - if (have_overflow) - { - yylval.lim64ptr = newLim64String( - Firebird::string(lex.last_token, lex.ptr - lex.last_token), scale); - lex.last_token_bk = lex.last_token; - lex.line_start_bk = lex.line_start; - lex.lines_bk = lex.lines; + // Too many digits after the period for an exact numeric - use DECFLOAT + if (scale < MIN_SCHAR) + { + isOverMantisa128b = true; + isOverExponent64b = true; + } - return TOK_NUM128; - } + lex.last_token_bk = lex.last_token; + lex.line_start_bk = lex.line_start; + lex.lines_bk = lex.lines; - if (!have_exp) + // Any with an exponent "E" or a very big number + if (state == state_exponent || isOverMantisa128b) + { + // Check for a more complex overflow case + if ((!isOverExponent64b) && (signExponent == 1) && (exponentValue > (-scale))) { - // We should return some kind (scaled-) integer type - // except perhaps in dialect 1. + const auto degreeBase = 10.0; + exponentValue += scale; + double check_num = DBL_MAX / pow(degreeBase, exponentValue); + if (mantisaValue.toDouble() > check_num) + isOverExponent64b = true; + } - if (!have_decimal && (number <= MAX_SLONG)) - { - yylval.int32Val = (SLONG) number; - return TOK_NUMBER32BIT; - } - else - { - /* We have either a decimal point with no exponent - or a string of digits whose value exceeds MAX_SLONG: - the returned type depends on the client dialect, - so warn of the difference if the client dialect is - SQL_DIALECT_V6_TRANSITION. - */ - - if (SQL_DIALECT_V6_TRANSITION == client_dialect) - { - /* Issue a warning about the ambiguity of the numeric - * numeric literal. There are multiple calls because - * the message text exceeds the 119-character limit - * of our message database. - */ - ERRD_post_warning(Arg::Warning(isc_dsql_warning_number_ambiguous) << - Arg::Str(Firebird::string(lex.last_token, lex.ptr - lex.last_token))); - ERRD_post_warning(Arg::Warning(isc_dsql_warning_number_ambiguous1)); - } + yylval.stringPtr = newString(pureString); + // Mantissa wider than 64 bits does not fit into double without loss + return (isOverExponent64b || isOverMantisa64b) ? TOK_DECIMAL_NUMBER : TOK_FLOAT_NUMBER; + } - lex.last_token_bk = lex.last_token; - lex.line_start_bk = lex.line_start; - lex.lines_bk = lex.lines; + // 128-bit + if (isOverMantisa64b) + { + yylval.lim64ptr = newLim64String(pureString, scale); - if (client_dialect < SQL_DIALECT_V6_TRANSITION) - { - yylval.stringPtr = newString( - Firebird::string(lex.last_token, lex.ptr - lex.last_token)); - return TOK_FLOAT_NUMBER; - } + // Special case - on the border of positive number: exactly (MAX_SINT64 + 1) + Int128 limit; + limit.set(MAX_SINT64, 0); + limit += 1U; + if (mantisaValue == limit) + return scale ? TOK_LIMIT64_NUMBER : TOK_LIMIT64_INT; - yylval.scaledNumber.number = number; - yylval.scaledNumber.scale = scale; - yylval.scaledNumber.hex = false; + return TOK_NUM128; + } - if (have_decimal) - return TOK_SCALEDINT; + Int128 tmp; + if (state != state_precision) + { + tmp.set(MAX_SLONG, 0); + if (tmp >= mantisaValue) + { + // A natural 32 bit number + yylval.int32Val = (SLONG)mantisaValue.toInteger(0); + return TOK_NUMBER32BIT; + } + } + /* We have either a decimal point with no exponent + or a string of digits whose value exceeds MAX_SLONG: + the returned type depends on the client dialect, + so warn of the difference if the client dialect is + SQL_DIALECT_V6_TRANSITION. + */ + if (SQL_DIALECT_V6_TRANSITION == client_dialect) + { + /* Issue a warning about the ambiguity of the numeric + * numeric literal. There are multiple calls because + * the message text exceeds the 119-character limit + * of our message database. + */ + ERRD_post_warning(Arg::Warning(isc_dsql_warning_number_ambiguous) << Arg::Str( + Firebird::string(lex.last_token, lex.ptr - lex.last_token))); + ERRD_post_warning(Arg::Warning(isc_dsql_warning_number_ambiguous1)); + } - return TOK_NUMBER64BIT; - } - } // else if (!have_exp) - } // if (!have_error) + if (client_dialect < SQL_DIALECT_V6_TRANSITION) + { + yylval.stringPtr = newString(pureString); + return TOK_FLOAT_NUMBER; + } - // we got some kind of error or overflow, so don't recognize this - // as a number: just pass it through to the next part of the lexer. + yylval.scaledNumber.number = mantisaValue.toInt64(0); + yylval.scaledNumber.scale = scale; + yylval.scaledNumber.hex = false; + return state == state_precision ? TOK_SCALEDINT : TOK_NUMBER64BIT; } // Restore the status quo ante, before we started our unsuccessful diff --git a/src/dsql/Parser.h b/src/dsql/Parser.h index 265a125cc9a..598cb8888bc 100644 --- a/src/dsql/Parser.h +++ b/src/dsql/Parser.h @@ -359,6 +359,14 @@ class Parser : public Firebird::PermanentStorage } void checkTimeDialect(); + void exceptionNumericLiterals(const Firebird::string& text) + { + using namespace Firebird; + ERRD_post(Arg::Gds(isc_sqlerr) << Arg::Num(-104) << Arg::Gds(isc_dsql_token_unk_err) + << Arg::Num(yyposn.firstLine) << Arg::Num(yyposn.firstColumn) + << Arg::Gds(isc_random) << Arg::Str(text)); + } + // start - defined in btyacc_fb.ske private: static void yySCopy(YYSTYPE* to, YYSTYPE* from, int size); diff --git a/src/dsql/chars.h b/src/dsql/chars.h index 8c5f899dae6..a6ddf7cdb83 100644 --- a/src/dsql/chars.h +++ b/src/dsql/chars.h @@ -21,17 +21,21 @@ * Contributor(s): ______________________________________. */ -inline constexpr SCHAR CHR_LETTER = 1; -inline constexpr SCHAR CHR_DIGIT = 2; -inline constexpr SCHAR CHR_IDENT = 4; -inline constexpr SCHAR CHR_QUOTE = 8; -inline constexpr SCHAR CHR_WHITE = 16; -inline constexpr SCHAR CHR_HEX = 32; -inline constexpr SCHAR CHR_INTRODUCER = 64; +inline constexpr SSHORT CHR_LETTER = (1 << 0); +inline constexpr SSHORT CHR_DIGIT = (1 << 1); +inline constexpr SSHORT CHR_IDENT = (1 << 2); +inline constexpr SSHORT CHR_QUOTE = (1 << 3); +inline constexpr SSHORT CHR_WHITE = (1 << 4); +inline constexpr SSHORT CHR_HEX = (1 << 5); +inline constexpr SSHORT CHR_INTRODUCER = (1 << 6); +inline constexpr SSHORT CHR_BIN = (1 << 7); +inline constexpr SSHORT CHR_OCT = (1 << 8); +inline constexpr SSHORT CHR_BRACE = (1 << 9); + // Use the functions at the end of this file; do not reference the array directly. -static inline constexpr SCHAR classes_array[] = { +static inline constexpr SSHORT classes_array[] = { /* 000 */ 0, /* 001 */ 0, /* 002 */ 0, @@ -80,14 +84,14 @@ static inline constexpr SCHAR classes_array[] = { /* 045 - */ 0, /* 046 . */ 0, /* 047 / */ 0, -/* 048 0 */ 0 | CHR_DIGIT | CHR_IDENT | CHR_HEX, -/* 049 1 */ 0 | CHR_DIGIT | CHR_IDENT | CHR_HEX, -/* 050 2 */ 0 | CHR_DIGIT | CHR_IDENT | CHR_HEX, -/* 051 3 */ 0 | CHR_DIGIT | CHR_IDENT | CHR_HEX, -/* 052 4 */ 0 | CHR_DIGIT | CHR_IDENT | CHR_HEX, -/* 053 5 */ 0 | CHR_DIGIT | CHR_IDENT | CHR_HEX, -/* 054 6 */ 0 | CHR_DIGIT | CHR_IDENT | CHR_HEX, -/* 055 7 */ 0 | CHR_DIGIT | CHR_IDENT | CHR_HEX, +/* 048 0 */ 0 | CHR_DIGIT | CHR_IDENT | CHR_HEX | CHR_OCT | CHR_BIN, +/* 049 1 */ 0 | CHR_DIGIT | CHR_IDENT | CHR_HEX | CHR_OCT | CHR_BIN, +/* 050 2 */ 0 | CHR_DIGIT | CHR_IDENT | CHR_HEX | CHR_OCT, +/* 051 3 */ 0 | CHR_DIGIT | CHR_IDENT | CHR_HEX | CHR_OCT, +/* 052 4 */ 0 | CHR_DIGIT | CHR_IDENT | CHR_HEX | CHR_OCT, +/* 053 5 */ 0 | CHR_DIGIT | CHR_IDENT | CHR_HEX | CHR_OCT, +/* 054 6 */ 0 | CHR_DIGIT | CHR_IDENT | CHR_HEX | CHR_OCT, +/* 055 7 */ 0 | CHR_DIGIT | CHR_IDENT | CHR_HEX | CHR_OCT, /* 056 8 */ 0 | CHR_DIGIT | CHR_IDENT | CHR_HEX, /* 057 9 */ 0 | CHR_DIGIT | CHR_IDENT | CHR_HEX, /* 058 : */ 0, @@ -155,9 +159,9 @@ static inline constexpr SCHAR classes_array[] = { /* 120 x */ 0 | CHR_LETTER | CHR_IDENT, /* 121 y */ 0 | CHR_LETTER | CHR_IDENT, /* 122 z */ 0 | CHR_LETTER | CHR_IDENT, -/* 123 { */ 0 | CHR_LETTER | CHR_IDENT, +/* 123 { */ 0 | CHR_LETTER | CHR_IDENT | CHR_BRACE, /* 124 | */ 0, -/* 125 } */ 0 | CHR_LETTER | CHR_IDENT, +/* 125 } */ 0 | CHR_LETTER | CHR_IDENT | CHR_BRACE, /* 126 ~ */ 0, /* 127 */ 0, /* 128 */ 0, @@ -290,12 +294,12 @@ static inline constexpr SCHAR classes_array[] = { /* 255 */ 0 }; -inline SCHAR classes(int idx) noexcept +inline SSHORT classes(int idx) noexcept { return classes_array[(UCHAR) idx]; } -inline SCHAR classes(UCHAR idx) noexcept +inline SSHORT classes(UCHAR idx) noexcept { return classes_array[idx]; } diff --git a/src/include/firebird/impl/consts_pub.h b/src/include/firebird/impl/consts_pub.h index b10250b0822..3831a26788a 100644 --- a/src/include/firebird/impl/consts_pub.h +++ b/src/include/firebird/impl/consts_pub.h @@ -445,6 +445,7 @@ #define isc_spb_bkp_no_triggers 0x8000 #define isc_spb_bkp_zip 0x010000 #define isc_spb_bkp_direct_io 0x020000 +#define isc_spb_bkp_fast_path 0x040000 /******************************************** * Parameters for isc_action_svc_properties * @@ -575,6 +576,7 @@ #define isc_spb_res_replace 0x1000 #define isc_spb_res_create 0x2000 #define isc_spb_res_use_all_space 0x4000 +#define isc_spb_res_fast_path isc_spb_bkp_fast_path #define isc_spb_res_direct_io isc_spb_bkp_direct_io #define isc_spb_res_replica_mode 20 diff --git a/src/include/firebird/impl/msg/gbak.h b/src/include/firebird/impl/msg/gbak.h index d678c187a57..7ff2b5855c8 100644 --- a/src/include/firebird/impl/msg/gbak.h +++ b/src/include/firebird/impl/msg/gbak.h @@ -423,3 +423,8 @@ FB_IMPL_MSG(GBAK, 424, gbak_writing_constant, -901, "00", "000", "writing consta FB_IMPL_MSG(GBAK, 425, gbak_constant, -901, "00", "000", "constant (in RDB$CONSTANTS)") FB_IMPL_MSG(GBAK, 426, gbak_restoring_constant, -901, "00", "000", "restoring constant %s") FB_IMPL_MSG_NO_SYMBOL(GBAK, 427, "skipping data for table @1") +FB_IMPL_MSG(GBAK, 428, gbak_inv_column, -901, "00", "000", "invalid column @1 in table @2: @3") +FB_IMPL_MSG(GBAK, 429, gbak_inv_record_length, -901, "00", "000", "invalid record length for table @1") +FB_IMPL_MSG(GBAK, 430, gbak_fast_path_needs_service, -901, "00", "000", "FAST_PATH requires the -service option") +FB_IMPL_MSG_NO_SYMBOL(GBAK, 431, " @1FAST(_PATH) fast-path data transfer (requires -service)") +FB_IMPL_MSG(GBAK, 432, gbak_fast_path_no_snapshot, -901, "00", "000", "cannot obtain transaction snapshot number for FAST_PATH backup") diff --git a/src/include/firebird/impl/msg/jrd.h b/src/include/firebird/impl/msg/jrd.h index 31b3e13807a..a9d76243c7b 100644 --- a/src/include/firebird/impl/msg/jrd.h +++ b/src/include/firebird/impl/msg/jrd.h @@ -1022,3 +1022,4 @@ FB_IMPL_MSG(JRD, 1019, dsql_agg_exit_group, -204, "42", "000", "EXIT is not allo FB_IMPL_MSG(JRD, 1020, dsql_agg_return, -204, "42", "000", "RETURN is not allowed in ON START DO, ON ACCUMULATE DO or ON FINISH DO sections of aggregate function; use EXIT instead") FB_IMPL_MSG(JRD, 1021, hypfun_args_non_equal_sort_item, -833, "42", "000", "Number of arguments of hypothetical-set aggregate function @1 must match number of sort items in WITHIN GROUP clause") FB_IMPL_MSG(JRD, 1022, old_format, -804, "07", "000", "Statement format outdated, need to be reprepared") +FB_IMPL_MSG(JRD, 1023, user_savepoint, -901, "0A", "000", "Running under user savepoint @1, DDL prohibited") diff --git a/src/include/gen/Firebird.pas b/src/include/gen/Firebird.pas index 03819b9c107..393399381a7 100644 --- a/src/include/gen/Firebird.pas +++ b/src/include/gen/Firebird.pas @@ -4513,6 +4513,7 @@ IPerformanceStatsImpl = class(IPerformanceStats) isc_spb_bkp_no_triggers = $8000; isc_spb_bkp_zip = $010000; isc_spb_bkp_direct_io = $020000; + isc_spb_bkp_fast_path = $040000; isc_spb_prp_page_buffers = byte(5); isc_spb_prp_sweep_interval = byte(6); isc_spb_prp_shutdown_db = byte(7); @@ -6073,6 +6074,7 @@ IPerformanceStatsImpl = class(IPerformanceStats) isc_dsql_agg_return = 335545340; isc_hypfun_args_non_equal_sort_item = 335545341; isc_old_format = 335545342; + isc_user_savepoint = 335545343; isc_gfix_db_name = 335740929; isc_gfix_invalid_sw = 335740930; isc_gfix_incmp_sw = 335740932; @@ -6342,6 +6344,10 @@ IPerformanceStatsImpl = class(IPerformanceStats) isc_gbak_writing_constant = 336331176; isc_gbak_constant = 336331177; isc_gbak_restoring_constant = 336331178; + isc_gbak_inv_column = 336331180; + isc_gbak_inv_record_length = 336331181; + isc_gbak_fast_path_needs_service = 336331182; + isc_gbak_fast_path_no_snapshot = 336331184; isc_dsql_too_old_ods = 336397205; isc_dsql_table_not_found = 336397206; isc_dsql_view_not_found = 336397207; diff --git a/src/jrd/Attachment.h b/src/jrd/Attachment.h index 44156d10557..7e9d41c40ed 100644 --- a/src/jrd/Attachment.h +++ b/src/jrd/Attachment.h @@ -537,7 +537,7 @@ class Attachment : public pool_alloc const ULONG att_lock_owner_id; // ID for the lock manager SLONG att_lock_owner_handle; // Handle for the lock manager ULONG att_backup_state_counter; // Counter of backup state locks for attachment - SLONG att_event_session; // Event session id, if any + std::atomic att_event_session; // Event session id, if any SecurityClass* att_security_class; // security class for database SecurityClassList* att_security_classes; // security classes RuntimeStatistics att_stats; diff --git a/src/jrd/CacheVector.h b/src/jrd/CacheVector.h index 599ac64e988..af4710ce31b 100644 --- a/src/jrd/CacheVector.h +++ b/src/jrd/CacheVector.h @@ -40,6 +40,7 @@ #include "../jrd/constants.h" #include "../jrd/tra_proto.h" #include "../jrd/QualifiedName.h" +#include "../jrd/obj.h" namespace Jrd { @@ -52,6 +53,10 @@ class ObjectBase { public: typedef unsigned Flag; + +public: + virtual ~ObjectBase() + { } }; @@ -87,6 +92,13 @@ class ElementBase virtual ~ElementBase(); virtual void cleanup(thread_db* tdbb) = 0; + virtual ObjectType getObjectType() = 0; + virtual void getObjectName(QualifiedName& name) = 0; + virtual void newVersion(thread_db* tdbb) = 0; + virtual void makeVersion(thread_db* tdbb) = 0; + virtual void makeRequests(thread_db* tdbb) = 0; + virtual void commit(thread_db* tdbb, TraNumber curNumber = 0) = 0; + virtual MdcVersion getVersion(thread_db* tdbb) = 0; public: [[noreturn]] void busyError(thread_db* tdbb, MetaId id, const char* family); @@ -99,6 +111,9 @@ class ElementBase return locked; } + // fill dependencies info + void fillDeps(thread_db* tdbb, bool forceRecompile); + private: Lock* lock = nullptr; std::atomic locked = false; @@ -163,11 +178,17 @@ class ListEntry : public HazardObject public: enum State { INITIAL, RELOAD, MISSING, SCANNING, READY }; - ListEntry(Versioned* object, TraNumber traNumber, ObjectBase::Flag fl, ListEntry* link = nullptr) + ListEntry(thread_db* tdbb, Versioned* object, TraNumber traNumber, ObjectBase::Flag fl, ListEntry* link = nullptr) : object(object), traNumber(traNumber), cacheFlags(fl), state(INITIAL) { if (fl & CacheFlag::ERASED) fb_assert(!object); + + // Handle front & back versions of MDC + VersionIncr incr(tdbb); + version = incr.getVersion(); + + // Add to linked list if (link) next.store(link); } @@ -550,19 +571,9 @@ class ListEntry : public HazardObject return state == READY ? false : (thd == Thread::getCurrentThreadId()) && (state == SCANNING); } - static bool upgradable(HazardPtr& listEntry, const Versioned* from) + MdcVersion getVersion() { - for (; listEntry; listEntry.set(listEntry->next)) - { - if (listEntry->object == from) - return false; // not found upgrade version - - if (listEntry->getFlags() & CacheFlag::COMMITTED) - return true; // already upgraded by someone else - } - - fb_assert(false); - return false; // miss from what to upgrade + return version; } private: @@ -582,7 +593,6 @@ class ListEntry : public HazardObject TraNumber traNumber; // when COMMITTED not set - stores transaction that created this list element // when COMMITTED is set - stores transaction after which older elements are not needed // traNumber to be changed BEFORE setting COMMITTED - MdcVersion version = 0; // version of metadata cache when object was added ThreadId thd = 0; // thread that performs object scan() std::atomic cacheFlags; @@ -677,6 +687,13 @@ class CacheElement : public ElementBase, public P return getVersioned(tdbb, TransactionNumber::current(tdbb), fl); } + void makeVersion(thread_db* tdbb) override + { + newVersion(tdbb); + auto rc = getVersioned(tdbb, 0); + fb_assert(rc); + } + bool isReady(thread_db* tdbb) { auto entry = getEntry(tdbb, TransactionNumber::current(tdbb), CacheFlag::NOSCAN | CacheFlag::NOCOMMIT); @@ -748,7 +765,7 @@ class CacheElement : public ElementBase, public P ListEntry* newEntry = nullptr; try { - newEntry = FB_NEW ListEntry(obj, traNum, fl & ~CacheFlag::ERASED); + newEntry = FB_NEW ListEntry(tdbb, obj, traNum, fl & ~CacheFlag::ERASED); } catch (const Firebird::Exception&) { @@ -776,6 +793,7 @@ class CacheElement : public ElementBase, public P return listEntry; // nullptr } + toUpdatedList(tdbb, fl); return HazardPtr>(newEntry); } @@ -784,7 +802,9 @@ class CacheElement : public ElementBase, public P fb_assert(list.load()); listEntry = list; } + fl &= ~CacheFlag::AUTOCREATE; + toUpdatedList(tdbb, fl); return ListEntry::getEntry(tdbb, listEntry, traNum, fl, this); } @@ -812,7 +832,7 @@ class CacheElement : public ElementBase, public P if (!cur) cur = TransactionNumber::current(tdbb); - ListEntry* newEntry = FB_NEW ListEntry(obj, cur, fl); + ListEntry* newEntry = FB_NEW ListEntry(tdbb, obj, cur, fl); if (!ListEntry::add(tdbb, list, newEntry)) { newEntry->cleanup(tdbb, false); @@ -867,15 +887,15 @@ class CacheElement : public ElementBase, public P return nullptr; } - void commit(thread_db* tdbb, TraNumber cur = 0) + void commit(thread_db* tdbb, TraNumber curNumber = 0) override { HazardPtr> current(list); if (current) { - if (!cur) - cur = TransactionNumber::current(tdbb); + if (!curNumber) + curNumber = TransactionNumber::current(tdbb); - auto flags = current->commit(tdbb, cur, TransactionNumber::next(tdbb)); + auto flags = current->commit(tdbb, curNumber, TransactionNumber::next(tdbb)); if (flags & CacheFlag::NOCOMMIT) // Committed newly created version in cache pingLock(tdbb, flags, this->getId(), Versioned::objectFamily(this)); @@ -945,12 +965,17 @@ class CacheElement : public ElementBase, public P return listEntry->scanInProgress(); } - static int getObjectType() + ObjectType getObjectType() override { return Versioned::objectType(); } - void newVersion(thread_db* tdbb) + void getObjectName(QualifiedName& name) override + { + name = this->getName(); + } + + void newVersion(thread_db* tdbb) override { TraNumber traNum; @@ -974,41 +999,25 @@ class CacheElement : public ElementBase, public P } } - bool upgrade(thread_db* tdbb, const Versioned* from) + bool nameIs(const QualifiedName& name) { - HazardPtr> l(list); - - // list of versions should be present - fb_assert(l); - if (!l) - return false; - - // if there is another version at the top nothing to be added - if (l->getVersioned() != from) - return ListEntry::upgradable(l, from); - - // we have candidate for upgrade - make sure it's not half-done - fb_assert(l->getFlags() & CacheFlag::COMMITTED); - if (!(l->getFlags() & CacheFlag::COMMITTED)) - return false; - - // Try to upgrade - ListEntry* newEntry = FB_NEW ListEntry(nullptr, TransactionNumber::current(tdbb), - CacheFlag::COMMITTED | CacheFlag::MINISCAN | CacheFlag::DB_VERSION, l.getPointer()); - if (l.replace(list, newEntry)) - return true; + return this->getName() == name; + } - // undo changes - delete newEntry; + // This is needed to check correctness of statements present in current object's version + void makeRequests(thread_db* tdbb) override + { + Versioned* v = getVersioned(tdbb, CacheFlag::AUTOCREATE); + if (!v) + return; - // Someone already added entry - see is it OK for us - l.set(list); - return ListEntry::upgradable(l, from); + v->makeRequests(tdbb); } - bool nameIs(const QualifiedName& name) + MdcVersion getVersion(thread_db* tdbb) override { - return this->getName() == name; + auto entry = getEntry(tdbb, TransactionNumber::current(tdbb), CacheFlag::NOSCAN | CacheFlag::NOCOMMIT); + return entry ? entry->getVersion() : 0; } private: @@ -1018,6 +1027,13 @@ class CacheElement : public ElementBase, public P atomics::memory_order_release, atomics::memory_order_relaxed); } + // Check flags and may be fill dependencies info + void toUpdatedList(thread_db* tdbb, ObjectBase::Flag fl) + { + if (fl & CacheFlag::DEPENDS) + ElementBase::fillDeps(tdbb, true); + } + private: std::atomic*> list = nullptr; std::atomic resetAt = 0; @@ -1218,28 +1234,6 @@ class CacheVector : public Firebird::PermanentStorage return data; } - bool upgrade(thread_db* tdbb, MetaId id, const Versioned* from) - { - fb_assert(id < getCount()); - - if (id < getCount()) - { - auto ptr = getDataPointer(id); - fb_assert(ptr); - - if (ptr) - { - StoredElement* data = ptr->load(atomics::memory_order_acquire); - fb_assert(data); - - if (data) - return data->upgrade(tdbb, from); - } - } - - return false; - } - bool lookup(thread_db* tdbb, const QualifiedName& name, ObjectBase::Flag fl, StoredElement** element, Versioned** versioned) { diff --git a/src/jrd/CharSetContainer.h b/src/jrd/CharSetContainer.h index df397fdb249..d25440cd251 100644 --- a/src/jrd/CharSetContainer.h +++ b/src/jrd/CharSetContainer.h @@ -134,6 +134,8 @@ class CharSetVers final : public ObjectBase static ObjectType objectType() noexcept; + void makeRequests(thread_db* tdbb) { } + bool hash(thread_db*, Firebird::sha512&) { return true; diff --git a/src/jrd/Function.h b/src/jrd/Function.h index 1f14c5c8a8c..d842ba5dc16 100644 --- a/src/jrd/Function.h +++ b/src/jrd/Function.h @@ -71,7 +71,7 @@ namespace Jrd } public: - int getObjectType() const noexcept override + ObjectType getObjectType() const noexcept override { return objectType(); } diff --git a/src/jrd/Package.h b/src/jrd/Package.h index 6d54bda85c4..036f614f83b 100644 --- a/src/jrd/Package.h +++ b/src/jrd/Package.h @@ -214,7 +214,7 @@ class Package final : public Firebird::PermanentStorage, public ObjectBase return getPermanent()->id; } - int getObjectType() const noexcept + ObjectType getObjectType() const noexcept { return objectType(); } @@ -226,6 +226,8 @@ class Package final : public Firebird::PermanentStorage, public ObjectBase static ObjectType objectType() noexcept; + void makeRequests(thread_db* tdbb) {/*!!!!!!!!!!!!!!!*/} + bool hash(thread_db* tdbb, Firebird::sha512& digest); Cached::Package* getPermanent() const noexcept diff --git a/src/jrd/Relation.cpp b/src/jrd/Relation.cpp index 7192d265deb..377445ab01e 100644 --- a/src/jrd/Relation.cpp +++ b/src/jrd/Relation.cpp @@ -736,6 +736,8 @@ IndexVersion::IndexVersion(MemoryPool& p, Cached::Index* idp) void IndexVersion::destroy(thread_db* tdbb, IndexVersion* idv) { + if (idv->idv_code) + IndexCode::release(tdbb, idv->idv_code); delete idv; } @@ -744,6 +746,18 @@ ObjectType IndexVersion::objectType() noexcept return obj_index; } +void IndexVersion::replaceCodeConditional(thread_db* tdbb, IndexCode* newCode) +{ + if (newCode && (newCode != idv_code)) + { + auto* oldCode = idv_code; + newCode->addRef(); + idv_code = newCode; + if (oldCode) + IndexCode::release(tdbb, oldCode); + } +} + void jrd_rel::releaseTriggers(thread_db* tdbb, bool destroy) { @@ -1114,20 +1128,13 @@ FB_UINT64 IndexPermanent::makeLockId(MetaId relId, MetaId indexId) return (FB_UINT64(relId) << REL_ID_KEY_OFFSET) + indexId; } -void IndexPermanent::releaseStatements(thread_db* tdbb) +void IndexCode::releaseStatements(thread_db* tdbb) { - if (idp_expression_statement) - { - idp_expression_statement->release(tdbb); - idp_expression_statement = nullptr; - idp_expression = nullptr; - } - if (idp_condition_statement) - { - idp_condition_statement->release(tdbb); - idp_condition_statement = nullptr; - idp_condition = nullptr; - } + if (expression_statement) + expression_statement->release(tdbb); + + if (condition_statement) + condition_statement->release(tdbb); } void IndexPermanent::reloadAst(thread_db* tdbb, TraNumber tran, bool erase) diff --git a/src/jrd/Relation.h b/src/jrd/Relation.h index 53710d394df..3cb57ab241d 100644 --- a/src/jrd/Relation.h +++ b/src/jrd/Relation.h @@ -254,6 +254,8 @@ class DbTriggers final : public Triggers, public ObjectBase static ObjectType objectType() noexcept; + void makeRequests(thread_db* tdbb) {/*!!!!!!!!!!!!!!!*/} + private: DbTriggersHeader* perm; @@ -455,6 +457,46 @@ enum IndexStatus // Index block +struct IndexCode +{ +public: + IndexCode() + { + expression_bid.clear(); + condition_bid.clear(); + addRef(); // first reference always comes from IndexPermanent + } + + bid expression_bid; + ValueExprNode* expression = nullptr; // node tree for index expression + Statement* expression_statement = nullptr; // statement for index expression evaluation + dsc expression_desc; // descriptor for expression result + + bid condition_bid; + BoolExprNode* condition = nullptr; // node tree for index condition + Statement* condition_statement = nullptr; // statement for index condition evaluation + + void addRef() + { + ++cnt; + } + + static void release(thread_db* tdbb, IndexCode*& code) + { + if (code && --code->cnt == 0) + { + code->releaseStatements(tdbb); + delete code; + code = nullptr; + } + } + +private: + void releaseStatements(thread_db* tdbb); + std::atomic cnt = 0; +}; + + class IndexPermanent : public Firebird::PermanentStorage { public: @@ -463,8 +505,6 @@ class IndexPermanent : public Firebird::PermanentStorage idp_relation(rel), idp_id(id) { - idp_expression_bid.clear(); - idp_condition_bid.clear(); } ~IndexPermanent() @@ -472,7 +512,8 @@ class IndexPermanent : public Firebird::PermanentStorage static bool destroy(thread_db* tdbb, IndexPermanent* idp) { - idp->releaseStatements(tdbb); + if (idp->idp_latest_code) + IndexCode::release(tdbb, idp->idp_latest_code); return false; } @@ -492,8 +533,12 @@ class IndexPermanent : public Firebird::PermanentStorage static FB_UINT64 makeLockId(MetaId relId, MetaId indexId); const QualifiedName& getName(); +public: + Firebird::Mutex idp_code_mutex; // Delays concurrent threads till the end of code refresh + private: RelationPermanent* idp_relation; + IndexCode* idp_latest_code = nullptr; MetaId idp_id; TraNumber idp_tranum = 0; UCHAR idp_state = 0; // Makes limited sense for segmented indices @@ -501,26 +546,25 @@ class IndexPermanent : public Firebird::PermanentStorage [[noreturn]] void errIndexGone(); - void releaseStatements(thread_db* tdbb); - public: - void lookupIndexCode(thread_db* tdbb, Cached::Relation* relation, index_desc* idx, - const Ods::index_root_page::irt_repeat* irt_desc) + void setState(UCHAR state) noexcept { - if ((irt_desc->getState() != idp_state) || (irt_desc->getTransaction() != idp_tranum)) - refreshIndexCode(tdbb, relation, idx, irt_desc); + idp_state = state; + } - idx->idx_condition_node = idp_condition; - idx->idx_condition_statement = idp_condition_statement; + UCHAR getState() const noexcept + { + return idp_state; + } - idx->idx_expression_node = idp_expression; - idx->idx_expression_statement = idp_expression_statement; - memcpy(&idx->idx_expression_desc, &idp_expression_desc, sizeof(struct dsc)); + void setTraNum(TraNumber num) noexcept + { + idp_tranum = num; } - void setState(UCHAR state) noexcept + TraNumber getTraNum() const noexcept { - idp_state = state; + return idp_tranum; } UCHAR getFormat() const noexcept @@ -533,20 +577,13 @@ class IndexPermanent : public Firebird::PermanentStorage idp_formatNumber = fmt; } -private: - void refreshIndexCode(thread_db* tdbb, Cached::Relation* relation, - index_desc* idx, const Ods::index_root_page::irt_repeat* irt_desc); - - Firebird::Mutex idp_code_mutex; // Delays concurrent threads till the end of code refresh + IndexCode* refreshIndexCode(thread_db* tdbb, Cached::Relation* relation, + const Ods::index_root_page::irt_repeat* irt_desc); - bid idp_expression_bid; - ValueExprNode* idp_expression = nullptr; // node tree for index expression - Statement* idp_expression_statement = nullptr; // statement for index expression evaluation - dsc idp_expression_desc; // descriptor for expression result - - bid idp_condition_bid; - BoolExprNode* idp_condition = nullptr; // node tree for index condition - Statement* idp_condition_statement = nullptr; // statement for index condition evaluation + IndexCode* getCode() const noexcept + { + return idp_latest_code; + } }; @@ -565,6 +602,8 @@ class IndexVersion final : public ObjectBase static std::optional getIdByName(thread_db* tdbb, ExName name); static ObjectType objectType() noexcept; + void makeRequests(thread_db* tdbb) {/*!!!!!!!!!!!!!!!*/} + ScanResult reload(thread_db* tdbb, ObjectBase::Flag flags) { return scan(tdbb, flags); @@ -610,11 +649,17 @@ class IndexVersion final : public ObjectBase void setLtt(thread_db* tdbb, const QualifiedName& name, bool unique, bool descending, USHORT segmentCount, bool inactive = false); + void lookupIndexCode(thread_db* tdbb, Cached::Relation* relation, index_desc* idx, + const Ods::index_root_page::irt_repeat* irt_desc); + void replaceCodeConditional(thread_db* tdbb, IndexCode* newCode); + IndexCode* refreshIndexCode(thread_db* tdbb, Cached::Relation* relation, + const Ods::index_root_page::irt_repeat* irt_desc); static const enum lck_t LOCKTYPE = LCK_idx_rescan; private: Cached::Index* perm; + IndexCode* idv_code = nullptr; QualifiedName idv_name; SSHORT idv_uniqFlag = 0; SSHORT idv_segmentCount = 0; @@ -694,6 +739,8 @@ class jrd_rel final : public ObjectBase static const char* objectFamily(RelationPermanent* perm); static ObjectType objectType() noexcept; + void makeRequests(thread_db* tdbb) {/*!!!!!!!!!!!!!!!*/} + void releaseTriggers(thread_db* tdbb, bool destroy); const Trigger* findTrigger(const QualifiedName& trig_name) const; const Format* currentFormat(thread_db* tdbb); @@ -856,7 +903,7 @@ class RelationPermanent : public Firebird::PermanentStorage Cached::Index* lookupIndex(thread_db* tdbb, const QualifiedName& name, ObjectBase::Flag flags); Cached::Index* ensureIndex(thread_db* tdbb, MetaId id); - void newIndexVersion(thread_db* tdbb, MetaId id, ObjectBase::Flag scanType) + void newIndexVersion(thread_db* tdbb, MetaId id) { [[maybe_unused]] auto chk = rel_indices.newVersion(tdbb, id); fb_assert(chk); @@ -1201,6 +1248,18 @@ inline void GCLock::Exclusive::release() } +/// class IndexVersion + +inline void IndexVersion::lookupIndexCode(thread_db* tdbb, Cached::Relation* relation, index_desc* idx, + const Ods::index_root_page::irt_repeat* irt_desc) +{ + if ((irt_desc->getState() != perm->getState()) || (irt_desc->getTransaction() != perm->getTraNum())) + idx->idx_code = perm->refreshIndexCode(tdbb, relation, irt_desc); + else + idx->idx_code = idv_code; +} + + // Field block, one for each field in a scanned relation inline constexpr USHORT FLD_parse_computed = 0x0001; // computed expression is being parsed diff --git a/src/jrd/Routine.cpp b/src/jrd/Routine.cpp index 5421e83efe8..e8cb44b659d 100644 --- a/src/jrd/Routine.cpp +++ b/src/jrd/Routine.cpp @@ -233,6 +233,13 @@ void Routine::parseMessages(thread_db* tdbb, CompilerScratch* csb, BlrReader blr } } +void Routine::makeRequests(thread_db* tdbb) +{ + auto *req = statement->findRequest(tdbb); + if (req) + req->setUnused(); +} + bool Routine::hash(thread_db* tdbb, Firebird::sha512& digest) { if (inputFormat) diff --git a/src/jrd/Routine.h b/src/jrd/Routine.h index acbdf75466d..2f23608c47a 100644 --- a/src/jrd/Routine.h +++ b/src/jrd/Routine.h @@ -169,9 +169,11 @@ namespace Jrd void sharedCheckUnlock(thread_db* tdbb); + void makeRequests(thread_db* tdbb); + public: virtual RoutinePermanent* getPermanent() const noexcept = 0; // Permanent part of data - virtual int getObjectType() const noexcept = 0; + virtual ObjectType getObjectType() const noexcept = 0; virtual SLONG getSclType() const noexcept = 0; private: diff --git a/src/jrd/Savepoint.cpp b/src/jrd/Savepoint.cpp index 215bffe1ab9..c86cd922cf9 100644 --- a/src/jrd/Savepoint.cpp +++ b/src/jrd/Savepoint.cpp @@ -513,8 +513,6 @@ Savepoint* Savepoint::rollback(thread_db* tdbb, Savepoint* prior, bool preserveL auto permanent = relation->getPermanent(); permanent->rollback(tdbb); - RelationPermanent::destroy(tdbb, permanent); - delete permanent; } delete *lttPtr; diff --git a/src/jrd/blb.cpp b/src/jrd/blb.cpp index c0c72b97bc9..dfcb58164af 100644 --- a/src/jrd/blb.cpp +++ b/src/jrd/blb.cpp @@ -1122,7 +1122,7 @@ void blb::move(thread_db* tdbb, dsc* from_desc, dsc* to_desc, } record->clearNull(fieldId); - jrd_tra* transaction = request->req_transaction; + jrd_tra* transaction = request ? request->req_transaction : tdbb->getTransaction(); transaction = transaction->getOuter(); // Declared LTT records live only in an execution frame, while their BLOB @@ -1343,23 +1343,40 @@ void blb::move(thread_db* tdbb, dsc* from_desc, dsc* to_desc, blobIndex->bli_materialized = true; blobIndex->bli_blob_id = *destination; - // Assign temporary BLOB ownership to top-level request if it is not assigned yet - Request* own_request; - if (blobIndex->bli_request) { - own_request = blobIndex->bli_request; + + if (request) + { + // Assign temporary BLOB ownership to top-level request if it is not assigned yet + Request* own_request; + if (blobIndex->bli_request) { + own_request = blobIndex->bli_request; + } + else + { + own_request = request; + while (own_request->req_caller) + own_request = own_request->req_caller; + blobIndex->bli_request = own_request; + own_request->req_blobs.add(blob->blb_temp_id); + } + // Not sure that this ownership is entirely correct for arrays, but + // even if I make mistake here widening array lifetime this should not hurt much + if (array) + array->arr_request = own_request; } - else + else if (array) { - own_request = request; - while (own_request->req_caller) - own_request = own_request->req_caller; - blobIndex->bli_request = own_request; - own_request->req_blobs.add(blob->blb_temp_id); + // Direct-VIO burp path (no request exists). The array payload has + // already been copied into the blob by store_array and the blob + // stored above, so the ArrayField is no longer needed. Release it + // immediately to keep memory bounded to the current batch instead + // of accumulating full array buffers until transaction end (the + // request-owned path releases at request end; here there is no + // request, so without this the 1MB message-batch limit would not + // bound engine allocations). + release_array(array); + array = nullptr; } - // Not sure that this ownership is entirely correct for arrays, but - // even if I make mistake here widening array lifetime this should not hurt much - if (array) - array->arr_request = own_request; } const bool purgeBlob = !materialized_blob || @@ -1953,6 +1970,23 @@ void blb::release_array(ArrayField* array) } +void blb::releaseRequestlessArrays(jrd_tra* transaction) +{ + if (!transaction) + return; + + for (ArrayField** ptr = &transaction->tra_arrays; *ptr;) + { + const auto array = *ptr; + + if (!array->arr_request) + release_array(array); + else + ptr = &(*ptr)->arr_next; + } +} + + void blb::scalar(thread_db* tdbb, jrd_tra* transaction, const bid* blob_id, diff --git a/src/jrd/blb.h b/src/jrd/blb.h index ffecca6a8bf..b0b294d40b3 100644 --- a/src/jrd/blb.h +++ b/src/jrd/blb.h @@ -118,6 +118,12 @@ class blb : public pool_alloc void BLB_put_segment(thread_db*, const void*, USHORT); static void put_slice(thread_db*, jrd_tra*, bid*, const UCHAR*, USHORT, const UCHAR*, SLONG, UCHAR*); static void release_array(ArrayField*); + + // Release ArrayFields created on the request-less fast-path (arr_request + // == nullptr), e.g. arrays belonging to records skipped after a conversion + // failure. Safe to call after a batch: future batches have not created their + // arrays yet, so all remaining request-less arrays belong to processed records. + static void releaseRequestlessArrays(jrd_tra* transaction); static void scalar(thread_db*, jrd_tra*, const bid*, USHORT, const SLONG*, impure_value*); static void delete_blob_id(thread_db*, const bid*, ULONG, Jrd::jrd_rel*); diff --git a/src/jrd/btr.cpp b/src/jrd/btr.cpp index 3ca62e6ff5a..e3b58eeece6 100644 --- a/src/jrd/btr.cpp +++ b/src/jrd/btr.cpp @@ -502,12 +502,12 @@ IndexCondition::IndexCondition(thread_db* tdbb, index_desc* idx) if (!(idx->idx_flags & idx_condition)) return; - fb_assert(idx->idx_condition_node); - m_condition = idx->idx_condition_node; + fb_assert(idx->idx_code && idx->idx_code->condition); + m_condition = idx->idx_code->condition; - fb_assert(idx->idx_condition_statement); + fb_assert(idx->idx_code->condition_statement); const auto orgRequest = tdbb->getRequest(); - m_request = idx->idx_condition_statement->findRequest(tdbb, true); + m_request = idx->idx_code->condition_statement->findRequest(tdbb, true); if (!m_request) ERR_post(Arg::Gds(isc_random) << "Attempt to evaluate index condition recursively"); @@ -605,12 +605,12 @@ IndexExpression::IndexExpression(thread_db* tdbb, index_desc* idx) if (!(idx->idx_flags & idx_expression)) return; - fb_assert(idx->idx_expression_node); - m_expression = idx->idx_expression_node; + fb_assert(idx->idx_code->expression); + m_expression = idx->idx_code->expression; - fb_assert(idx->idx_expression_statement); + fb_assert(idx->idx_code->expression_statement); const auto orgRequest = tdbb->getRequest(); - m_request = idx->idx_expression_statement->findRequest(tdbb, true); + m_request = idx->idx_code->expression_statement->findRequest(tdbb, true); if (!m_request) ERR_post(Arg::Gds(isc_random) << "Attempt to evaluate index expression recursively"); @@ -1691,10 +1691,7 @@ bool BTR_description(thread_db* tdbb, Cached::Relation* relation, const index_ro idx->idx_foreign_dep.clear(); idx->idx_primary_relation = 0; idx->idx_primary_index = 0; - idx->idx_expression_node = nullptr; - idx->idx_expression_statement = nullptr; - idx->idx_condition_node = nullptr; - idx->idx_condition_statement = nullptr; + idx->idx_code = nullptr; idx->idx_fraction = 1.0; idx->idx_state = irt_desc->getState(); @@ -1714,11 +1711,17 @@ bool BTR_description(thread_db* tdbb, Cached::Relation* relation, const index_ro ISC_STATUS error = 0; if (idx->idx_flags & (idx_expression | idx_condition)) { - auto* idp = relation->ensureIndex(tdbb, idx->idx_id); - if (idp) - idp->lookupIndexCode(tdbb, relation, idx, irt_desc); + auto* idv = relation->lookup_index(tdbb, idx->idx_id, CacheFlag::AUTOCREATE); + if (idv) + idv->lookupIndexCode(tdbb, relation, idx, irt_desc); + else + { + auto* idp = relation->ensureIndex(tdbb, idx->idx_id); + fb_assert(idp); + idx->idx_code = idp->refreshIndexCode(tdbb, relation, irt_desc, nullptr); + } - if (idx->idx_flags & idx_expression && !idx->idx_expression_node) + if (idx->idx_flags & idx_expression && !idx->idx_code->expression) { if (tdbb->tdbb_flags & TDBB_sweeper) return false; @@ -1726,7 +1729,7 @@ bool BTR_description(thread_db* tdbb, Cached::Relation* relation, const index_ro // Definition of index expression is not found for index @1 error = isc_idx_expr_not_found; } - else if (idx->idx_flags & idx_condition && !idx->idx_condition_node) + else if (idx->idx_flags & idx_condition && !idx->idx_code->condition) { if (tdbb->tdbb_flags & TDBB_sweeper) return false; @@ -2382,9 +2385,9 @@ USHORT BTR_key_length(thread_db* tdbb, jrd_rel* relation, index_desc* idx) default: if (idx->idx_flags & idx_expression) { - fb_assert(idx->idx_expression_node); - length = idx->idx_expression_desc.dsc_length; - if (idx->idx_expression_desc.dsc_dtype == dtype_varying) + fb_assert(idx->idx_code->expression); + length = idx->idx_code->expression_desc.dsc_length; + if (idx->idx_code->expression_desc.dsc_dtype == dtype_varying) { length = length - sizeof(SSHORT); } diff --git a/src/jrd/btr.h b/src/jrd/btr.h index 41f3092471a..4d5b5398018 100644 --- a/src/jrd/btr.h +++ b/src/jrd/btr.h @@ -55,6 +55,7 @@ class PartitionedSort; struct sort_key_def; struct record_param; struct win; +struct IndexCode; // Dependencies from/to foreign references @@ -95,11 +96,7 @@ struct index_desc MetaId idx_primary_relation; // id for primary key partner relation USHORT idx_count; // number of keys dep idx_foreign_dep; // foreign key partner - ValueExprNode* idx_expression_node; // node tree for indexed expression - dsc idx_expression_desc; // descriptor for expression result - Statement* idx_expression_statement; // stored statement for expression evaluation - BoolExprNode* idx_condition_node; // node tree for index condition - Statement* idx_condition_statement; // stored statement for index condition + IndexCode* idx_code; // support of expression / condition index float idx_fraction; // fraction of keys included in the index UCHAR idx_state; // state from irt_rpt // This structure should exactly match IRTD structure for current ODS diff --git a/src/jrd/build_no.h b/src/jrd/build_no.h index 78f3e331686..afd461dfe3f 100644 --- a/src/jrd/build_no.h +++ b/src/jrd/build_no.h @@ -3,16 +3,16 @@ *** DO NOT EDIT *** TO CHANGE ANY INFORMATION IN HERE PLEASE EDIT src/misc/writeBuildNum.sh - FORMAL BUILD NUMBER:2178 + FORMAL BUILD NUMBER:2191 */ -#define PRODUCT_VER_STRING "6.0.0.2178" -#define FILE_VER_STRING "WI-T6.0.0.2178" -#define LICENSE_VER_STRING "WI-T6.0.0.2178" -#define FILE_VER_NUMBER 6, 0, 0, 2178 +#define PRODUCT_VER_STRING "6.0.0.2191" +#define FILE_VER_STRING "WI-T6.0.0.2191" +#define LICENSE_VER_STRING "WI-T6.0.0.2191" +#define FILE_VER_NUMBER 6, 0, 0, 2191 #define FB_MAJOR_VER "6" #define FB_MINOR_VER "0" #define FB_REV_NO "0" -#define FB_BUILD_NO "2178" +#define FB_BUILD_NO "2191" #define FB_BUILD_TYPE "T" #define FB_BUILD_SUFFIX "Firebird 6.0 Initial" diff --git a/src/jrd/dfw.epp b/src/jrd/dfw.epp index 9b1b5b375ac..784577aa18a 100644 --- a/src/jrd/dfw.epp +++ b/src/jrd/dfw.epp @@ -421,6 +421,7 @@ static bool clear_cache(thread_db*, SSHORT, DeferredWork*, jrd_tra*); static bool change_repl_state(thread_db*, SSHORT, DeferredWork*, jrd_tra*); static bool set_statistics(thread_db*, SSHORT, DeferredWork*, jrd_tra*); static bool deps_to_disk(thread_db*, SSHORT, DeferredWork*, jrd_tra*); +static bool update_dependencies(thread_db*, SSHORT, DeferredWork*, jrd_tra*); // ---------------------------------------------------------------- @@ -1221,6 +1222,7 @@ static inline constexpr deferred_task task_table[] = { dfw_modify_package_constant, createOrAlterConstant }, { dfw_delete_package_constant, deleteConstant }, { dfw_create_package, createPackage }, + { dfw_update_dependencies, update_dependencies }, // must be last { dfw_null, NULL } }; @@ -3477,6 +3479,46 @@ static bool delete_collation(thread_db* tdbb, SSHORT phase, DeferredWork* work, } +static bool update_dependencies(thread_db* tdbb, SSHORT phase, DeferredWork* work, jrd_tra* transaction) +{ +/******************************************* + * + * u p d a t e _ d e p e n d e n c i e s + * + ******************************************* + * + * Functional description + * Update dependent from changed in this transaction objects + * and commit them at specific DFW phase. + * + **************************************/ + SET_TDBB(tdbb); + + switch (phase) + { + case 0: + return false; + + case 1: + case 2: + case 3: + case 4: + case 5: + return true; + + case 6: + transaction->processUpdates(tdbb); + return true; + + case 7: + transaction->processCommits(tdbb); + break; + } + + return false; +} + + static bool delete_parameter(thread_db* tdbb, SSHORT phase, DeferredWork*, jrd_tra*) { /************************************** @@ -3534,12 +3576,7 @@ static bool create_index(thread_db* tdbb, SSHORT phase, DeferredWork* work, jrd_ * Create a new index or change the state of an index between active/inactive. * **************************************/ - jrd_rel* relation = nullptr; - SET_TDBB(tdbb); - Jrd::Attachment* attachment = tdbb->getAttachment(); - Database* dbb = tdbb->getDatabase(); - AutoRequest request; switch (phase) { @@ -4069,6 +4106,7 @@ static bool delete_rfr(thread_db* tdbb, SSHORT phase, DeferredWork* work, jrd_tr CROSS RFLD IN RDB$RELATION_FIELDS WITH RFLD.RDB$SCHEMA_NAME EQ REL.RDB$SCHEMA_NAME AND RFLD.RDB$PACKAGE_NAME EQUIV REL.RDB$PACKAGE_NAME AND + RFLD.RDB$RELATION_NAME EQ REL.RDB$RELATION_NAME AND REL.RDB$RELATION_ID EQ work->dfw_id { field_count++; diff --git a/src/jrd/event.cpp b/src/jrd/event.cpp index 59b8139f9ad..38a749ac2d6 100644 --- a/src/jrd/event.cpp +++ b/src/jrd/event.cpp @@ -66,12 +66,38 @@ using namespace Firebird; namespace Jrd { +// Check that the event parameter block is well formed before +// changing the shared global region. Each item shall completely +// fit into the buffer: one byte of the name length, the name +// itself and four bytes of the prior count. +static void validateEpb(USHORT eventsLength, const UCHAR* events) +{ + const UCHAR* p = events + 1; + const UCHAR* const end = events + eventsLength; + + while (p < end) + { + if (end - p < 5 || p[0] > end - p - 5) + Arg::Gds(isc_bad_epb_form).raise(); + + p += 1 + p[0] + 4; + } +} + + void EventManager::init(Attachment* attachment) { Database* const dbb = attachment->att_database; + EventManager* const eventMgr = dbb->eventManager(); - if (!attachment->att_event_session) - attachment->att_event_session = dbb->eventManager()->create_session(); + SLONG expected = attachment->att_event_session; + if (expected) + return; + + const SLONG session = eventMgr->create_session(); + + if (!attachment->att_event_session.compare_exchange_strong(expected, session)) + eventMgr->deleteSession(session); // another thread won the race } @@ -212,6 +238,10 @@ SLONG EventManager::queEvents(SLONG session_id, Arg::Gds(isc_bad_epb_form).raise(); } + // Validate the EPB up front to avoid any changes of the shared global + // region in case of a malformed buffer + validateEpb(events_length, events); + acquire_shmem(); // Allocate request block diff --git a/src/jrd/event_proto.h b/src/jrd/event_proto.h index 7ab5ac91e5d..db2a240e369 100644 --- a/src/jrd/event_proto.h +++ b/src/jrd/event_proto.h @@ -49,6 +49,7 @@ class EventManager final : public Firebird::GlobalStorage, public Firebird::IpcO static void init(Attachment*); void deleteSession(SLONG); + SLONG create_session(); SLONG queEvents(SLONG, USHORT, const UCHAR*, Firebird::IEventCallback*); void cancelEvents(SLONG); @@ -68,7 +69,6 @@ class EventManager final : public Firebird::GlobalStorage, public Firebird::IpcO void acquire_shmem(); frb* alloc_global(UCHAR type, ULONG length, bool recurse); void create_process(); - SLONG create_session(); void delete_event(evnt*); void delete_process(SLONG); void delete_request(evt_req*); diff --git a/src/jrd/idx.cpp b/src/jrd/idx.cpp index 24941dd9ae4..de1361e4caf 100644 --- a/src/jrd/idx.cpp +++ b/src/jrd/idx.cpp @@ -596,24 +596,12 @@ class IndexCreateTask : public Task } FbLocalStatus status; - if (m_tra || m_idx.idx_expression_statement || m_idx.idx_condition_statement) + if (m_tra) { BackgroundContextHolder tdbb(att->att_database, att, &status, FB_FUNCTION); if (m_tra) TRA_commit(tdbb, m_tra, false); - - if (m_idx.idx_expression_statement) - { - m_idx.idx_expression_statement->release(tdbb); - m_idx.idx_expression_statement = NULL; - } - - if (m_idx.idx_condition_statement) - { - m_idx.idx_condition_statement->release(tdbb); - m_idx.idx_condition_statement = NULL; - } } WorkerAttachment::releaseAttachment(&status, m_attStable); @@ -665,15 +653,6 @@ class IndexCreateTask : public Task if (!m_sort) { m_idx = *creation->index; // copy - if (m_ownAttach) - { - m_idx.idx_expression_node = NULL; - m_idx.idx_expression_statement = NULL; - m_idx.idx_foreign_dep.clear(); - m_idx.idx_condition_node = NULL; - m_idx.idx_condition_statement = NULL; - } - FPTR_REJECT_DUP_CALLBACK callback = NULL; void* callback_arg = NULL; @@ -815,32 +794,43 @@ bool IndexCreateTask::handler(WorkItem& _item) partner_index_id = idx->idx_primary_index; } - if ((idx->idx_flags & idx_expression) && (idx->idx_expression_node == NULL)) + // Assertion checks block + if (idx->idx_flags & (idx_expression | idx_condition)) + fb_assert(idx->idx_code); + if (idx->idx_flags & idx_expression) + fb_assert(idx->idx_code->expression); + if (idx->idx_flags & idx_condition) + fb_assert(idx->idx_code->condition); +/* + if ((idx->idx_flags & (idx_expression|idx_condition)) && (idx->idx_code == NULL)) + idx->idx_code = FB_NEW_POOL(*dbb->dbb_permanent) IndexCode(); + + if ((idx->idx_flags & idx_expression) && (idx->idx_code->expression == NULL)) { fb_assert(!m_exprBlob.isEmpty()); CompilerScratch* csb = NULL; Jrd::ContextPoolHolder context(tdbb, dbb->createPool()); - idx->idx_expression_node = static_cast (MET_parse_blob(tdbb, &relation->getName().schema, - getPermanent(relation), &m_exprBlob, &csb, &idx->idx_expression_statement, false, false)); + idx->idx_code->expression = static_cast (MET_parse_blob(tdbb, &relation->getName().schema, + getPermanent(relation), &m_exprBlob, &csb, &idx->idx_code->expression_statement, false, false)); delete csb; } - if ((idx->idx_flags & idx_condition) && (idx->idx_condition_node == NULL)) + if ((idx->idx_flags & idx_condition) && (idx->idx_code->condition == NULL)) { fb_assert(!m_condBlob.isEmpty()); CompilerScratch* csb = NULL; Jrd::ContextPoolHolder context(tdbb, dbb->createPool()); - idx->idx_condition_node = static_cast (MET_parse_blob(tdbb, &relation->getName().schema, - getPermanent(relation), &m_condBlob, &csb, &idx->idx_condition_statement, false, false)); + idx->idx_code->condition = static_cast (MET_parse_blob(tdbb, &relation->getName().schema, + getPermanent(relation), &m_condBlob, &csb, &idx->idx_code->condition_statement, false, false)); delete csb; } - +*/ RecordStack stack(*transaction->tra_pool), free(*transaction->tra_pool); if (m_flags & IS_LARGE_SCAN) @@ -2044,7 +2034,7 @@ static bool cmpRecordKeys(thread_db* tdbb, // So we must save the first result into another dsc. tempDesc = *idxDesc; - const auto idxDscLength = idx2->idx_expression_desc.dsc_length; + const auto idxDscLength = idx2->idx_code->expression_desc.dsc_length; tempDesc.dsc_address = tmp.getBuffer(idxDscLength + FB_DOUBLE_ALIGN); tempDesc.dsc_address = FB_ALIGN(tempDesc.dsc_address, FB_DOUBLE_ALIGN); fb_assert(idxDesc->dsc_length <= idxDscLength); diff --git a/src/jrd/jrd.cpp b/src/jrd/jrd.cpp index 063fc5ac05a..fdb63403818 100644 --- a/src/jrd/jrd.cpp +++ b/src/jrd/jrd.cpp @@ -3218,9 +3218,10 @@ JAttachment* JProvider::createDatabase(CheckStatusWrapper* user_status, const ch // Initialize TIP cache dbb->dbb_tip_cache = TipCache::create(tdbb); - // Init complete - we can release dbInitMutex + // Init complete - we can release dbInitMutex & dbb_sync dbb->dbb_flags &= ~(DBB_new | DBB_creating); guardDbInit.leave(); + dbbGuard.unlock(); REPL_attach(tdbb, false); diff --git a/src/jrd/met.epp b/src/jrd/met.epp index dc37d8225d7..b72b5743daf 100644 --- a/src/jrd/met.epp +++ b/src/jrd/met.epp @@ -2224,9 +2224,79 @@ ElementBase::ReturnedId MetadataCache::lookup_index_name(thread_db* tdbb, const return id; } - -void IndexPermanent::refreshIndexCode(thread_db* tdbb, Cached::Relation* relation, index_desc* idx, +/* +IndexCode* IndexVersion::refreshIndexCode(thread_db* tdbb, Cached::Relation* relation, const Ods::index_root_page::irt_repeat* irt_desc) +{ + auto* code = perm->refreshIndexCode(tdbb, relation, irt_desc); + + if (code) + { + auto* idv = relation->lookup_index(tdbb, getId(), CacheFlag::AUTOCREATE); + if (!idv->idv_code) + { + code->addRef(); + idv->idv_code = code; + } + } + + return code; +} + +*/ +IndexCode* IndexPermanent::refreshIndexCode(thread_db* tdbb, RelationPermanent* relation, + const Ods::index_root_page::irt_repeat& irt_desc) +{ + SET_TDBB(tdbb); + IndexCode* code = nullptr; + + MutexLockGuard g(idp_code_mutex, FB_FUNCTION); +/* + if (iCode && irt_desc) + { + if ((idp_state == Ods::irt_unused) && // permanent index block in cache just created + (getTraNum() == 0) && // recheck just created fact + (irt_desc->getState() == Ods::irt_normal)) // no current DDL activity on index + { + setTraNum(irt_desc->getTransaction()); + setState(Ods::irt_normal); + return iCode; + } + } +*/ + if ((irt_desc.getState() != idp_state) || (irt_desc.getTransaction() != idp_tranum)) + { + if (irt_desc.getState() == Ods::) + { + relation->newIndexVersion(tdbb, getId()); + auto* idv = relation->lookup_index(tdbb, getId(), CacheFlag::AUTOCREATE); + return + + // Переделать в buildIndexCode который вызывать из lookupIndexCode и Index::scan() + + if (!idv->idv_code) + { + code->addRef(); + idv->idv_code = code; + } + } + + if (irt_desc) + { + idp_state = irt_desc.getState(); + idp_tranum = irt_desc.getTransaction(); + } + + if (code) + { + auto* old_code = idp_latest_code; + idp_latest_code = code; + if (old_code) + IndexCode::release(tdbb, old_code); + } + + +void IndexPermanent::buildIndexCode(thread_db* tdbb, Cached::Relation* relation) { /*********************************************** * @@ -2239,85 +2309,84 @@ void IndexPermanent::refreshIndexCode(thread_db* tdbb, Cached::Relation* relatio * **************************************/ SET_TDBB(tdbb); + Database* dbb = tdbb->getDatabase(); + Attachment* attachment = tdbb->getAttachment(); - MutexLockGuard g(idp_code_mutex, FB_FUNCTION); + bid expression, condition; + expression.clear(); + condition.clear(); + + MetaId relId = relation->getId(); + + Jrd::ContextPoolHolder context(tdbb, dbb->dbb_permanent); - if ((irt_desc->getState() != idp_state) || (irt_desc->getTransaction() != idp_tranum)) + AUTO_HANDLE(handle); + FOR(REQUEST_HANDLE handle TRANSACTION_HANDLE attachment->getMetaTransaction(tdbb)) + IND IN RDB$INDICES + CROSS REL IN RDB$RELATIONS + WITH IND.RDB$INDEX_ID EQ getId() + 1 AND + REL.RDB$RELATION_ID EQ relId AND + REL.RDB$SCHEMA_NAME EQ IND.RDB$SCHEMA_NAME AND + REL.RDB$PACKAGE_NAME EQUIV IND.RDB$PACKAGE_NAME AND + REL.RDB$RELATION_NAME EQ IND.RDB$RELATION_NAME { - Database* dbb = tdbb->getDatabase(); - Attachment* attachment = tdbb->getAttachment(); + if (!IND.RDB$EXPRESSION_BLR.NULL) + expression = IND.RDB$EXPRESSION_BLR; + if (!IND.RDB$CONDITION_BLR.NULL) + condition = IND.RDB$CONDITION_BLR; + } + END_FOR - bid expression, condition; - expression.clear(); - condition.clear(); + if (!(expression.hasData() || condition.hasData())) + return; - MetaId relId = relation->getId(); + auto* code = FB_NEW_POOL(*dbb->dbb_permanent) IndexCode; + code->expression_bid = expression; + code->condition_bid = condition; - Jrd::ContextPoolHolder context(tdbb, dbb->dbb_permanent); + if (expression.hasData()) + { + MemoryPool* stmtPool = dbb->createPool(); + try + { + Jrd::ContextPoolHolder context(tdbb, stmtPool); - AUTO_HANDLE(handle); - FOR(REQUEST_HANDLE handle) // Use system transaction - IND IN RDB$INDICES - CROSS REL IN RDB$RELATIONS - WITH IND.RDB$INDEX_ID EQ getId() + 1 AND - REL.RDB$RELATION_ID EQ relId AND - REL.RDB$SCHEMA_NAME EQ IND.RDB$SCHEMA_NAME AND - REL.RDB$PACKAGE_NAME EQUIV IND.RDB$PACKAGE_NAME AND - REL.RDB$RELATION_NAME EQ IND.RDB$RELATION_NAME + CompilerScratch* csb = nullptr; + Cleanup cc([csb]() {delete csb;}); + MET_parse_blob(tdbb, &relation->getName().schema, relation, &expression, &csb, nullptr, false, false); + code->expression_statement = + Statement::makeValueExpression(tdbb, code->expression, code->expression_desc, csb, false); + } + catch (const Exception&) { - if (!IND.RDB$EXPRESSION_BLR.NULL) - expression = IND.RDB$EXPRESSION_BLR; - if (!IND.RDB$CONDITION_BLR.NULL) - condition = IND.RDB$CONDITION_BLR; + dbb->deletePool(stmtPool); + throw; } - END_FOR - - releaseStatements(tdbb); - - idp_expression_bid = expression; - idp_condition_bid = condition; + } - if (expression.hasData()) + if (condition.hasData()) + { + MemoryPool* stmtPool = dbb->createPool(); + try { - MemoryPool* stmtPool = dbb->createPool(); - try - { - Jrd::ContextPoolHolder context(tdbb, stmtPool); + Jrd::ContextPoolHolder context(tdbb, stmtPool); - CompilerScratch* csb = nullptr; - Cleanup cc([csb]() {delete csb;}); - MET_parse_blob(tdbb, &relation->getName().schema, relation, &expression, &csb, nullptr, false, false); - idp_expression_statement = Statement::makeValueExpression(tdbb, idp_expression, idp_expression_desc, csb, false); - } - catch (const Exception&) - { - dbb->deletePool(stmtPool); - throw; - } + CompilerScratch* csb = nullptr; + Cleanup cc([csb]() {delete csb;}); + MET_parse_blob(tdbb, &relation->getName().schema, relation, &condition, &csb, nullptr, false, false); + code->condition_statement = Statement::makeBoolExpression(tdbb, code->condition, csb, false); } - - if (condition.hasData()) + catch (const Exception&) { - MemoryPool* stmtPool = dbb->createPool(); - try - { - Jrd::ContextPoolHolder context(tdbb, stmtPool); - - CompilerScratch* csb = nullptr; - Cleanup cc([csb]() {delete csb;}); - MET_parse_blob(tdbb, &relation->getName().schema, relation, &condition, &csb, nullptr, false, false); - idp_condition_statement = Statement::makeBoolExpression(tdbb, idp_condition, csb, false); - } - catch (const Exception&) - { - dbb->deletePool(stmtPool); - throw; - } + dbb->deletePool(stmtPool); + throw; } - - idp_state = irt_desc->getState(); - idp_tranum = irt_desc->getTransaction(); } + + auto* old_code = idp_latest_code; + idp_latest_code = code; + if (old_code) + IndexCode::release(tdbb, old_code); } @@ -5158,14 +5227,16 @@ ScanResult IndexVersion::scan(thread_db* tdbb, ObjectBase::Flag flags) } END_FOR - if (idv_name.isEmpty()) + if (idv_name.isEmpty() || !found) { idv_active = MET_index_inactive; return ScanResult::MISS; } + auto* code = buildIndexCode(tdbb, MetadataCache::getPerm(tdbb, relId, 0), nullptr); + replaceCodeConditional(tdbb, code); - return found ? ScanResult::COMPLETE : ScanResult::MISS; + return ScanResult::COMPLETE; } void IndexVersion::setLtt(thread_db* tdbb, const QualifiedName& name, bool unique, bool descending, @@ -5657,3 +5728,137 @@ jrd_rel* MetadataCache::getLtt(thread_db* tdbb, const QualifiedName& name) return nullptr; } + + +void ElementBase::fillDeps(thread_db* tdbb, bool forceRecompile) +{ + auto* attachment = tdbb->getAttachment(); + auto* metaTransaction = attachment->getMetaTransaction(tdbb); + auto* transaction = tdbb->getTransaction(); + + ObjectType objType = getObjectType(); + QualifiedName name; + getObjectName(name); + + MetaName object = name.object; + if (name.package.hasData()) + { + objType = obj_package_header; + object = name.package; + } + + AUTO_HANDLE(hndl); + + FOR (REQUEST_HANDLE hndl TRANSACTION_HANDLE metaTransaction) + D IN RDB$DEPENDENCIES + WITH D.RDB$DEPENDED_ON_TYPE EQ objType + AND D.RDB$DEPENDED_ON_SCHEMA_NAME EQ name.schema.c_str() + AND D.RDB$DEPENDED_ON_NAME EQ object.c_str() + REDUCED TO D.RDB$DEPENDENT_TYPE, D.RDB$DEPENDENT_SCHEMA_NAME, D.RDB$PACKAGE_NAME, D.RDB$DEPENDENT_NAME + { + ElementBase* element = nullptr; + AutoRequest request; + + QualifiedName name; + name.schema = D.RDB$DEPENDENT_SCHEMA_NAME; + if (!D.RDB$PACKAGE_NAME.NULL) + name.package = D.RDB$PACKAGE_NAME; + name.object = D.RDB$DEPENDENT_NAME; + + switch (D.RDB$DEPENDENT_TYPE) + { + case obj_procedure: + element = MetadataCache::getPerm(tdbb, name, CacheFlag::AUTOCREATE); + break; + + case obj_udf: + element = MetadataCache::getPerm(tdbb, name, CacheFlag::AUTOCREATE); + break; + + case obj_relation: + case obj_view: + element = MetadataCache::getPerm(tdbb, name, CacheFlag::AUTOCREATE); + break; + + case obj_trigger: + { + // Distinguish between triggers on table and database-wide + FOR (REQUEST_HANDLE request TRANSACTION_HANDLE metaTransaction) + TRG IN RDB$TRIGGERS + WITH TRG.RDB$SCHEMA_NAME EQ D.RDB$DEPENDENT_SCHEMA_NAME AND + TRG.RDB$TRIGGER_NAME EQ D.RDB$DEPENDENT_NAME + { + if (TRG.RDB$RELATION_NAME.NULL) + element = MetadataCache::get(tdbb)->getTriggersSet(tdbb, TRG.RDB$TRIGGER_TYPE); + else + { + name.object = TRG.RDB$RELATION_NAME; + name.schema = TRG.RDB$SCHEMA_NAME; + name.package = ""; + element = MetadataCache::getPerm(tdbb, name, CacheFlag::AUTOCREATE); + } + } + END_FOR + } + break; + + case obj_computed: + { + // should refresh ralations that have such computed fields + FOR (REQUEST_HANDLE request TRANSACTION_HANDLE metaTransaction) + FLD IN RDB$RELATION_FIELDS + WITH FLD.RDB$FIELD_SOURCE EQ D.RDB$DEPENDENT_NAME + AND FLD.RDB$FIELD_SOURCE_SCHEMA_NAME EQ D.RDB$DEPENDENT_SCHEMA_NAME + { + name.object = FLD.RDB$RELATION_NAME; + name.schema = FLD.RDB$SCHEMA_NAME; + name.package = ""; + auto e = MetadataCache::getPerm(tdbb, name, CacheFlag::AUTOCREATE); + if (e) + transaction->storeUpdate(e, forceRecompile); + } + END_FOR + } + break; + + case obj_index_expression: + case obj_index_condition: + { + // should reload that index + + // skip dependencies from relation - expect to be processed in DDL node + if (D.RDB$DEPENDED_ON_TYPE == obj_relation) + break; + + FOR (REQUEST_HANDLE request TRANSACTION_HANDLE metaTransaction) + IND IN RDB$INDICES + WITH IND.RDB$INDEX_NAME EQ D.RDB$DEPENDENT_NAME + AND IND.RDB$SCHEMA_NAME EQ D.RDB$DEPENDENT_SCHEMA_NAME + AND IND.RDB$PACKAGE_NAME EQUIV NULLIF(D.RDB$PACKAGE_NAME, "") + { + name.object = IND.RDB$RELATION_NAME; + name.schema = IND.RDB$SCHEMA_NAME; + name.package = IND.RDB$PACKAGE_NAME.NULL ? "" : IND.RDB$PACKAGE_NAME; + auto rel = MetadataCache::getPerm(tdbb, name, CacheFlag::AUTOCREATE); + if (rel) + { + fb_assert(IND.RDB$INDEX_ID > 0); + auto index = rel->lookupIndex(tdbb, IND.RDB$INDEX_ID - 1, CacheFlag::AUTOCREATE); + printf("Process index\n"); + transaction->storeUpdate(index, forceRecompile); + } + } + END_FOR + } + break; + + default: + break; + } + + if (element) + transaction->storeUpdate(element, forceRecompile); + } + END_FOR +} + diff --git a/src/jrd/met.h b/src/jrd/met.h index cbbb1b6bb90..2a15d322ade 100644 --- a/src/jrd/met.h +++ b/src/jrd/met.h @@ -141,7 +141,7 @@ class jrd_prc : public Routine } public: - int getObjectType() const noexcept override + ObjectType getObjectType() const noexcept override { return obj_procedure; } diff --git a/src/jrd/optimizer/InnerJoin.cpp b/src/jrd/optimizer/InnerJoin.cpp index e52c02086f4..baee2852754 100644 --- a/src/jrd/optimizer/InnerJoin.cpp +++ b/src/jrd/optimizer/InnerJoin.cpp @@ -214,7 +214,13 @@ void InnerJoin::estimateCost(unsigned position, // its real cardinality during execution. So, unless we have some index-based // filtering applied, let's better be pessimistic and avoid hash joining due to // likely cardinality under-estimation. - const bool avoidHashJoin = (streamCardinality <= MINIMUM_CARDINALITY && !stream->baseIndexes); + bool avoidHashJoin = (streamCardinality <= MINIMUM_CARDINALITY && !stream->baseIndexes); + + // If the user-defined plan is provided and we were able to utilize indices for this retrieval, + // then such indices were explicitly specified in the plan. + // It means the user seems to prefers a loop-join over a hash-join. + if (csb->csb_rpt[stream->number].csb_plan && candidate->indexes && candidate->dependencies) + avoidHashJoin = true; // Consider whether the current stream can be hash-joined to the prior ones. // Beware conditional retrievals, this is impossible for them. @@ -542,6 +548,7 @@ River* InnerJoin::formRiver() // Prepare record sources and corresponding equivalence keys for hash-joining RecordSource* hashJoinRsbs[] = {priorRsb, rsb}; + BoolExprNode* boolean = nullptr; HalfStaticArray keys; keys.add(FB_NEW_POOL(getPool()) NestValueArray(getPool())); @@ -567,6 +574,7 @@ River* InnerJoin::formRiver() keys[1]->add(node2); equiMatches.add(match); + BinaryBoolNode::compose(getPool(), boolean, match); } // Ensure the smallest stream is the one to be hashed, @@ -582,7 +590,7 @@ River* InnerJoin::formRiver() // Create a hash join rsb = FB_NEW_POOL(getPool()) - HashJoin(tdbb, csb, JoinType::INNER, 2, hashJoinRsbs, keys.begin(), stream.selectivity); + HashJoin(tdbb, csb, JoinType::INNER, boolean, 2, hashJoinRsbs, keys.begin(), stream.selectivity); // Clear priorly processed rsb's, as they're already incorporated into a hash join rsbs.clear(); @@ -601,7 +609,7 @@ River* InnerJoin::formRiver() rsb = (rsbs.getCount() == 1) ? rsbs[0] : FB_NEW_POOL(getPool()) NestedLoopJoin(csb, JoinType::INNER, rsbs.getCount(), rsbs.begin()); - // Ensure matching booleans are rechecked early + // Mark matching booleans as used as they're checked inside the hash join if (equiMatches.hasData()) { auto iter = optimizer->getConjuncts(); diff --git a/src/jrd/optimizer/Optimizer.cpp b/src/jrd/optimizer/Optimizer.cpp index 2eccaef6e85..af89857223c 100644 --- a/src/jrd/optimizer/Optimizer.cpp +++ b/src/jrd/optimizer/Optimizer.cpp @@ -265,12 +265,6 @@ namespace } }; - inline void compose(MemoryPool& pool, BoolExprNode** node1, BoolExprNode* node2) - { - if (node2) - *node1 = (*node1) ? FB_NEW_POOL(pool) BinaryBoolNode(pool, blr_and, *node1, node2) : node2; - } - void classMask(unsigned count, ValueExprNode** eq_class, ULONG* mask) { // Given an sort/merge join equivalence class (vector of node pointers @@ -883,7 +877,7 @@ RecordSource* Optimizer::compile(BoolExprNodeStack* parentStack) iter->deterministic(tdbb) && iter->computable(csb, INVALID_STREAM, false)) { - compose(getPool(), &invariantBoolean, iter); + BinaryBoolNode::compose(getPool(), invariantBoolean, iter); iter |= CONJUNCT_USED; } } @@ -1046,7 +1040,6 @@ RecordSource* Optimizer::compile(BoolExprNodeStack* parentStack) // dependent rivers from hashing or sort/merging that happens below. rivers.add(FB_NEW_POOL(getPool()) CrossJoin(this, rivers, JoinType::INNER)); - rivers.back()->activate(csb); } // Now process streams dependent on rivers that are dependent themselves @@ -1064,7 +1057,7 @@ RecordSource* Optimizer::compile(BoolExprNodeStack* parentStack) if (rivers.isEmpty() && dependentRivers.isEmpty()) { // This case may look weird, but it's possible for recursive unions - rsb = FB_NEW_POOL(csb->csb_pool) NestedLoopJoin(csb, JoinType::INNER, 0, nullptr); + rsb = FB_NEW_POOL(csb->csb_pool) DummyJoin(csb); } else { @@ -2792,10 +2785,14 @@ bool Optimizer::generateEquiJoin(RiverList& rivers, JoinType joinType) if (orgCount < 2) return false; + fb_assert(joinType == JoinType::INNER || orgCount == 2); + HalfStaticArray scratch; scratch.grow(baseConjuncts * orgCount); ValueExprNode** classes = scratch.begin(); + BoolExprNode* boolean = nullptr; + // Compute equivalence classes among streams. This involves finding groups // of streams joined by field equalities. @@ -2849,7 +2846,9 @@ bool Optimizer::generateEquiJoin(RiverList& rivers, JoinType joinType) if (eq_class == last_class) last_class += orgCount; + // Mark matching booleans as used as they're checked inside the hash/merge join iter |= Optimizer::CONJUNCT_JOINED; + BinaryBoolNode::compose(getPool(), boolean, *iter); } } } @@ -2976,7 +2975,7 @@ bool Optimizer::generateEquiJoin(RiverList& rivers, JoinType joinType) } finalRsb = FB_NEW_POOL(getPool()) - MergeJoin(csb, rsbs.getCount(), (SortedStream**) rsbs.begin(), keys.begin()); + MergeJoin(csb, boolean, rsbs.getCount(), (SortedStream**) rsbs.begin(), keys.begin()); } else { @@ -2998,7 +2997,7 @@ bool Optimizer::generateEquiJoin(RiverList& rivers, JoinType joinType) rsbs.add(river->getRecordSource()); finalRsb = FB_NEW_POOL(getPool()) - HashJoin(tdbb, csb, joinType, rsbs.getCount(), rsbs.begin(), keys.begin()); + HashJoin(tdbb, csb, joinType, boolean, rsbs.getCount(), rsbs.begin(), keys.begin()); } // Pick up any boolean that may apply @@ -3192,7 +3191,7 @@ RecordSource* Optimizer::generateRetrieval(StreamType stream, if ((inversion && iter->containsStream(stream)) || (!inversion && iter->computable(csb, stream, true))) { - compose(getPool(), &boolean, iter); + BinaryBoolNode::compose(getPool(), boolean, iter); iter |= CONJUNCT_USED; if (!(iter & CONJUNCT_MATCHED)) @@ -3300,7 +3299,7 @@ RecordSource* Optimizer::applyResidualBoolean(RecordSource* rsb) { if (!(iter & CONJUNCT_USED)) { - compose(getPool(), &boolean, iter); + BinaryBoolNode::compose(getPool(), boolean, iter); iter |= CONJUNCT_USED; if (!(iter & (CONJUNCT_MATCHED | CONJUNCT_JOINED))) @@ -3324,7 +3323,7 @@ BoolExprNode* Optimizer::composeBoolean(ConjunctIterator& iter, BooleanList& fil !(iter->nodFlags & ExprNode::FLAG_RESIDUAL) && iter->computable(csb, INVALID_STREAM, false)) { - compose(getPool(), &boolean, iter); + BinaryBoolNode::compose(getPool(), boolean, iter); iter |= CONJUNCT_USED; if (!(iter & (CONJUNCT_MATCHED | CONJUNCT_JOINED))) diff --git a/src/jrd/optimizer/OuterJoin.cpp b/src/jrd/optimizer/OuterJoin.cpp index 0011c3959b1..b8d6376604e 100644 --- a/src/jrd/optimizer/OuterJoin.cpp +++ b/src/jrd/optimizer/OuterJoin.cpp @@ -150,7 +150,7 @@ RecordSource* OuterJoin::generate() RecordSource* OuterJoin::process() { - BoolExprNode* boolean = nullptr; + BoolExprNode* outerBoolean = nullptr; auto& outer = joinStreams[0]; auto& inner = joinStreams[1]; @@ -164,7 +164,7 @@ RecordSource* OuterJoin::process() if (outer.number != INVALID_STREAM) { outerRsb = optimizer->generateRetrieval(outer.number, - optimizer->isFullJoin() ? nullptr : sortPtr, true, false, &boolean); + optimizer->isFullJoin() ? nullptr : sortPtr, true, false, &outerBoolean); } else { @@ -178,7 +178,7 @@ RecordSource* OuterJoin::process() } // Collect booleans computable for the outer sub-stream, it must be active now - boolean = optimizer->composeBoolean(); + outerBoolean = optimizer->composeBoolean(); } fb_assert(outerRsb); @@ -214,5 +214,5 @@ RecordSource* OuterJoin::process() // Allocate and return the join record source - return FB_NEW_POOL(getPool()) NestedLoopJoin(csb, outerRsb, innerRsb, boolean); + return FB_NEW_POOL(getPool()) NestedLoopJoin(csb, outerBoolean, outerRsb, innerRsb); }; diff --git a/src/jrd/optimizer/Retrieval.cpp b/src/jrd/optimizer/Retrieval.cpp index 4ab26cdc2da..245d1399694 100644 --- a/src/jrd/optimizer/Retrieval.cpp +++ b/src/jrd/optimizer/Retrieval.cpp @@ -84,16 +84,16 @@ namespace return newValue; } - bool matchSubset(const BoolExprNode* boolean, const BoolExprNode* sub) + bool matchSubset(const BoolExprNode* boolean, const BoolExprNode* sub, StreamType stream) { - if (boolean->sameAs(sub, true)) + if (boolean->sameAs(sub, true) && boolean->containsStream(stream)) return true; auto binaryNode = nodeAs(boolean); if (binaryNode && binaryNode->blrOp == blr_or) { - if (matchSubset(binaryNode->arg1, sub) || - matchSubset(binaryNode->arg2, sub)) + if (matchSubset(binaryNode->arg1, sub, stream) || + matchSubset(binaryNode->arg2, sub, stream)) { return true; } @@ -101,8 +101,8 @@ namespace binaryNode = nodeAs(sub); if (binaryNode && binaryNode->blrOp == blr_or) { - if (matchSubset(boolean, binaryNode->arg1) && - matchSubset(boolean, binaryNode->arg2)) + if (matchSubset(boolean, binaryNode->arg1, stream) && + matchSubset(boolean, binaryNode->arg2, stream)) { return true; } @@ -859,9 +859,9 @@ bool Retrieval::betterInversion(const InversionCandidate* inv1, bool Retrieval::checkIndexCondition(index_desc& idx, BooleanList& matches) const { - fb_assert(idx.idx_condition_node); + fb_assert(idx.idx_code->condition); - if (!idx.idx_condition_node->containsStream(0, true)) + if (!idx.idx_code->condition->containsStream(0, true)) return false; fb_assert(matches.isEmpty()); @@ -869,7 +869,7 @@ bool Retrieval::checkIndexCondition(index_desc& idx, BooleanList& matches) const auto iter = optimizer->getConjuncts(outerFlag, innerFlag); BoolExprNodeStack idxConjuncts; - const auto conjunctCount = optimizer->decomposeBoolean(idx.idx_condition_node, idxConjuncts); + const auto conjunctCount = optimizer->decomposeBoolean(idx.idx_code->condition, idxConjuncts); fb_assert(conjunctCount); idx.idx_fraction = MAXIMUM_SELECTIVITY; @@ -888,7 +888,7 @@ bool Retrieval::checkIndexCondition(index_desc& idx, BooleanList& matches) const if (!iter->containsStream(stream)) continue; - if (matchSubset(boolean, *iter)) + if (matchSubset(boolean, *iter, stream)) { matches.add(*iter); break; @@ -911,15 +911,23 @@ bool Retrieval::checkIndexCondition(index_desc& idx, BooleanList& matches) const const auto cmpNode = nodeAs(*iter); if (cmpNode && cmpNode->blrOp != blr_equiv) { - if (cmpNode->arg1->sameAs(missingNode->arg, true) || - cmpNode->arg2->sameAs(missingNode->arg, true)) + if (cmpNode->arg1->sameAs(missingNode->arg, true) && + cmpNode->arg1->containsStream(stream)) + { + matches.add(*iter); + break; + } + + if (cmpNode->arg2->sameAs(missingNode->arg, true) && + cmpNode->arg2->containsStream(stream)) { matches.add(*iter); break; } if (cmpNode->arg3 && - cmpNode->arg3->sameAs(missingNode->arg, true)) + cmpNode->arg3->sameAs(missingNode->arg, true) && + cmpNode->arg3->containsStream(stream)) { matches.add(*iter); break; @@ -936,11 +944,11 @@ bool Retrieval::checkIndexCondition(index_desc& idx, BooleanList& matches) const bool Retrieval::checkIndexExpression(const index_desc* idx, ValueExprNode* node) const { - fb_assert(idx && idx->idx_expression_node); + fb_assert(idx && idx->idx_code->expression); // The desired expression can be hidden inside a derived expression node, // so try to recover it (see CORE-4118). - while (!idx->idx_expression_node->sameAs(node, true)) + while (!idx->idx_code->expression->sameAs(node, true)) { const auto derivedExpr = nodeAs(node); const auto cast = nodeAs(node); @@ -955,7 +963,7 @@ bool Retrieval::checkIndexExpression(const index_desc* idx, ValueExprNode* node) // Check the index for matching both the given stream and the given expression tree - return idx->idx_expression_node->containsStream(0, true) && + return idx->idx_code->expression->containsStream(0, true) && node->containsStream(stream, true); } @@ -1449,7 +1457,7 @@ InversionCandidate* Retrieval::makeInversion(InversionCandidateList& inversions) // for a retrieval. Internal (system) requests used by the engine itself are // often optimized using zero or non-actual statistics, so they are processed // using somewhat relaxed rules. - const bool customPlan = csb->csb_rpt[stream].csb_plan; + const bool customPlan = (csb->csb_rpt[stream].csb_plan != nullptr); const bool sysRequest = (csb->csb_g_flags & csb_internal); double totalSelectivity = MAXIMUM_SELECTIVITY; // worst selectivity @@ -1493,7 +1501,7 @@ InversionCandidate* Retrieval::makeInversion(InversionCandidateList& inversions) for (auto otherInversion : inversions) { if (otherInversion->boolean && - idx->idx_condition_node->sameAs(otherInversion->boolean, true)) + idx->idx_code->condition->sameAs(otherInversion->boolean, true)) { otherInversion->used = true; } @@ -1798,7 +1806,7 @@ bool Retrieval::matchBoolean(IndexScratch* indexScratch, { // If index condition matches the boolean, this should not be // considered a match. Full index scan will be used instead. - if (idx->idx_condition_node->sameAs(boolean, true)) + if (idx->idx_code->condition->sameAs(boolean, true)) return false; } diff --git a/src/jrd/recsrc/HashJoin.cpp b/src/jrd/recsrc/HashJoin.cpp index 8b416c15742..d7fceae8e92 100644 --- a/src/jrd/recsrc/HashJoin.cpp +++ b/src/jrd/recsrc/HashJoin.cpp @@ -249,10 +249,11 @@ class HashJoin::HashTable final : public PermanentStorage }; -HashJoin::HashJoin(thread_db* tdbb, CompilerScratch* csb, JoinType joinType, +HashJoin::HashJoin(thread_db* tdbb, CompilerScratch* csb, + JoinType joinType, NestConst joinBoolean, FB_SIZE_T count, RecordSource* const* args, NestValueArray* const* keys, double selectivity) - : Join(csb, count, joinType), + : Join(csb, count, joinType, joinBoolean), m_subs(csb->csb_pool, count - 1) { fb_assert(count >= 2); @@ -261,10 +262,10 @@ HashJoin::HashJoin(thread_db* tdbb, CompilerScratch* csb, JoinType joinType, } HashJoin::HashJoin(thread_db* tdbb, CompilerScratch* csb, - BoolExprNode* boolean, + NestConst joinBoolean, NestConst outerBoolean, RecordSource* const* args, NestValueArray* const* keys, double selectivity) - : Join(csb, 2, JoinType::OUTER, boolean), + : Join(csb, 2, JoinType::OUTER, joinBoolean, outerBoolean), m_subs(csb->csb_pool, 1) { init(tdbb, csb, 2, args, keys, selectivity); @@ -419,7 +420,7 @@ bool HashJoin::internalGetRecord(thread_db* tdbb) const if (!m_leader.source->getRecord(tdbb)) return false; - if (m_boolean && m_boolean->execute(tdbb, request) != TriState(true)) + if (!checkOuterBoolean(tdbb)) { // The boolean pertaining to the left sub-stream is false // so just join sub-stream to a null valued right sub-stream @@ -639,35 +640,30 @@ ULONG HashJoin::computeHash(thread_db* tdbb, bool HashJoin::fetchRecord(thread_db* tdbb, Impure* impure, FB_SIZE_T stream) const { + Request* const request = tdbb->getRequest(); HashTable* const hashTable = impure->irsb_hash_table; const BufferedStream* const arg = m_subs[stream].buffer; - ULONG position; - if (hashTable->iterate(stream, impure->irsb_leader_hash, position)) - { - arg->locate(tdbb, position); - - if (arg->getRecord(tdbb)) - return true; - } - - if (m_joinType == JoinType::SEMI || m_joinType == JoinType::ANTI) - return false; - while (true) { - if (stream == 0 || !fetchRecord(tdbb, impure, stream - 1)) - return false; - - hashTable->reset(stream, impure->irsb_leader_hash); - - if (hashTable->iterate(stream, impure->irsb_leader_hash, position)) + ULONG position; + while (hashTable->iterate(stream, impure->irsb_leader_hash, position)) { arg->locate(tdbb, position); - if (arg->getRecord(tdbb)) + if (arg->getRecord(tdbb) && checkJoinBoolean(tdbb)) return true; } + + if (m_joinType == JoinType::SEMI || m_joinType == JoinType::ANTI) + break; + + if (stream == 0 || !fetchRecord(tdbb, impure, stream - 1)) + break; + + hashTable->reset(stream, impure->irsb_leader_hash); } + + return false; } diff --git a/src/jrd/recsrc/MergeJoin.cpp b/src/jrd/recsrc/MergeJoin.cpp index 2259731ddad..e8da87c7380 100644 --- a/src/jrd/recsrc/MergeJoin.cpp +++ b/src/jrd/recsrc/MergeJoin.cpp @@ -36,9 +36,9 @@ static const char* const SCRATCH = "fb_merge_"; // Data access: merge join // ----------------------- -MergeJoin::MergeJoin(CompilerScratch* csb, FB_SIZE_T count, - SortedStream* const* args, const NestValueArray* const* keys) - : Join(csb, count, JoinType::INNER), +MergeJoin::MergeJoin(CompilerScratch* csb, NestConst joinBoolean, + FB_SIZE_T count, SortedStream* const* args, const NestValueArray* const* keys) + : Join(csb, count, JoinType::INNER, joinBoolean), m_keys(csb->csb_pool, count) { const size_t size = sizeof(struct Impure) + count * sizeof(Impure::irsb_mrg_repeat); @@ -132,13 +132,173 @@ bool MergeJoin::internalGetRecord(thread_db* tdbb) const if (!(impure->irsb_flags & irsb_open)) return false; - // If there is a record group already formed, fetch the next combination + while (true) + { + // If there is a record group already formed, fetch the next combination + + while (fetchRecord(tdbb, m_args.getCount() - 1)) + { + if (checkJoinBoolean(tdbb)) + return true; + } + + // Once consumed the whole record group, form the new one and return first record from there + + if (!formGroup(tdbb)) + break; + + if (checkJoinBoolean(tdbb)) + return true; + } + + return false; +} + +void MergeJoin::getLegacyPlan(thread_db* tdbb, string& plan, unsigned level) const +{ + level++; + plan += "MERGE ("; + Join::getLegacyPlan(tdbb, plan, level); + plan += ")"; +} + +void MergeJoin::internalGetPlan(thread_db* tdbb, PlanEntry& planEntry, unsigned level, bool recurse) const +{ + planEntry.className = "MergeJoin"; + + planEntry.lines.add().text = "Merge Join " + printType(); + + string extras; + extras.printf(" (keys: %" ULONGFORMAT", total key length: %" ULONGFORMAT")", + m_keys[0]->getCount(), m_args[0]->getKeyLength()); + + planEntry.lines.back().text += extras; + + printOptInfo(planEntry.lines); + + Join::internalGetPlan(tdbb, planEntry, level, recurse); +} + +int MergeJoin::compare(thread_db* tdbb, const NestValueArray* node1, + const NestValueArray* node2) const +{ + const auto request = tdbb->getRequest(); + + const NestConst* ptr1 = node1->begin(); + const NestConst* ptr2 = node2->begin(); + + for (const NestConst* const end = node1->end(); ptr1 != end; ++ptr1, ++ptr2) + { + const auto desc1 = EVL_expr(tdbb, request, *ptr1); + const auto desc2 = EVL_expr(tdbb, request, *ptr2); + + if (!desc1 && desc2) + return -1; + + if (desc1 && !desc2) + return 1; + + if (desc1 && desc2) + { + if (const int result = MOV_compare(tdbb, desc1, desc2)) + return result; + } + } + + return 0; +} + +UCHAR* MergeJoin::getData(thread_db* /*tdbb*/, MergeFile* mfb, SLONG record) const +{ + fb_assert(record >= 0 && record < (SLONG) mfb->mfb_equal_records); + + const ULONG merge_block = record / mfb->mfb_blocking_factor; + if (merge_block != mfb->mfb_current_block) + { + Sort::readBlock(mfb->mfb_space, mfb->mfb_block_size * merge_block, + mfb->mfb_block_data, mfb->mfb_block_size); + mfb->mfb_current_block = merge_block; + } + + const ULONG merge_offset = (record % mfb->mfb_blocking_factor) * mfb->mfb_record_size; + return mfb->mfb_block_data + merge_offset; +} + +SLONG MergeJoin::getRecordByIndex(thread_db* tdbb, FB_SIZE_T index) const +{ + Request* const request = tdbb->getRequest(); + Impure* const impure = request->getImpure(m_impure); + + const SortedStream* const sort_rsb = m_args[index]; + Impure::irsb_mrg_repeat* const tail = &impure->irsb_mrg_rpt[index]; + + const UCHAR* sort_data = sort_rsb->getData(tdbb); + if (!sort_data) + return -1; + + MergeFile* const mfb = &tail->irsb_mrg_file; + const SLONG record = mfb->mfb_equal_records; + + const ULONG merge_block = record / mfb->mfb_blocking_factor; + if (merge_block != mfb->mfb_current_block) + { + if (!mfb->mfb_space) + { + MemoryPool& pool = *getDefaultMemoryPool(); + mfb->mfb_space = FB_NEW_POOL(pool) TempSpace(pool, SCRATCH, false); + } + + Sort::writeBlock(mfb->mfb_space, mfb->mfb_block_size * mfb->mfb_current_block, + mfb->mfb_block_data, mfb->mfb_block_size); + mfb->mfb_current_block = merge_block; + } + + const ULONG merge_offset = (record % mfb->mfb_blocking_factor) * mfb->mfb_record_size; + UCHAR* merge_data = mfb->mfb_block_data + merge_offset; + + memcpy(merge_data, sort_data, sort_rsb->getLength()); + ++mfb->mfb_equal_records; + + return record; +} + +bool MergeJoin::fetchRecord(thread_db* tdbb, FB_SIZE_T index) const +{ + Request* const request = tdbb->getRequest(); + Impure* const impure = request->getImpure(m_impure); + Impure::irsb_mrg_repeat* tail = &impure->irsb_mrg_rpt[index]; + + const SSHORT m = tail->irsb_mrg_order; + tail = &impure->irsb_mrg_rpt[m]; + const SortedStream* const sort_rsb = m_args[m]; + + SLONG record = tail->irsb_mrg_equal_current; + ++record; + + if (record > tail->irsb_mrg_equal_end) + { + if (index == 0 || !fetchRecord(tdbb, index - 1)) + return false; + + record = tail->irsb_mrg_equal; + } + + tail->irsb_mrg_equal_current = record; + + MergeFile* const mfb = &tail->irsb_mrg_file; + sort_rsb->mapData(tdbb, request, getData(tdbb, mfb, record)); - if (fetchRecord(tdbb, m_args.getCount() - 1)) - return true; + return true; +} +bool MergeJoin::formGroup(thread_db* tdbb) const +{ // Assuming we are done with the current value group, advance each // stream one record. If any comes up dry, we're done. + + Request* const request = tdbb->getRequest(); + Impure* const impure = request->getImpure(m_impure); + const NestConst* highest_ptr = m_args.begin(); FB_SIZE_T highest_index = 0; @@ -321,140 +481,3 @@ bool MergeJoin::internalGetRecord(thread_db* tdbb) const return true; } - -void MergeJoin::getLegacyPlan(thread_db* tdbb, string& plan, unsigned level) const -{ - level++; - plan += "MERGE ("; - Join::getLegacyPlan(tdbb, plan, level); - plan += ")"; -} - -void MergeJoin::internalGetPlan(thread_db* tdbb, PlanEntry& planEntry, unsigned level, bool recurse) const -{ - planEntry.className = "MergeJoin"; - - planEntry.lines.add().text = "Merge Join " + printType(); - - string extras; - extras.printf(" (keys: %" ULONGFORMAT", total key length: %" ULONGFORMAT")", - m_keys[0]->getCount(), m_args[0]->getKeyLength()); - - planEntry.lines.back().text += extras; - - printOptInfo(planEntry.lines); - - Join::internalGetPlan(tdbb, planEntry, level, recurse); -} - -int MergeJoin::compare(thread_db* tdbb, const NestValueArray* node1, - const NestValueArray* node2) const -{ - const auto request = tdbb->getRequest(); - - const NestConst* ptr1 = node1->begin(); - const NestConst* ptr2 = node2->begin(); - - for (const NestConst* const end = node1->end(); ptr1 != end; ++ptr1, ++ptr2) - { - const auto desc1 = EVL_expr(tdbb, request, *ptr1); - const auto desc2 = EVL_expr(tdbb, request, *ptr2); - - if (!desc1 && desc2) - return -1; - - if (desc1 && !desc2) - return 1; - - if (desc1 && desc2) - { - if (const int result = MOV_compare(tdbb, desc1, desc2)) - return result; - } - } - - return 0; -} - -UCHAR* MergeJoin::getData(thread_db* /*tdbb*/, MergeFile* mfb, SLONG record) const -{ - fb_assert(record >= 0 && record < (SLONG) mfb->mfb_equal_records); - - const ULONG merge_block = record / mfb->mfb_blocking_factor; - if (merge_block != mfb->mfb_current_block) - { - Sort::readBlock(mfb->mfb_space, mfb->mfb_block_size * merge_block, - mfb->mfb_block_data, mfb->mfb_block_size); - mfb->mfb_current_block = merge_block; - } - - const ULONG merge_offset = (record % mfb->mfb_blocking_factor) * mfb->mfb_record_size; - return mfb->mfb_block_data + merge_offset; -} - -SLONG MergeJoin::getRecordByIndex(thread_db* tdbb, FB_SIZE_T index) const -{ - Request* const request = tdbb->getRequest(); - Impure* const impure = request->getImpure(m_impure); - - const SortedStream* const sort_rsb = m_args[index]; - Impure::irsb_mrg_repeat* const tail = &impure->irsb_mrg_rpt[index]; - - const UCHAR* sort_data = sort_rsb->getData(tdbb); - if (!sort_data) - return -1; - - MergeFile* const mfb = &tail->irsb_mrg_file; - const SLONG record = mfb->mfb_equal_records; - - const ULONG merge_block = record / mfb->mfb_blocking_factor; - if (merge_block != mfb->mfb_current_block) - { - if (!mfb->mfb_space) - { - MemoryPool& pool = *getDefaultMemoryPool(); - mfb->mfb_space = FB_NEW_POOL(pool) TempSpace(pool, SCRATCH, false); - } - - Sort::writeBlock(mfb->mfb_space, mfb->mfb_block_size * mfb->mfb_current_block, - mfb->mfb_block_data, mfb->mfb_block_size); - mfb->mfb_current_block = merge_block; - } - - const ULONG merge_offset = (record % mfb->mfb_blocking_factor) * mfb->mfb_record_size; - UCHAR* merge_data = mfb->mfb_block_data + merge_offset; - - memcpy(merge_data, sort_data, sort_rsb->getLength()); - ++mfb->mfb_equal_records; - - return record; -} - -bool MergeJoin::fetchRecord(thread_db* tdbb, FB_SIZE_T index) const -{ - Request* const request = tdbb->getRequest(); - Impure* const impure = request->getImpure(m_impure); - Impure::irsb_mrg_repeat* tail = &impure->irsb_mrg_rpt[index]; - - const SSHORT m = tail->irsb_mrg_order; - tail = &impure->irsb_mrg_rpt[m]; - const SortedStream* const sort_rsb = m_args[m]; - - SLONG record = tail->irsb_mrg_equal_current; - ++record; - - if (record > tail->irsb_mrg_equal_end) - { - if (index == 0 || !fetchRecord(tdbb, index - 1)) - return false; - - record = tail->irsb_mrg_equal; - } - - tail->irsb_mrg_equal_current = record; - - MergeFile* const mfb = &tail->irsb_mrg_file; - sort_rsb->mapData(tdbb, request, getData(tdbb, mfb, record)); - - return true; -} diff --git a/src/jrd/recsrc/NestedLoopJoin.cpp b/src/jrd/recsrc/NestedLoopJoin.cpp index 715690c333a..a9105c43a6c 100644 --- a/src/jrd/recsrc/NestedLoopJoin.cpp +++ b/src/jrd/recsrc/NestedLoopJoin.cpp @@ -57,9 +57,9 @@ NestedLoopJoin::NestedLoopJoin(CompilerScratch* csb, JoinType joinType, } NestedLoopJoin::NestedLoopJoin(CompilerScratch* csb, - RecordSource* outer, RecordSource* inner, - BoolExprNode* boolean) - : Join(csb, 2, JoinType::OUTER, boolean) + NestConst outerBoolean, + RecordSource* outer, RecordSource* inner) + : Join(csb, 2, JoinType::OUTER, nullptr, outerBoolean) { fb_assert(outer && inner); @@ -205,7 +205,7 @@ bool NestedLoopJoin::internalGetRecord(thread_db* tdbb) const if (!outer->getRecord(tdbb)) return false; - if (m_boolean && m_boolean->execute(tdbb, request) != TriState(true)) + if (!checkOuterBoolean(tdbb)) { // The boolean pertaining to the left sub-stream is false // so just join sub-stream to a null valued right sub-stream diff --git a/src/jrd/recsrc/RecordSource.h b/src/jrd/recsrc/RecordSource.h index a07e808fa43..432ee3ecf21 100644 --- a/src/jrd/recsrc/RecordSource.h +++ b/src/jrd/recsrc/RecordSource.h @@ -1309,11 +1309,15 @@ namespace Jrd class Join : public RecordSource { public: - Join(CompilerScratch* csb, FB_SIZE_T count, JoinType joinType, BoolExprNode* boolean = nullptr) - : RecordSource(csb), m_joinType(joinType), m_boolean(boolean), - m_args(csb->csb_pool, count) - { - fb_assert(!m_boolean || m_joinType == JoinType::OUTER); + Join(CompilerScratch* csb, FB_SIZE_T count, JoinType joinType, + NestConst joinBoolean = nullptr, NestConst outerBoolean = nullptr) + : RecordSource(csb), + m_joinType(joinType), + m_args(csb->csb_pool, count), + m_joinBoolean(joinBoolean), + m_outerBoolean(outerBoolean) + { + fb_assert(!m_outerBoolean || m_joinType == JoinType::OUTER); } void close(thread_db* tdbb) const override @@ -1355,7 +1359,8 @@ namespace Jrd return true; } - return (m_boolean && m_boolean->containsAnyStream(streams)); + return (m_joinBoolean && m_joinBoolean->containsAnyStream(streams)) || + (m_outerBoolean && m_outerBoolean->containsAnyStream(streams)); } void invalidateRecords(Request* request) const override @@ -1416,10 +1421,27 @@ namespace Jrd return ""; } + bool checkJoinBoolean(thread_db* tdbb) const + { + return m_joinBoolean ? + m_joinBoolean->execute(tdbb, tdbb->getRequest()) == Firebird::TriState(true) : + true; + } + + bool checkOuterBoolean(thread_db* tdbb) const + { + return m_outerBoolean ? + m_outerBoolean->execute(tdbb, tdbb->getRequest()) == Firebird::TriState(true) : + true; + } + protected: const JoinType m_joinType; - const NestConst m_boolean; Firebird::Array> m_args; + + private: + const NestConst m_joinBoolean; + const NestConst m_outerBoolean; }; class NestedLoopJoin : public Join @@ -1427,8 +1449,8 @@ namespace Jrd public: NestedLoopJoin(CompilerScratch* csb, JoinType joinType, FB_SIZE_T count, RecordSource* const* args); - NestedLoopJoin(CompilerScratch* csb, RecordSource* outer, RecordSource* inner, - BoolExprNode* boolean); + NestedLoopJoin(CompilerScratch* csb, NestConst outerBoolean, + RecordSource* outer, RecordSource* inner); void close(thread_db* tdbb) const override; void getLegacyPlan(thread_db* tdbb, Firebird::string& plan, unsigned level) const override; @@ -1442,6 +1464,14 @@ namespace Jrd bool fetchRecord(thread_db*, FB_SIZE_T) const; }; + class DummyJoin : public NestedLoopJoin + { + public: + explicit DummyJoin(CompilerScratch* csb) + : NestedLoopJoin(csb, JoinType::INNER, 0, nullptr) + {} + }; + class FullOuterJoin : public Join { public: @@ -1485,13 +1515,14 @@ namespace Jrd }; public: - HashJoin(thread_db* tdbb, CompilerScratch* csb, JoinType joinType, - FB_SIZE_T count, RecordSource* const* args, NestValueArray* const* keys, - double selectivity = 0); HashJoin(thread_db* tdbb, CompilerScratch* csb, - BoolExprNode* boolean, + NestConst joinBoolean, NestConst outerBoolean, RecordSource* const* args, NestValueArray* const* keys, double selectivity = 0); + HashJoin(thread_db* tdbb, CompilerScratch* csb, + JoinType joinType, NestConst joinBoolean, + FB_SIZE_T count, RecordSource* const* args, NestValueArray* const* keys, + double selectivity = 0); void close(thread_db* tdbb) const override; void getLegacyPlan(thread_db* tdbb, Firebird::string& plan, unsigned level) const override; @@ -1546,9 +1577,8 @@ namespace Jrd static const FB_SIZE_T MERGE_BLOCK_SIZE = 65536; public: - MergeJoin(CompilerScratch* csb, FB_SIZE_T count, - SortedStream* const* args, - const NestValueArray* const* keys); + MergeJoin(CompilerScratch* csb, NestConst joinBoolean, + FB_SIZE_T count, SortedStream* const* args, const NestValueArray* const* keys); void close(thread_db* tdbb) const override; void getLegacyPlan(thread_db* tdbb, Firebird::string& plan, unsigned level) const override; @@ -1564,6 +1594,7 @@ namespace Jrd UCHAR* getData(thread_db* tdbb, MergeFile* mfb, SLONG record) const; SLONG getRecordByIndex(thread_db* tdbb, FB_SIZE_T index) const; bool fetchRecord(thread_db* tdbb, FB_SIZE_T index) const; + bool formGroup(thread_db* tdbb) const; Firebird::Array m_keys; }; diff --git a/src/jrd/svc.cpp b/src/jrd/svc.cpp index 88d42ddd573..f08a0b8024f 100644 --- a/src/jrd/svc.cpp +++ b/src/jrd/svc.cpp @@ -3163,6 +3163,8 @@ bool Service::process_switches(ClumpletReader& spb, string& switches) status_exception::raise(Arg::Gds(isc_unexp_spb_form) << Arg::Str(SPB_SEC_USERNAME)); } + // FAST_PATH requires -service; this is enforced in burp.cpp via uSvc->isService(). + // postfixes for burp & nbackup switch (svc_action) { diff --git a/src/jrd/tests/EventManagerTest.cpp b/src/jrd/tests/EventManagerTest.cpp new file mode 100644 index 00000000000..aacae1d22df --- /dev/null +++ b/src/jrd/tests/EventManagerTest.cpp @@ -0,0 +1,389 @@ +/* + * 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) 2026 Adriano dos Santos Fernandes + * and all contributors signed below. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + */ + +#include "firebird.h" +#include "boost/test/unit_test.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../common/classes/fb_string.h" +#include "../common/classes/ImplementHelper.h" +#include "../common/config/config.h" +#include "../common/status.h" +#include "../common/ThreadStart.h" +#include "../jrd/event_proto.h" +#include "../yvalve/gds_proto.h" + +using namespace Firebird; +using namespace Jrd; + + +namespace +{ + +// Wait for a condition with a deadline, polling periodically. +bool waitCond(const std::function& cond, unsigned timeoutMs) +{ + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs); + + while (!cond()) + { + if (std::chrono::steady_clock::now() >= deadline) + return false; + + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } + + return true; +} + +std::string getUniqueId() +{ + static std::atomic counter{0}; + + const auto now = std::chrono::system_clock::now(); + const auto nowNs = std::chrono::duration_cast( + now.time_since_epoch()).count(); + + return "evt_" + std::to_string(nowNs) + "_" + std::to_string(counter.fetch_add(1)); +} + +// Build an EPB (event parameter block) v1 for a single event. +std::vector makeEpb(const std::string& name, SLONG priorCount) +{ + if (name.length() > 255) + BOOST_FAIL("event name is too long"); + + std::vector epb; + epb.push_back(EPB_version1); + epb.push_back(static_cast(name.length())); + epb.insert(epb.end(), name.begin(), name.end()); + + for (int i = 0; i < 4; ++i) + epb.push_back(static_cast((priorCount >> (8 * i)) & 0xFF)); + + return epb; +} + + +class TestEventCallback final : + public RefCntIface > +{ +public: + // Number of times the AST was invoked. + std::atomic calls{0}; + + void eventCallbackFunction(unsigned length, const unsigned char* events) override + { + std::lock_guard lock(mutex); + ++calls; + + if (length >= 1 && events[0] == EPB_version1) + { + const UCHAR* p = events + 1; + const UCHAR* const end = events + length; + + while (p + 1 <= end) + { + const USHORT count = *p++; + if (USHORT(end - p) < count + 4u) + break; + + std::string name(reinterpret_cast(p), count); + p += count; + received.emplace_back(name, gds__vax_integer(p, 4)); + p += 4; + } + } + } + + // Returns the count reported for the given event name, or -1 if not found. + SLONG getCount(const std::string& name) const + { + std::lock_guard lock(mutex); + + for (const auto& item : received) + { + if (item.first == name) + return item.second; + } + + return -1; + } + +private: + mutable std::mutex mutex; + std::vector> received; +}; + + +// Holds the configuration alive for the duration of a test. +struct TestEnv +{ + ConfigFile configFile; + Config config; + + TestEnv() + : configFile(ConfigFile::USE_TEXT, "\n"), + config(configFile) + { + } +}; + + +struct ManagerHolder +{ + TestEnv env; + Firebird::string id; + std::unique_ptr manager; + SLONG session = 0; + + ManagerHolder() + : id(getUniqueId().c_str()) + { + try + { + manager = std::make_unique(id, &env.config); + session = manager->create_session(); + } + catch (const Exception& ex) + { + FbLocalStatus status; + ex.stuffException(&status); + const ISC_STATUS* s = status->getErrors(); + TEXT buffer[1024]; + while (fb_interpret(buffer, sizeof(buffer), &s)) + fprintf(stderr, "EVENTTEST: init failed: %s\n", buffer); + throw; + } + } + + ~ManagerHolder() + { + manager->deleteSession(session); + } +}; + +} // anonymous namespace + + +BOOST_AUTO_TEST_SUITE(EngineSuite) +BOOST_AUTO_TEST_SUITE(EventManagerSuite) + + +BOOST_AUTO_TEST_CASE(BasicRoundTripTest) +{ + ManagerHolder h; + TestEventCallback cb; + + // A request with prior count equal to the current count is satisfied + // immediately and delivered asynchronously without any explicit posting. + const auto epbAuto = makeEpb("EV_AUTO", 0); + h.manager->queEvents(h.session, epbAuto.size(), epbAuto.data(), &cb); + + BOOST_TEST(waitCond([&]() { return cb.calls >= 1; }, 5000)); + BOOST_TEST(cb.getCount("EV_AUTO") == 1); + + // A cancelled request must never be delivered. + const auto epbCancel = makeEpb("EV_CANCEL", 50); + const SLONG cancelId = h.manager->queEvents(h.session, epbCancel.size(), epbCancel.data(), &cb); + BOOST_TEST(cancelId != 0); + h.manager->cancelEvents(cancelId); + + h.manager->postEvent(sizeof("EV_CANCEL") - 1, "EV_CANCEL", 100); + h.manager->deliverEvents(); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + BOOST_TEST(cb.calls.load() == 1); + BOOST_TEST(cb.getCount("EV_CANCEL") == -1); + + // A request satisfied by postEvent is delivered by deliverEvents(). + const auto epbPost = makeEpb("EV_POST", 10); + h.manager->queEvents(h.session, epbPost.size(), epbPost.data(), &cb); + + h.manager->postEvent(sizeof("EV_POST") - 1, "EV_POST", 20); + h.manager->deliverEvents(); + + BOOST_TEST(waitCond([&]() { return cb.getCount("EV_POST") >= 0; }, 5000)); + BOOST_TEST(cb.getCount("EV_POST") == 21); +} + + +BOOST_AUTO_TEST_CASE(MalformedEpbTruncatedTailTest) +{ + // An EPB whose last item declares a name that does not fit into the + // buffer together with the mandatory 4-byte prior count must be rejected. + ManagerHolder h; + TestEventCallback cb; + + auto epb = makeEpb("AAA", 0); + epb.push_back(10); // name length of the last item + epb.push_back(1); // a couple of trailing bytes, far too few + epb.push_back(2); + + bool raised = false; + + try + { + h.manager->queEvents(h.session, epb.size(), epb.data(), &cb); + } + catch (const Exception&) + { + raised = true; + } + + BOOST_TEST(raised); + + // Nothing must be delivered for the malformed request and no residue + // must be left behind. + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + BOOST_TEST(cb.calls.load() == 0); + + // The manager must stay fully functional. + const auto epbGood = makeEpb("EV_AFTER_TRUNCATED", 0); + h.manager->queEvents(h.session, epbGood.size(), epbGood.data(), &cb); + BOOST_TEST(waitCond([&]() { return cb.getCount("EV_AFTER_TRUNCATED") >= 0; }, 5000)); + BOOST_TEST(cb.getCount("EV_AFTER_TRUNCATED") == 1); + BOOST_TEST(cb.calls.load() == 1); +} + + +BOOST_AUTO_TEST_CASE(MalformedEpbOverlongNameTest) +{ + // An EPB declaring an event name longer than the whole buffer must be + // rejected and must leave no partially built request behind. Such a + // leftover request would be considered completed and asynchronously + // deliver a garbage event block to the callback. + ManagerHolder h; + TestEventCallback cb; + + auto epb = makeEpb("BBB", 0); + epb.push_back(250); // bogus name length, exceeds the buffer + + bool raised = false; + + try + { + h.manager->queEvents(h.session, epb.size(), epb.data(), &cb); + } + catch (const Exception&) + { + raised = true; + } + + BOOST_TEST(raised); + + // No garbage delivery may arrive for the malformed request. + std::this_thread::sleep_for(std::chrono::milliseconds(2000)); + BOOST_TEST(cb.calls.load() == 0); + + // The manager must stay fully functional. + const auto epbGood = makeEpb("EV_AFTER_OVERLONG", 0); + h.manager->queEvents(h.session, epbGood.size(), epbGood.data(), &cb); + BOOST_TEST(waitCond([&]() { return cb.getCount("EV_AFTER_OVERLONG") >= 0; }, 5000)); + BOOST_TEST(cb.getCount("EV_AFTER_OVERLONG") == 1); + BOOST_TEST(cb.calls.load() == 1); +} + + +BOOST_AUTO_TEST_CASE(ConcurrentStressTest) +{ + constexpr unsigned THREAD_COUNT = 4u; + constexpr unsigned ITERATION_COUNT = 300u; + + ManagerHolder h; + TestEventCallback cb; + std::atomic expectedCalls{0}; + + std::vector threads; + std::atomic startBarrier{0}; + + for (unsigned threadNum = 0; threadNum < THREAD_COUNT; ++threadNum) + { + threads.emplace_back([&, threadNum]() { + const std::string prefix = "ST_" + std::to_string(threadNum) + "_"; + ULONG localExpected = 0; + + startBarrier.fetch_add(1, std::memory_order_acq_rel); + while (startBarrier.load(std::memory_order_acquire) < THREAD_COUNT) + std::this_thread::yield(); + + for (unsigned i = 0; i < ITERATION_COUNT; ++i) + { + const std::string name = prefix + std::to_string(i); + const auto epb = makeEpb(name, 0); + + switch (i % 3) + { + // Immediately satisfied request, delivered on its own. + case 0: + h.manager->queEvents(h.session, epb.size(), epb.data(), &cb); + ++localExpected; + break; + + // Request satisfied by postEvent and delivered by deliverEvents(). + case 1: + { + const auto epbLate = makeEpb(name, 5); + h.manager->queEvents(h.session, epbLate.size(), epbLate.data(), &cb); + h.manager->postEvent(name.length(), name.c_str(), 10); + h.manager->deliverEvents(); + ++localExpected; + break; + } + + // Cancelled request, never delivered. + case 2: + { + const auto epbCancelled = makeEpb(name, 100); + const SLONG id = h.manager->queEvents(h.session, + epbCancelled.size(), epbCancelled.data(), &cb); + h.manager->cancelEvents(id); + break; + } + } + } + + expectedCalls += localExpected; + }); + } + + for (auto& thread : threads) + thread.join(); + + BOOST_TEST(waitCond([&]() { return cb.calls >= expectedCalls; }, 30000)); + + // Every satisfied request must be delivered exactly once. + h.manager->deliverEvents(); + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + BOOST_TEST(cb.calls.load() == expectedCalls.load()); +} + + +BOOST_AUTO_TEST_SUITE_END() // EventManagerSuite +BOOST_AUTO_TEST_SUITE_END() // EngineSuite diff --git a/src/jrd/tra.cpp b/src/jrd/tra.cpp index a2576184ed2..a103fc22de7 100644 --- a/src/jrd/tra.cpp +++ b/src/jrd/tra.cpp @@ -4415,8 +4415,8 @@ void TraceSweepEvent::report(ntrace_process_state_t state) m_need_trace = false; } -SecDbContext::SecDbContext(IAttachment* a, ITransaction* t) noexcept - : att(a), tra(t), savePoint(0) +SecDbContext::SecDbContext(MemoryPool& p, IAttachment* a, ITransaction* t) noexcept + : att(a), tra(t), savePoint(p) { } SecDbContext::~SecDbContext() @@ -4444,7 +4444,7 @@ SecDbContext* jrd_tra::setSecDbContext(IAttachment* att, ITransaction* tra) { fb_assert(!tra_sec_db_context); - tra_sec_db_context = FB_NEW_POOL(*getDefaultMemoryPool()) SecDbContext(att, tra); + tra_sec_db_context = FB_NEW_POOL(*getDefaultMemoryPool()) SecDbContext(getPool(), att, tra); return tra_sec_db_context; } @@ -4453,3 +4453,148 @@ void jrd_tra::eraseSecDbContext() noexcept delete tra_sec_db_context; tra_sec_db_context = NULL; } + +void jrd_tra::storeUpdate(ElementBase* obj, bool forceRecompile) +{ + if (!accumulatedDeps) + { + accumulatedDeps = FB_NEW_POOL(getPool()) Deps(getPool()); + DFW_post_work(this, dfw_update_dependencies, nullptr, nullptr, 0u); + } + + bool* recompile = accumulatedDeps->get(obj); + if (recompile) + *recompile = forceRecompile || *recompile; + else + { + recompile = accumulatedDeps->put(obj); + *recompile = forceRecompile; + } +} + +void jrd_tra::storeCommit(ElementBase* obj) +{ + if (!updateCommits) + updateCommits = FB_NEW_POOL(getPool()) UpdateCommits(getPool()); + + updateCommits->push(obj); +} + +bool jrd_tra::processUpdates(thread_db* tdbb) +{ + if (!(accumulatedDeps || processingDeps)) + return false; + + MdcVersion startingVersion; + { + VersionIncr incr(tdbb); + startingVersion = incr.getVersion(); + } + + while (accumulatedDeps || processingDeps) + { + // switch deps + if (!processingDeps) + processingDeps = accumulatedDeps.release(); + + // process updates + for(auto iter : *processingDeps) + { + auto* elem = iter.first; + bool use = iter.second; + + // may be this element was already processed in this update? + if (elem->getVersion(tdbb) >= startingVersion) + continue; + + if (!use) + { + // now ask cache element to create all possible requests + try + { + elem->makeRequests(tdbb); + } + + // handle specific for outdated element error + catch (const status_exception& ex) + { + if (ex.value()[1] != isc_old_format) + throw; + tdbb->tdbb_status_vector->init(); + use = true; + } + } + + if (use) + { + // get all existing currently dependencies + elem->fillDeps(tdbb, false); + + // make new version and compile it + elem->makeVersion(tdbb); + + storeCommit(elem); + } + } + + // This portion of update is ready + delete processingDeps.release(); + } + + return true; +} + +void jrd_tra::processCommits(thread_db* tdbb) +{ + if (!updateCommits) + return; + + unsigned pos = 0; + try + { + for (pos = 0; pos < updateCommits->getCount(); pos++) + updateCommits->getElement(pos)->commit(tdbb); + } + catch(const Exception&) + { + // We do not expect exceptions in metacache commits - but + // already committed elements should better go away + if (pos > 0) + updateCommits->removeRange(0, pos); + + throw; + } +} + +void SecDbContext::setSavePoint() +{ + fb_assert(savePoint.isEmpty()); + FbLocalStatus status; + + savePoint.printf("ExecInSecurityDb%d", savePointNumber++); + att->execute(&status, tra, 0, ("SAVEPOINT " + savePoint).c_str(), SQL_DIALECT_V6, NULL, NULL, NULL, NULL); + check(&status); +} + +void SecDbContext::releaseSavePoint() +{ + if (savePoint.hasData()) + { + FbLocalStatus status; + att->execute(&status, tra, 0, ("RELEASE SAVEPOINT " + savePoint).c_str(), + SQL_DIALECT_V6, NULL, NULL, NULL, NULL); + check(&status); + rollbackSavePoint(); + } +} + +void SecDbContext::rollbackSavePoint() noexcept +{ + if (savePoint.hasData()) + { + FbLocalStatus status; + att->execute(&status, tra, 0, ("ROLLBACK TO SAVEPOINT " + savePoint).c_str(), + SQL_DIALECT_V6, NULL, NULL, NULL, NULL); + savePoint.erase(); + } +} diff --git a/src/jrd/tra.h b/src/jrd/tra.h index 0e06f7c3c07..ec94ea85ed8 100644 --- a/src/jrd/tra.h +++ b/src/jrd/tra.h @@ -35,6 +35,8 @@ #include "../include/fb_blk.h" #include "../common/classes/tree.h" #include "../common/classes/GenericMap.h" +#include "../common/classes/auto.h" +#include "../common/classes/array.h" #include "../jrd/exe.h" #include "../jrd/rpb_chain.h" #include "../jrd/blb.h" // For bid structure @@ -74,12 +76,19 @@ class BulkInsert; class SecDbContext { public: - SecDbContext(Firebird::IAttachment* a, Firebird::ITransaction* t) noexcept; + SecDbContext(MemoryPool& p, Firebird::IAttachment* a, Firebird::ITransaction* t) noexcept; ~SecDbContext(); Firebird::IAttachment* att; Firebird::ITransaction* tra; - int savePoint; + + void setSavePoint(); + void releaseSavePoint(); + void rollbackSavePoint() noexcept; + +private: + int savePointNumber = 0; + Firebird::string savePoint; }; // Blobs active in transaction identified by bli_temp_id. Please keep this @@ -430,6 +439,26 @@ class jrd_tra final : public pool_alloc // Finish and delete BulkInsert that belongs to the request void finiBulkInsert(thread_db* tdbb, Request* request); + + // Store an object to be updated + void storeUpdate(ElementBase* obj, bool forceRecompile); + + // Store updated object to be committed + void storeCommit(ElementBase* obj); + + // Process updates/commits accumulated by transaction + bool processUpdates(thread_db* tdbb); + void processCommits(thread_db* tdbb); + +private: + // Under processing and accumulated sets of dependencies + typedef Firebird::GenericMap> Deps; + Firebird::AutoPtr processingDeps; + Firebird::AutoPtr accumulatedDeps; + + // Set of updated objects to be committed + typedef Firebird::HalfStaticArray UpdateCommits; + Firebird::AutoPtr updateCommits; }; // System transaction is always transaction 0. @@ -574,7 +603,10 @@ enum dfw_t : int { dfw_delete_package_constant, // Package - dfw_create_package + dfw_create_package, + + // Update various objects dependent from modified in this transaction + dfw_update_dependencies }; } //namespace Jrd diff --git a/src/misc/writeBuildNum.sh b/src/misc/writeBuildNum.sh index 0e0ac604cd1..29b212ee51d 100755 --- a/src/misc/writeBuildNum.sh +++ b/src/misc/writeBuildNum.sh @@ -9,7 +9,7 @@ BuildType=T MajorVer=6 MinorVer=0 RevNo=0 -BuildNum=2178 +BuildNum=2191 NowAt=`pwd` cd `dirname $0` diff --git a/src/remote/parser.cpp b/src/remote/parser.cpp index ed9a1369730..cea7aaca6ff 100644 --- a/src/remote/parser.cpp +++ b/src/remote/parser.cpp @@ -299,7 +299,7 @@ static rem_fmt* parse_format(const UCHAR*& blr, size_t& blr_length) break; case blr_int128: - if (blr_length < 1) + if (blr_length-- == 0) return nullptr; desc->dsc_dtype = dtype_int128; desc->dsc_length = sizeof(Int128); diff --git a/src/utilities/fbsvcmgr/fbsvcmgr.cpp b/src/utilities/fbsvcmgr/fbsvcmgr.cpp index 00d17f99106..74281d9c1b2 100644 --- a/src/utilities/fbsvcmgr/fbsvcmgr.cpp +++ b/src/utilities/fbsvcmgr/fbsvcmgr.cpp @@ -427,6 +427,7 @@ constexpr SvcSwitches backupOptions[] = {"bkp_zip", putOption, 0, isc_spb_bkp_zip, 0 }, {"bkp_parallel_workers", putIntArgument, 0, isc_spb_bkp_parallel_workers, 0}, {"bkp_direct_io", putOption, 0, isc_spb_bkp_direct_io, 0}, + {"bkp_fast_path", putOption, 0, isc_spb_bkp_fast_path, 0}, {0, 0, 0, 0, 0} }; @@ -460,6 +461,7 @@ constexpr SvcSwitches restoreOptions[] = {"res_crypt", putStringArgument, 0, isc_spb_res_crypt, 0 }, {"res_replica_mode", putReplicaMode, 0, isc_spb_res_replica_mode, 0}, {"res_parallel_workers", putIntArgument, 0, isc_spb_res_parallel_workers, 0}, + {"res_fast_path", putOption, 0, isc_spb_res_fast_path, 0}, {"res_direct_io", putOption, 0, isc_spb_res_direct_io, 0}, {0, 0, 0, 0, 0} };