Skip to content

TBoldDbCopy fails silently on FireDAC: Prepare before parameter types, swallowed worker exceptions, leaked count query #94

Description

@bero

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

  1. 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.
  2. 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.
  3. Run never releases its row-counting query. SourceQuery := SourcePersistenceHandle.DatabaseInterface.GetQuery has no matching ReleaseQuery.
  4. 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

  • RED: TestCopiesAllRowsToTheDestination fails on current code - Expected [3] but got [0] every BOLD_OBJECT row must have been copied, FastMM reports the leaked query
  • GREEN after the fix on FireDAC/SQLite (single worker there: SQLite's shared-cache mode refuses concurrent writers with SQLITE_LOCKED; two workers on server engines)
  • Full suite green: 2173 found / 2171 passed / 2 ignored / 0 failed / 0 leaked
  • UniDAC: not verifiable end to end here (the fix removes a call UniDAC tolerated; behaviour unchanged for it)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions