4

I have a problem to resolve cache updates when delta includes fields that have UNIQUE constraint on the database. I have a database with the following DDL schema (SQLite in memory can be used to reproduce):

create table FOO
(
  ID integer primary key,
  DESC char(2) UNIQUE
);

The initial database table contains one record with ID = 1 and DESC = R1

Acessing this table with a TFDQuery (select * from FOO), if the following steps are performed, the generated delta will be correctly applied with ApplyUpdates:

  1. Update record ID = 1 to DESC = R2
  2. Append a new record ID = 2 with DESC = R1

Delta includes the following:

  1. R2
  2. R1

No error will be generated on ApplyUpdates, because the first operation on delta will be an update. The second will be an insert. As record 1 now is R2, the insertion can be done because there are no violation of the unique contraint on this transaction.

Now, performing the following steps, will generate the exactly same delta (look at the FDQuery.Delta property), but a UNIQUE constraint violation will be generated.

  1. Append a new temporary record ID = 2 with DESC = TT
  2. Update the first record ID = 1 to DESC = R2
  3. Update the temporary record 2 - TT to DESC = R1

Delta includes the following:

  1. R2
  2. R1

Note that FireDAC generates the same delta on both scenarios, this can be viewed through the FDquery's Delta property.

This steps cand be used to reproduce the error:

File > New VCL Forms Application; Drop a FDConnection and FDQuery on form; Set FDConnection to use SQLite driver (using in memory database); Drop two buttons on form, one to reproduce the correctly behavior, and another to reproduce the error, as follows:

Button OK:

procedure TFrmMain.btnOkClick(Sender: TObject);
begin
  // create the default database with a FOO table
  con.Open();
  con.ExecSQL('create table FOO' + '(ID integer primary key, DESC char(2) UNIQUE)');
  // insert a default record
  con.ExecSQL('insert into FOO values (1,''R1'')');
  qry.CachedUpdates := true;
  qry.Open('select * from FOO');
  // update the first record to T2
  qry.First();
  qry.Edit();
  qry.Fields[1].AsString := 'R2';
  qry.Post();
  // append the second record to T1
  qry.Append();
  qry.Fields[0].AsInteger := 2;
  qry.Fields[1].AsString := 'R1';
  qry.Post();
  // apply will not generate a unique constraint violation
  qry.ApplyUpdates();
end;

Button Error:

  // create the default database with a FOO table
  con.Open();
  con.ExecSQL('create table FOO' + '(ID integer primary key, DESC char(2) UNIQUE)');
   // insert a default record
  con.ExecSQL('insert into FOO values (1,''R1'')');
  qry.CachedUpdates := true;
  qry.Open('select * from FOO');
  // append a temporary record (TT)
  qry.Append();
  qry.Fields[0].AsInteger := 2;
  qry.Fields[1].AsString := 'TT';
  qry.Post();
  // update R1 to R2
  qry.First();
  qry.Edit();
  qry.Fields[1].AsString := 'R2';
  qry.Post();
  qry.Next();
  // update TT to R1
  qry.Edit();
  qry.Fields[1].AsString := 'R1';
  qry.Post();
  // apply will generate a unique contraint violation
  qry.ApplyUpdates();
  • I was going to answer that inside a transaction you could drop the unique constraint before updating and re-enabling it after the Applyupdates. But it looks like Sqlite can't drop unique constraints (you need to create a new table without the constraint and move all the data there), so this is not a feasible solution. – Marc Guillot Oct 07 '16 at 07:44
  • Welcome to StackOverflow and excellently written question. I can reproduce this in D Seattle, even with `CachedUpdates := False`. The same steps executed as a Sql script work w/o error. Interestingly, the error still occurs after inserting an extra `ApplyUpdates` immediately before `qrry.Next`. – MartynA Oct 07 '16 at 08:03

1 Answers1

3

Update Since writing the original version of this answer, I've done some more investigation and am beginning to think that either there is a problem with ApplyUpdates, etc, in FireDAC's support for Sqlite (in Seattle, at least), or we are not using the FD components correctly. It would need FireDAC's author (who is a contributor here) to say which it is.

Leaving aside the ApplyUpdates business for a moment, there are a number of other problems with your code, namely your dataset navigation makes assumptions about the ordering on the rows in qry and the numbering of its Fields.

The test case I have used is to start (before execution of the application) with the Foo table containing the single row

(1, 'R1')

Then, I execute the following Delphi code, at the same time as monitoring the contents of Foo using an external application (the Sqlite Manager plug-in for FireFox). The code executes without an error being reported in the application, but notice that it does not call ApplyUpdates.

  Con.Open();
  Con.StartTransaction;
  qry.Open('select * from FOO');
  qry.InsertRecord([2, 'TT']);
  assert(qry.Locate('ID', 1, []));
  qry.Edit;
  qry.FieldByName('DESC').AsString := 'R2';
  qry.Post;
  assert(qry.Locate('ID', 2, []));
  qry.Edit;
  qry.FieldByName('DESC').AsString := 'R1';
  qry.Post;
  Con.Commit;
  qry.Close;
  Con.Close;

The added row (ID = 2) is not visible to the external application until after Con.Close has executed, which I find puzzling. Once Con.Close has been called, the external application shows Foo as containing

(1, 'R2')
(2, 'R1')

However, I have been unable to avoid the constraint violation error if I call ApplyUpdates, regardless of any other changes I make to the code, including adding a call to ApplyUpdates after the first Post.

So, it seems to me that either the operation of ApplyUpdates is flawed or it is not being used correctly.

I mentioned FireDAC's author. His name is Dmitry Arefiev and he has answered a lot of FD qs on SO, though I haven't noticed him here in the past couple of months or so. You might try catching his attention by posting in EMBA's FireDAC NG forum, https://forums.embarcadero.com/forum.jspa?forumID=502.

MartynA
  • 30,454
  • 4
  • 32
  • 73