Unable to find record, no key specified - delphi

I am coming across this infamous error in my Delphi application.
In my ADODataSet, I have set my CommandType to cmdText, and then set CommandText to
SELECT * FROM production.details
When I do a record change on my DBGrid, it throws the error of "Unable to find record, no key specified".
Under my ClientDataSet, I have set the various fields to have the Provider Flags of [pfInWhere, pfInUpdate, pfInKey], all other fields [pfInWhere, pfInUpdate]
A few people mention that it is the Provider Flags of the ADODataSet that need to be changed, since that is where the query originates. I tried this by the following
ADODataSet.Fields[0].ProviderFlags := [pfInKey, pfInUpdate, pfInWhere];
However, when I do this it throws a different error of List index out of bounds
Eventually I have all this in my DataSourceDataChange procedure:
if mySQLDataModule.dieDetailClientDataSet.ChangeCount > 0 then
begin
dieDetailClientDataSet.FieldByName('die_no').ProviderFlags := [pfInWhere, pfInUpdate, pfInKey];
dieDetailClientDataSet.FieldByName('comment').ProviderFlags := [pfInWhere, pfInUpdate];
dieDetailADODataSet.Fields[0].ProviderFlags := [pfInKey, pfInUpdate, pfInWhere];
dieDetailClientDataSet.ApplyUpdates(0);
end;
More details as requested : this is a MySQL database, the primary key is the same as what is specified in Fields[0]. In the CDS I have already set the ProviderFlags in the Designer Mode. It was because it wasn't working as expected that I read that it has to instead be set in the AdoDataSet as that is where my query has been set, hence me setting it manually here. Because all my fields are set up in the CDS (as there are also calculated fields) for my DBGrid that displays the data, I didn't redo all my fields in the AdoDataSet so there wasn't a designer option there, hence me doing it in code.

Related

Duplicate key error when updating key field in a set of records

