TBoldDbCopy (Source/Persistence/DB/BoldDbCopy.pas) reports a successful run on FireDAC while copying nothing. Found by the first end-to-end DbCopy test (Test.BoldDbCopy.TTestBoldDbCopyEndToEnd, written for #92): three SomeClass rows copied from the test database into a second SQLite in-memory database - OnComplete fired, TotalTables > 0, destination empty.
Symptom
- Every worker thread dies on its first table with
EFDException: [FireDAC][Phys][SQLite]-335. Parameter [P0_0] data type is unknown. Hint: specify TFDParam.DataType or assign TFDParam value before Prepare/Execute call.
- Nothing reports it:
OnComplete fires, no error is logged, the caller sees a completed run.
- One
TBoldFireDACQuery/TFDQuery (or the UniDAC equivalents) leaks per Run.
Root causes
- Prepare before the parameters have types.
ProcessTables.CalcBatchSize builds the multi-row INSERT, assigns it and calls DestinationQuery.Prepare immediately - before any parameter has been assigned a value or type. FireDAC needs the types at Prepare; UniDAC tolerates a typeless prepare, which is why the tool worked there. It is the only explicit Prepare in the mapper/DB layer; both adapters prepare on Execute anyway, and the loop only needs ParamCount, which is available as soon as the SQL is assigned with ParamCheck on.
- Worker exceptions are swallowed. The anonymous worker thread runs
ProcessTables inside try … finally with no except: the exception dies with the FreeOnTerminate thread, the finally removes the thread from the list and the last one fires OnComplete. The Assert(count = SourceRecordCount) at the end of each table is lost the same way.
Run never releases its row-counting query. SourceQuery := SourcePersistenceHandle.DatabaseInterface.GetQuery has no matching ReleaseQuery.
- The FireDAC parameter wrapper renames the parameter (found once the worker error became visible — the INSERT then failed with
NOT NULL constraint failed: BOLD_MEMBERMAPPING.CLASSNAME). TBoldFireDACParameter.AssignFieldValue did FDParam.Assign(source.Field); TParam.Assign(TField) copies the field's name onto the parameter, FireDAC binds by name, the :p0_0 markers find nothing and bind NULL. The base class (used by UniDAC) calls AssignFieldValue(Field, Value), which sets type and value only. The other caller (BoldPMappersDefault, via ParamByName(FieldName)) was unaffected because there the parameter already carried the field's name.
~~3. Run never releases its row-counting query. SourceQuery := SourcePersistenceHandle.DatabaseInterface.GetQuery has no matching ReleaseQuery.
Fix
TBoldDbCopy.ProcessTables.CalcBatchSize: drop the explicit Prepare.
TBoldDbCopy.Run worker: catch exceptions from ProcessTables, log them through BoldLog as errors and record them in a new Errors: TStrings (thread-safe add) with HasErrors; completion is still signalled, so callers can tell a failed run from a successful one.
TBoldDbCopy.Run: release the row-counting query in a finally.
TBoldFireDACParameter.AssignFieldValue: FDParam.AssignFieldValue(source.Field, source.AsVariant) instead of FDParam.Assign(source.Field).
- The end-to-end test asserts
not HasErrors and the copied row counts and values.
Files changed
Source/Persistence/DB/BoldDbCopy.pas
Source/Persistence/FireDAC/BoldFireDACInterfaces.pas
UnitTest/Code/Persistence/Test.BoldDbCopy.pas
Testing
TBoldDbCopy(Source/Persistence/DB/BoldDbCopy.pas) reports a successful run on FireDAC while copying nothing. Found by the first end-to-end DbCopy test (Test.BoldDbCopy.TTestBoldDbCopyEndToEnd, written for #92): three SomeClass rows copied from the test database into a second SQLite in-memory database -OnCompletefired,TotalTables > 0, destination empty.Symptom
EFDException: [FireDAC][Phys][SQLite]-335. Parameter [P0_0] data type is unknown. Hint: specify TFDParam.DataType or assign TFDParam value before Prepare/Execute call.OnCompletefires, no error is logged, the caller sees a completed run.TBoldFireDACQuery/TFDQuery(or the UniDAC equivalents) leaks perRun.Root causes
ProcessTables.CalcBatchSizebuilds the multi-rowINSERT, assigns it and callsDestinationQuery.Prepareimmediately - before any parameter has been assigned a value or type. FireDAC needs the types atPrepare; UniDAC tolerates a typeless prepare, which is why the tool worked there. It is the only explicitPreparein the mapper/DB layer; both adapters prepare onExecuteanyway, and the loop only needsParamCount, which is available as soon as the SQL is assigned withParamCheckon.ProcessTablesinsidetry … finallywith noexcept: the exception dies with theFreeOnTerminatethread, thefinallyremoves the thread from the list and the last one firesOnComplete. TheAssert(count = SourceRecordCount)at the end of each table is lost the same way.Runnever releases its row-counting query.SourceQuery := SourcePersistenceHandle.DatabaseInterface.GetQueryhas no matchingReleaseQuery.NOT NULL constraint failed: BOLD_MEMBERMAPPING.CLASSNAME).TBoldFireDACParameter.AssignFieldValuedidFDParam.Assign(source.Field);TParam.Assign(TField)copies the field's name onto the parameter, FireDAC binds by name, the:p0_0markers find nothing and bind NULL. The base class (used by UniDAC) callsAssignFieldValue(Field, Value), which sets type and value only. The other caller (BoldPMappersDefault, viaParamByName(FieldName)) was unaffected because there the parameter already carried the field's name.~~3.
Runnever releases its row-counting query.SourceQuery := SourcePersistenceHandle.DatabaseInterface.GetQueryhas no matchingReleaseQuery.Fix
TBoldDbCopy.ProcessTables.CalcBatchSize: drop the explicitPrepare.TBoldDbCopy.Runworker: catch exceptions fromProcessTables, log them throughBoldLogas errors and record them in a newErrors: TStrings(thread-safe add) withHasErrors; completion is still signalled, so callers can tell a failed run from a successful one.TBoldDbCopy.Run: release the row-counting query in afinally.TBoldFireDACParameter.AssignFieldValue:FDParam.AssignFieldValue(source.Field, source.AsVariant)instead ofFDParam.Assign(source.Field).not HasErrorsand the copied row counts and values.Files changed
Source/Persistence/DB/BoldDbCopy.pasSource/Persistence/FireDAC/BoldFireDACInterfaces.pasUnitTest/Code/Persistence/Test.BoldDbCopy.pasTesting
TestCopiesAllRowsToTheDestinationfails on current code -Expected [3] but got [0] every BOLD_OBJECT row must have been copied, FastMM reports the leaked query