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}
};