From 2d98e88d0d41eefeb1c4f1c1f3108e809d4e1f74 Mon Sep 17 00:00:00 2001 From: Popa Adrian Marius Date: Sun, 26 Jul 2026 21:33:18 +0300 Subject: [PATCH 1/4] Allow a backup or restore to request parallel workers Firebird 5 can run a backup or restore with several workers, as gbak -par does, but there was no way to ask for it: TIBXBackupService and TIBXRestoreService build their service parameter block without it. Adds a published ParallelWorkers property to TIBXBackupRestoreService, the base both share - isc_spb_bkp_parallel_workers and isc_spb_res_parallel_workers are the same parameter code, so one property serves both directions. The parameter is only sent when it is greater than one and the server is Firebird 5 or later, so nothing changes for existing callers or older servers, which reject the parameter. Zero or one means the default single-threaded behaviour. Requires the isc_spb_bkp_parallel_workers constant, which fbintf does not currently define: MWASoftware/fbintf#8. Verified against a live Firebird 6.0.0 server: with one worker the parameter is not sent; with four it is, the server accepts it, and the resulting backup restores to a working database. --- runtime/nongui/IBXServices.pas | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/runtime/nongui/IBXServices.pas b/runtime/nongui/IBXServices.pas index 4f9b8c40..9a1371db 100644 --- a/runtime/nongui/IBXServices.pas +++ b/runtime/nongui/IBXServices.pas @@ -341,11 +341,17 @@ TIBXBackupRestoreService = class(TIBXControlAndQueryService) private FStatisticsRequested: TBackupStatsOptions; FVerbose: Boolean; + FParallelWorkers: Integer; protected procedure SetServiceStartOptions; override; property Verbose : Boolean read FVerbose write FVerbose default False; property StatisticsRequested: TBackupStatsOptions read FStatisticsRequested write FStatisticsRequested; published + {Number of workers the server should use for the backup or restore, as + gbak -par does. Firebird 5 and later; an older server rejects the + parameter, so it is only sent when the server can take it. Zero or one + means the default, single-threaded behaviour.} + property ParallelWorkers: Integer read FParallelWorkers write FParallelWorkers default 0; end; TBackupOption = (IgnoreChecksums, IgnoreLimbo, MetadataOnly, NoGarbageCollection, @@ -1928,6 +1934,14 @@ procedure TIBXBackupRestoreService.SetServiceStartOptions; if Verbose then SRB.Add(isc_spb_verbose); + {Firebird 5 and later. isc_spb_bkp_parallel_workers and + isc_spb_res_parallel_workers are the same parameter code, so one property + serves both directions.} + if FParallelWorkers > 1 then + with ServicesConnection do + if ServerVersionNo[1] >= 5 then + SRB.Add(isc_spb_bkp_parallel_workers).AsInteger := FParallelWorkers; + with ServicesConnection do {Firebird 2.5.5 and later} if (ServerVersionNo[1] < 2) or From ee9027ccf7c6a67df0059976cef067d0751ac94e Mon Sep 17 00:00:00 2001 From: Popa Adrian Marius Date: Sun, 26 Jul 2026 21:41:23 +0300 Subject: [PATCH 2/4] Allow an in-place ODS upgrade through the validation service Firebird 5 can upgrade a database's on-disk structure in place, as gfix -upgrade does, instead of requiring a backup and restore. There was no way to ask for it: TIBXValidationService maps its options onto the isc_spb_rpr_* bits and had no entry for it. Adds UpgradeODS to TValidateOption, appended rather than inserted so the existing values keep their ordinals, and sends isc_spb_rpr_upgrade_db only when the server is Firebird 5 or later - an older one rejects the option. Requires the isc_spb_rpr_upgrade_db constant, which fbintf does not currently define. Verified against a live Firebird 6.0.0 server: the assembled repair parameter is 0x1000 and the request completes, leaving a database already at the server's format unchanged. --- runtime/nongui/IBXServices.pas | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/runtime/nongui/IBXServices.pas b/runtime/nongui/IBXServices.pas index 9a1371db..c9f7cf9e 100644 --- a/runtime/nongui/IBXServices.pas +++ b/runtime/nongui/IBXServices.pas @@ -485,8 +485,10 @@ TIBXOnlineValidationService = class(TIBXControlAndQueryService) property DatabaseName; end; + {UpgradeODS is appended rather than inserted so that the existing values keep + their ordinals for anything already compiled against this unit.} TValidateOption = (CheckDB, IgnoreChecksum, KillShadows, MendDB, - SweepDB, ValidateDB, ValidateFull); + SweepDB, ValidateDB, ValidateFull, UpgradeODS); TValidateOptions = set of TValidateOption; { TIBXValidationService } @@ -1700,6 +1702,13 @@ procedure TIBXValidationService.SetServiceStartOptions; if not (MendDB in Options) then param := param or isc_spb_rpr_validate_db; end; + {Firebird 5 and later: upgrade the on-disk structure in place, as + gfix -upgrade does, rather than by backup and restore. Only sent to a + server that knows the option, since an older one rejects it.} + if (UpgradeODS in Options) then + with ServicesConnection do + if ServerVersionNo[1] >= 5 then + param := param or isc_spb_rpr_upgrade_db; if param > 0 then SRB.Add(isc_spb_options).AsInteger := param; end; From 5555abd2021d268943133bda3d4f6c71ae499646 Mon Sep 17 00:00:00 2001 From: Popa Adrian Marius Date: Sun, 26 Jul 2026 22:38:52 +0300 Subject: [PATCH 3/4] Set isc_dpb_page_size as an integer rather than a string isc_dpb_page_size is an integer DPB parameter, but GenerateDPB grouped it with the string ones and wrote it with SetAsString. That stores the digits as text, and the clumplet writer rejects an integer parameter carrying more than four bytes, so DB.Params.Values['page_size'] := '16384'; DB.CreateDatabase; fails with Invalid clumplet buffer structure: length of integer exceeds 4 bytes (5) Only the four-digit page sizes appeared to work, and those worked by accident: TFBAttachment.GenerateCreateDatabaseSQL reads the item back with AsString and pastes it into PAGE_SIZE, so the text happened to round-trip. Setting it as an integer fixes the five-digit sizes and leaves the create path unchanged, since getAsString on an integer item returns IntToStr of its value. Verified against Firebird 6.0.0: creating a database with page_size 16384 now yields MON$DATABASE.MON$PAGE_SIZE = 16384. --- runtime/nongui/IBDatabase.pas | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/runtime/nongui/IBDatabase.pas b/runtime/nongui/IBDatabase.pas index ab3ec71b..aec2b011 100644 --- a/runtime/nongui/IBDatabase.pas +++ b/runtime/nongui/IBDatabase.pas @@ -2407,10 +2407,18 @@ function TIBDataBase.GenerateDPB(FirebirdAPI: IFirebirdAPI; sl: TStrings): IDPB; case DPBItem.getParamType of isc_dpb_user_name, isc_dpb_password, isc_dpb_password_enc, isc_dpb_sys_user_name, isc_dpb_license, isc_dpb_encrypt_key, - isc_dpb_lc_messages, isc_dpb_lc_ctype, isc_dpb_page_size, + isc_dpb_lc_messages, isc_dpb_lc_ctype, isc_dpb_sql_role_name: DPBItem.SetAsString(ParamValue); + {isc_dpb_page_size is an integer parameter, not a string one. Writing it + with SetAsString stores the digits as text, which the clumplet writer + rejects outright once there are more than four of them - so + Params.Values['page_size'] := '16384' failed with "length of integer + exceeds 4 bytes (5)", and only the four-digit sizes appeared to work.} + isc_dpb_page_size: + DPBItem.SetAsInteger(StrToUInt(ParamValue)); + isc_dpb_sql_dialect: begin if (ParamValue = '') or (ParamValue[1] = '3') then From f25e48f831d1baf7bc999ba2972ad3a150d8ad0a Mon Sep 17 00:00:00 2001 From: Popa Adrian Marius Date: Wed, 29 Jul 2026 16:17:10 +0300 Subject: [PATCH 4/4] Do not touch the transaction list after the unit has finalized TIBTransaction.Create added itself to FTransactionList without checking it was still there, while Destroy already guarded the matching Remove. That asymmetry is an access violation on the way out of a GUI program: EAccessViolation CREATE, line 1603 of runtime/nongui/IBDatabase.pas ... FREECOMPONENT / PROCESSASYNCCALLQUEUE DESTROY, line 137 of include/application.inc INTERFACES_$$_finalize$, line 38 of interfaces.pas The LCL frees forms left on Application's async release queue from inside Application.Destroy, which runs from interfaces.pas finalization - by which time this unit may already have finalized. Tearing down a data-aware control there reaches IBX again and constructs a transaction, and the list it wants to join has gone. The guard Destroy already had did not work either. Finalization called Free without nilling, so Assigned stayed true on a dangling pointer and the test let the code through to read released memory. FreeAndNil is what makes both guards mean anything. Left alone deliberately: FCriticalSection.Enter/Leave around EndTransaction have the same exposure, but pairing a lock with a nil check is a change to the locking rather than a missing guard, and nothing has been seen to hit it. Found from a symbolic backtrace out of Marathon, whose only symptom was an exit code of 217 after the window had closed and the work was saved. Verified by reasoning and by a full pass of that project's suites; the crash itself is intermittent and needs a real window close, so it has not been watched to disappear. --- runtime/nongui/IBDatabase.pas | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/runtime/nongui/IBDatabase.pas b/runtime/nongui/IBDatabase.pas index aec2b011..d1ca5c8d 100644 --- a/runtime/nongui/IBDatabase.pas +++ b/runtime/nongui/IBDatabase.pas @@ -1599,7 +1599,13 @@ constructor TIBTransaction.Create(AOwner: TComponent); FTimer.Interval := 0; FTimer.OnTimer := TimeoutTransaction; FDefaultAction := taCommit; - FTransactionList.Add(self); + { Guarded the way Destroy's Remove already is. A transaction can be + constructed after this unit has finalized - the LCL frees documents from + Application's async queue during Application.Destroy, and tearing a + data-aware control down there reaches IBX again - and the list is gone by + then. Unguarded, that is an access violation on the way out. } + if Assigned(FTransactionList) then + FTransactionList.Add(self); if (FTransactionName = '') and (CreateGUID(uuid) = 0) then FTransactionName := GUIDToString(uuid); end; @@ -2500,8 +2506,13 @@ function TIBTransaction.GenerateTPB(FirebirdAPI: IFirebirdAPI; sl: TStrings): IT TIBTransaction.FTransactionList := TList.Create; Finalization - if assigned(TIBTransaction.FCriticalSection) then TIBTransaction.FCriticalSection.Free; - if assigned(TIBTransaction.FTransactionList) then TIBTransaction.FTransactionList.Free; + { FreeAndNil rather than Free: these are class vars that outlive the unit's + finalization in the sense that something can still reach them afterwards, + and Free leaves the reference dangling - so the Assigned tests guarding + them stayed true and went on reading released memory. Nil is what makes + those guards mean anything. } + if assigned(TIBTransaction.FCriticalSection) then FreeAndNil(TIBTransaction.FCriticalSection); + if assigned(TIBTransaction.FTransactionList) then FreeAndNil(TIBTransaction.FTransactionList); end.