I have a master detail relation between two tables. Troq is the master table and Troq_Alma is the detail table. Troq_Alma primary key is formed by Troq primary key (Troq.cod) and "Num_Orden". This "Num_Orden" field is not only part of the PK of the table but is the priority of that specific Alma out of the several Almas for one Troq. (Troq_Alma is the table that relates the different Troq's with their related Alma's)
And at a certain point, the user needs to modify this priority therefore modify the primary key of a set of records (The set of almas related to one specific troq). In the form there are two buttons (Spinbuttons) related with one TDBGrid. If I push the "Up" button, selected Troq_Alma should decrease "Num_Orden" by 1 and the Troq_alma with that value in "Num_Orden" would increase its value by 1. So in the "image" we have of the table in the dbgrid, there is no duplicate key.
Naturally when I do Apply this updates: TFDQ_Troq_Alma.ApplyUpdates(-1);
I get duplicate key error which I find very logical as in any way that firedac would try to make this update, the first modification is going to throw duplicate key error until the amendment is made for that other record that previously had that primary key, being the fact that it still has that primary key.
I really don't know if there is any "fair" solution to this problem, the only thing that I imagined was to first add some amount to all "num_orden" for one specific Troq do the update and then update again to the originally modified values, which is a really odd job, but, on my short knowledge of delphi and firedac, I really donĀ“t appear to find any other way to solve it.
Working on Delphi XE6 with cached Updates against Postgres 11.8 Database with firedac.
In case it could be of any interest, here is the code for both spin buttons (Up and Down):
procedure TFRM_Mant_TROQ.SpinBut_Troq_AlmaDownClick(Sender: TObject);
var iValActNumOrd, iValNumOrd2 : Integer;
RegActual: TBookMark;
iValMax: Integer;
begin
RegActual := DM_Mant_Troq.FDQ_Troq_Alma.GetBookmark;
iValMax := DM_DatosComun.MaxVal_FDQ(DM_Mant_Troq.FDQ_Troq_Alma, 'num_orden');
DM_Mant_Troq.FDQ_Troq_Alma.GotoBookmark(RegActual);
iValActNumOrd := DM_Mant_Troq.FDQ_Troq_Alma.FieldByName('NUM_ORDEN').Value;
if iValActNumOrd < iValMax then begin
DM_Mant_Troq.FDQ_Troq_Alma.Next;
iValNumOrd2 := DM_Mant_Troq.FDQ_Troq_Alma.FieldByName('NUM_ORDEN').Value;
DM_Mant_Troq.FDQ_Troq_Alma.Edit;
DM_Mant_Troq.FDQ_Troq_Alma.FieldByName('NUM_ORDEN').Value := iValActNumOrd;
// DM_Mant_Troq.FDQ_Troq_Alma.Post;
DM_Mant_Troq.FDQ_Troq_Alma.GotoBookmark(RegActual);
DM_Mant_Troq.FDQ_Troq_Alma.Edit;
DM_Mant_Troq.FDQ_Troq_Alma.FieldByName('NUM_ORDEN').Value := iValNumOrd2;
DM_Mant_Troq.FDQ_Troq_Alma.Post;
end;
end;
procedure TFRM_Mant_TROQ.SpinBut_Troq_AlmaUpClick(Sender: TObject);
var iValActNumOrd, iValNumOrd2 : Integer;
RegActual: TBookMark;
iValMin: Integer;
begin
RegActual := DM_Mant_Troq.FDQ_Troq_Alma.GetBookmark;
iValMin := DM_DatosComun.MinVal_FDQ(DM_Mant_Troq.FDQ_Troq_Alma, 'num_orden');
DM_Mant_Troq.FDQ_Troq_Alma.GotoBookmark(RegActual);
iValActNumOrd := DM_Mant_Troq.FDQ_Troq_Alma.FieldByName('NUM_ORDEN').Value;
if iValActNumOrd > iValMin then begin
DM_Mant_Troq.FDQ_Troq_Alma.Prior;
iValNumOrd2 := DM_Mant_Troq.FDQ_Troq_Alma.FieldByName('NUM_ORDEN').Value;
DM_Mant_Troq.FDQ_Troq_Alma.Edit;
DM_Mant_Troq.FDQ_Troq_Alma.FieldByName('NUM_ORDEN').Value := iValActNumOrd;
DM_Mant_Troq.FDQ_Troq_Alma.GotoBookmark(RegActual);
DM_Mant_Troq.FDQ_Troq_Alma.Edit;
DM_Mant_Troq.FDQ_Troq_Alma.FieldByName('NUM_ORDEN').Value := iValNumOrd2;
DM_Mant_Troq.FDQ_Troq_Alma.Post;
end;
end;
This code can probably be improved but as I mention, I believe this code is working alright, the matter on my point of view is on how to indicate firedac to "eventually avoid" this unique key validation.
Actually Troq.Cod, Num_Orden is not the primary key of the table Troq_Alma, but there is a unique index on this two fields and I really like this unique index for database integrity purposes.
If we accept that you have a good reason to change a unique key then you need to ensure that you avoid a key violation. It looks like you are working with data cached locally in a TFDQuery and then applying the updates.
The problem with that approach is that the TFDQuery remembers the loaded value of the field and the current value, so your intermediate changes are lost, resulting in a key violation.
A quick way to avoid that problem is to ApplyUpdates after every change. This could be a problem for you if you are remote from the main store.
If you need to assign a temporary value to an indexed field, you need to ensure that that value is unique. If the data schema allows it you could just negate the value of the Integer field (if it's not defined as unsigned). Although there could be another record with that value, if your convention is to only allocate these values temporarily you should avoid a key conflict.

Making TJvAppDBStorage work with FireDAC/Firebird

Using either Delphi 10.2 or 10.3, FireDAC and either Firebird 2.5 or 3.0: I've used the JVCL TJvAppStorage components for years and never had a problem with them, either to INI/XML storage or to a table in an AbsoluteDB database. I'm trying to migrate an app from AbsoluteDB to Firebird via FireDAC, and can't get the TJvAppDBStorage to write entries - it returns no errors, but nothing actually gets written into the table.
I have a datamodule containing the FireDAC connection and driver components, the JvAppDBStorage, a TDataSource and a TFDTable component. The FDB exists containing an appropriate table, the TFDTable is open on that table, the JvAppDBStorage has its properties set to match the table's fields, and the TFDTable, datasource and JvAppDBStorage are properly linked. (This all mirrors what has existed and worked against AbsoluteDB.) A call to dmStorage.FBStorage.WriteString(dmStorage.FBStorage.ConcatPaths(['General', 'LastStarted']), FormatDateTime(StdDTFmtStr, Now)); does not throw any exceptions, but nothing actually gets written into the table. Doing a normal append/set fields/post construct via the TFDTable works properly.
Any help appreciated!
Steve
There are 3 critical parts to this:
The underlying table must have a PK. I have an ID column with an associated sequence with before-insert trigger to make an auto-inc column for my PK, and an index that's a merge of the SectionID and KeyID fields to speed up the Locate() calls.
When creating the TFDTable object, you must create a new TFDTransaction object and attach it to the TFDTable and the TFDConnection.
On the TFDTable, you must set UpdateOptions.UpdateMode to upWhereKeyOnly. If you only do the first two steps, reads and appends will work but edits will not.
function TFDDBMgr.MakeTableObj(const ATblName: String): TFDTable;
begin
Result := TFDTable.Create(Self);
with Result do begin
//hook up all our needed stuff
Connection := Self.Connection;
TableName := ATblName;
CachedUpdates := False;
{...}
//!!.SS 09/11/19: REQUIRED for TJvAppDBStorage use,
//also requires that a PK be defined on the table
UpdateOptions.UpdateMode := upWhereKeyOnly;
Transaction := TFDTransaction.Create(Result);
Transaction.Connection := Self.Connection;
end;
end;

Can one create an MS Access table in Delphi (e.g., FireDAC) from the field defs of an existing table without using SQL?

I wanted to create a new mdb file containing tables based on the structure of previously existing tables. I knew that I can use newTable.FieldDefs.Add() to recreate the fields one by one in a loop. But since there is already an oldTable fully stocked with the correct FieldDefs, that seemed terribly inelegant. I was looking for a single-statement solution!
I had found that newTable.FieldDefs.Assign(oldTable.FieldDefs) would compile (and run) without error but it left newTable with zero defined fields. This caused me to erroneously conclude that I didn't understand that statement's function. (Later I found that it failed only when oldTable.open had not occurred, which could not happen when the database was not available, even though the FieldDefs had been made Persistent and were clearly visible in the Object Inspector)
Here is my original code after some sleuthing:
procedure TForm2.Button1Click(Sender: TObject);
var
fname: string;
Table: TFDTable;
FDConn: TFDConnection;
begin
fname := 'C:\ProgramData\mymdb.mdb';
if FileExists(fname) then DeleteFile(fname);
{ Make new file for Table }
FDMSAccessService1.Database := fname;
FDMSAccessService1.DBVersion := avAccess2000;
FDMSAccessService1.CreateDB;
{ Connect to new file }
FDConn := TFDConnection.Create(nil);
FDConn.Params.Database := fname;
FDConn.Params.DriverID := 'MSAcc';
FDConn.Connected := true;
{ Set up new Table using old table's structure }
Table := TFDTable.Create(nil);
try
{ ADOTable1 has been linked to an existing table in a prior
database with Field Defs made Persistent using the Fields
Editor in the Object Inspector. That database will not be
available in my actual use scenario }
try
ADOTable1.open; // Throws exception when database file not found
except
end;
Table.Connection := FDConn;
{ specify table name }
Table.TableName := ADOTable1.TableName;
Table.FieldDefs.Assign(ADOTable1.FieldDefs); // No errors reported
ShowMessageFmt('New Table %s has %d fields',[Table.TableName,
Table.FieldDefs.Count]);
{ Reports correct TableName but "0 fields" with table not open
(i.e. file not found). Reports "23 fields" with table open }
{ Set Table definition into new mdb file }
Table.CreateTable(False); // Throws exception when 0 fields found
finally
Table.Free;
end;
end;
It turned out that the solution was to use a ClientDataSet originally linked to the same old database instead of the ADOTable. See the working solution below in my answer.
Edit: A final note. I had hoped to use this FireDAC approach, as indicated here, to get around the lack of a TADOTable.CreateTable method. Alas, although the "solutions" above and below do work to create a new TADOTable, that table's field definitions are not faithful replicas of the original table. There may be a combination of the myriad TFDTable options that would get around this, but I was not able to discover it so I reverted to creating my ADO tables with SQL.
Thanks to #Ken White's (unfortunately deleted) pointer, I now think that I have a solution to my original question about cloning the field defs from an old table into a newly created database. My original problem stemmed from the fact that the FieldDefs function for a table evidently does not return the actual stored field data if the table is not "open" (i.e., connected to the relevant database). Since my use scenario would not have a valid database available I could not "open" the table. However, ClientDataSets have an additional option to "StoreDefs" along with editor options to "Fetch Params" and "Assign Local Data". With those settings saved, the ClientDataSet renders its FieldDefs properties without being "open". Using that approach it seems that I can clone the stored field defs to a new table without needing a currently valid database to read them from. Thanks again, Ken, you saved a lot of my remaining hair! I sure wish that Embarcadero would do a better job of rationalizing their help files. They removed BDE from the default installation of Rio while still pointing in their help file discussion on creating Access tables to its TTable type as the way to create new tables and then never point to the equivalent capabilities in FireDAC (or elsewhere) which they continue to support. I wasted a lot of time because of this "oversight"!
Here is my working code after Ken's tip:
procedure TForm1.Button1Click(Sender: TObject);
var
i: integer;
fname: string;
Table: TFDTable;
FDConn: TFDConnection;
begin
fname := 'C:\ProgramData\mymdb.mdb';
if FileExists(fname) then DeleteFile(fname);
FDMSAccessService1.Database := fname;
FDMSAccessService1.DBVersion := avAccess2000;
FDMSAccessService1.CreateDB;
FDConn := TFDConnection.Create(nil);
FDConn.Params.Database := fname;
FDConn.Params.DriverID := 'MSAcc';
FDConn.Connected := true;
Table := TFDTable.Create(nil);
try
Table.Connection := FDConn;
{ specify table name }
Table.TableName := 'ATable';
{ The existingClientDataSet has been linked to a table in the
prior, no longer valid, database using StoreDefs, Fetch Params,
and Assign Local Data in the Object Inspector }
Table.FieldDefs.Assign(existingClientDataSet.FieldDefs);
ShowMessageFmt('New Table has %d fields', [Table.FieldDefs.Count]);
Table.CreateTable(False);
finally
Table.Free;
end;

I have a syntax error in my insert into statement

I'm using a MS Access database, with the following columns in the Admins table:
Column Type
====== ====
Name Text
Surname Text
Dateadded Date/time
Adminnumber Number(long integer)
Password Text
ID type Autonumber (Not sure if ID is relevant)
This is my code but it keeps giving me a syntax error.
ADOquery1.Active := false;
adoquery1.sql.Text := 'insert into Admins(Name, surname, Adminnumber, Dateadded,password)Values('''+edit11.Text+''', '''+edit12.text+''', '''+edit13.Text+''', '''+edit14.Text+''', '''+edit15.text+''')';
ADOquery1.ExecSQL;
Adoquery1.SQL.Text := 'select * from Admins';
ADOquery1.Active := true;
i have been trying for a day to figure it out but its the same error no matter what code i use. The error is
Project project1.exe raised exception class eoleException
with message 'Syntax error in INSERT INTO statement'.
i have also tried:
ADOquery1.SQL.Add('Insert into admins');
ADOquery1.SQL.Add('(Name , Surname, Dateadded, Adminnumber, Password)');
ADOquery1.SQL.Add('Values :Name, :Surname, :Dateadded, :adminnumber :Password)');
ADOquery1.Parameters.ParamByName('Name').Value := edit11.Text;
ADOquery1.Parameters.ParamByName('Surname').Value := edit12.Text;
ADOquery1.Parameters.ParamByName('Dateadded').Value := edit13.Text;
ADOquery1.Parameters.ParamByName('Password').Value := edit14.Text;
ADOquery1.Parameters.ParamByName('Adminnumber').Value := edit15.Text;
ADOquery1.ExecSQL;
ADOquery1.SQL.Text := 'Select * from admins';
ADOquery1.Open ;
But this code gives me a problem with the from clause
The problem is that Name (and possibly Password) is a reserved word in MS Access. It's a poor choice for a column name, but if you must use it you should escape it by enclosing it in square brackets ([]). You're also missing an opening parenthesis (() after your VALUES statement, and a comma after the :adminnumber parameter.
ADOquery1.SQL.Add('Insert into admins');
ADOquery1.SQL.Add('([Name] , [Surname], [Dateadded], [Adminnumber], [Password])');
ADOquery1.SQL.Add('Values (:Name, :Surname, :Dateadded, :adminnumber, :Password)');
ADOquery1.Parameters.ParamByName('Name').Value := edit11.Text;
ADOquery1.Parameters.ParamByName('Surname').Value := edit12.Text;
ADOquery1.Parameters.ParamByName('Dateadded').Value := edit13.Text;
ADOquery1.Parameters.ParamByName('Password').Value := edit14.Text;
ADOquery1.Parameters.ParamByName('Adminnumber').Value := edit15.Text;
ADOquery1.ExecSQL;
ADOquery1.SQL.Text := 'Select * from admins';
ADOquery1.Open;
(The error can't be moving around, as you say in the comments to your question. The only line that can possibly cause the problem is the ADOQuery1.ExecSQL; line, as it's the only one that executes the INSERT statement. It's impossible for any other line to raise the exception.)
You should make some changes here that are pretty important to the maintainability of your code.
First, break the habit immediately of using the default names for controls, especially those you need to access from your code later. You change the name by changing the Name property for the control in the Object Inspector.
It's much easier in the code to use NameEdit.Text than it is to use Edit1.Text, especially by the time you get to Edit14. It would be much clearer if Edit14 was named PasswordEdit instead, and you'll be happy you did six months from now when you have to change the code.
Second, you should avoid using the default variant conversion from string that happens when you use ParamByName().Value. It works fine when you're assigning to a text column, but isn't really good when the type isn't text (such as when using dates or numbers). In those cases, you should convert to the proper data type before doing the assignment, so that you're sure it's done correctly.
ADOQuery1.ParamByName('DateAdded').Value := StrToDate(DateEdit.Text);
ADOQuery1.ParamByName('AdminNumber').Value := StrToInt(AdminNum.Text);
Finally, you should never, ever use string concatenation such as 'SOME SQL ''' + Edit1.Text + ''','''. This can lead to a severe security issue called SQL injection that can allow a malicious user to delete your data, drop tables, or reset user ids and passwords and giving them free access to your data. A Google search will find tons of information about the vulnerabilities that it can create. You shouldn't even do it in code you think is safe, because things can change in the future or you can get a disgruntled employee who decides to cause problems on the way out.
As an example, if a user decides to put John';DROP TABLE Admins; into edit14 in your application, and you call ExecSQL with that SQL, you will no longer have an Admins table. What happens if they instead use John';UPDATE Admins SET PASSWORD = NULL; instead? You now have no password for any of your admin users.

Delphi BDE Double type Field changed to a String type

I'm using a BDE TTable which had certain fields which were originally ftDouble.
Because the input to be stored is sometimes non-numeric, I now changed the field type to ftString.
Input into the field is done with a TEdit. When the code gets to:
with tblDM do
begin
Edit;
FieldByName('s01_amt').AsString := Edit1.Text;
Post;
end;
if the entry is not a number, I get the BDE error:
'a' is not a valid floating point
value for field 's01_amt'.
That error message is only created by fields of type TFloatField, which is only created when the TFieldDef has a DataType value of ftFloat. Double-check that you've changed the property think you did.
The field definitions can be populated from the fields themselves. Make sure you've changed the underlying database schema and not just your TTable component.
I would just convert it to a float:
var
dFloat : double;
begin
try dFloat := strToFloat(edit1.txt); except dFloat := 0; end;
edit;
FieldByName('s01_amt').AsFloat := dFloat;
post;
end;
When you changed the field type, did you also change the database's field in the schema (structure in xBASE/Clipper)? If not, you're trying to assign a non-numeric value to a numeric type field, and that's what's causing the exception.
If you're still using DBF style files, you need to change the type of the field in the database from NUMERIC to CHARACTER. You can do it using SQL from the Database Desktop, IIRC, with the BDE's DBASE support.
Just changing the TField's type from ftFloat to ftString won't alter the database storage for that field automatically; you have to do it in both places yourself.

Resources