i am working on Delphi 7+ SQL server.
i am converting my application from BDE to ADO.
and in some places they are handling Record/Key deleted error and the error code they are checking for is 8708.
do we have Record/Key deleted Error in ADO? and can any one please explain me in what scenario it will raise that error?
The following may help you exploring how to reconcile update conflicts in Sql Server tables
using TAdo* components. By way of preparation, create a table on your server with a definition like this
CREATE TABLE [dbo].[ATable](
[ID] [int] NOT NULL,
[name] [varchar](40) NOT NULL,
PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
and then populate it with a few rows.
Then, create a minimal Ado Delphi project with a TBGrid and TDNNavigator and the code below.
Update If you have read the original version of my answer to your q,
it talked in terms of doing batch updates on the dataset. However, since posting it, I've discovered what seems to be an anomaly in how
TAdoQuery.UpdateBatch works, so I've simplified the example code
to avoid using batched updates.
Now, compile and run the project, and open a second instance of it in a CMD window.
Change the [name] field in a row in the 2nd instance and save it, then try and make
a different change to the same row in the IDE instance. You should get an error
with words to the effect of
Record cannot be located for updating. Some values may have changed since was
last read.
How you deal with this condition is entirely up to you. You could, for example, save a local copy of the current user's version of the row's
fields, cancel the edit to the row and then fetch the new row version from the server and ask the user
whether his changes should be applied to it.
YOu should see that when the error occurs, the NativeError code is 32. Unfortunately, that is also the NativeError returned when the second
instance of the app deletes the record rather than changes it (which
makes sense in a way, because in either case, the original version
of the record no longer exists in the server table. If you want to be
able to distinguish between rows that have been changed and ones that
have been deleted, you could e.g. run a query against the table to
see if the row with the current row ID exists.
Code
type
TForm1 = class(TForm)
DataSource1: TDataSource;
DBGrid1: TDBGrid;
DBNavigator1: TDBNavigator;
ADOConnection1: TADOConnection;
ADOQuery1: TADOQuery;
Button1: TButton;
Memo1: TMemo;
procedure FormCreate(Sender: TObject);
private
procedure OnException(Sender: TObject; E: Exception);
public
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
begin
Application.OnException := OnException;
AdoQuery1.LockType := ltOptimistic;
AdoQuery1.CursorType := ctKeySet;
AdoQuery1.SQL.Text := 'select * from atable';
AdoQuery1.Open;
DBGrid1.Options := DBGrid1.Options + [dgEditing];
DBGrid1.Columns[0].ReadOnly := True;
end;
procedure TForm1.OnException(Sender: TObject; E: Exception);
var
AErrors : Errors;
AError : Error;
i : Integer;
S : String;
begin
Caption := 'Exception';
if E is EDatabaseError then begin
AErrors := AdoQuery1.Connection.Errors;
for i := 0 to AErrors.Count - 1 do begin
AError := AErrors.Item[i];
S := Format('Number: %d, NativeError: %d, source: %s, description: %s',
[AError.Number, AError.NativeError, AError.Source, AError.Description]);
Memo1.Lines.Add(S);
end;
end;
end;
end.
Related
Can anyone help me to clone a TFDQuery in run-time? I'm coding in Delphi Tokyo , I have a Datamodule with a TFDQuery in which I defined all fields properties using Fields Editor at design time, in this way my DBGrid1 that points to this a Datamodule of this dataset, has all columns properly formated (dislay names, width, format, order). During run-time I need to create new instances of TFDQuery, TDatamodule and link these new objects with the Dbgrid1. I need this new TFDQuery be identiical to the existing one defined at design-time in order to keep DBgrid1 with same display names, display width and display formats as the design-time!
I tried the following approaches to copy dataset field definitions :
**1st Approach : Method Assign for TFDQuery (didn't work) **
type
TFormDados = class(TForm)
Edit1: TEdit;
Button1: TButton;
DBGrid1: TDBGrid;
Edit2: TEdit;
Label1: TLabel;
Label2: TLabel;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
vconnection : TFDConnection;
vdataset : TFDQuery;
vdatasource : Tdatasource;
public
{ Public declarations }
end;
var
FormDados: TFormDados;
implementation
{$R *.dfm}
Uses
unitdata;
procedure TFormDados.Button1Click(Sender: TObject);
var
i : integer;
begin
vconnection := TFDConnection.Create(nil);
vconnection.Assign(Dtmodule.FDConGrafico);
vdataset := TFDQuery.Create(nil);
vdataset.Connection := vconnection;
vdataset.Assign(Dtmodule.FDQueryDados); // Runtime Error : Cannot assign a TFDQuery to a TFDQuery
2nd Approach : Assign FieldDefs from the existing Dataset to the new one - didn't work !
...
vdataset.FieldDefs.Assign(Dtmodule.FDQueryDados.FieldDefs);
vdataset.sql := Dtmodule.FDQueryDados.sql;
vdataset.params := Dtmodule.FDQueryDados.Params;
vdataset.FieldDefs.Update;
vdataset.CreateDataSet;
vdatasource := Tdatasource.create(nil);
vdatasource.DataSet := vdataset;
dbgrid1.DataSource := vdatasource;
vdataset.close;
vdataset.Params[0].Asinteger := strtoint(edit1.Text);
vdataset.Params[1].Asinteger := strtoint(edit2.Text);
vdataset.Open;
Althought Assign method had run, vdataset didn't receive the fields definitions of the existing FDQquery . After open the vdataset , DBGrid1 did not show the columns sequence, labels and formats fro the source dataset , WHY ?
3rd Approach - Copy fields definition, one by one - didn't work
for i:=0 to Dtmodule.FDQueryDados.Fields.Count -1 do
begin
with vdataset.FieldDefs.AddFieldDef do
begin
Name := Dtmodule.FDQueryDados.FieldDefs[i].Name;
Datatype := Dtmodule.FDQueryDados.FieldDefs[i].DataType;
Displayname := Dtmodule.FDQueryDados.FieldDefs[i].Displayname;
Fieldno := Dtmodule.FDQueryDados.FieldDefs[i].FieldNo;
end;
end;
vdataset.FieldDefs.Update;
vdataset.CreateDataSet;
vdatasource := Tdatasource.create(nil);
vdatasource.DataSet := vdataset;
dbgrid1.DataSource := vdatasource;
...
This code lead to the same result as the approach 2nd, i.e., it run but after opened vdataset , DBGrid1 did not show the columns sequence, labels and formats fro the source dataset.
I appreciate your help to fix the above code OR to implement the right method to copy dataset fields definitions from one existing dataset to a new one.
Thank you all in advance !
When you use the Fields editor for queries you are creating Fields not FieldDefs. From what I can tell the FieldDefs are kept in sync with the FieldsCollection when the component is created (or maybe opened not 100% sure). The Display* properties are not available on the FieldDef object - they only exist on the Field object. When you go to copy the structure you need to iterate the fields. The the method we use is below.
Note that the loop and the items created are "Fields", but we use a temporary FieldDef object to make the code simpler. The TFieldDef.CreatField serves as a class factory method to get the correct type of field i.e. TIntegerField vs TStringField. Also if you are using calculated fields you will need to hookup the OnCalcField event. This method does not do that.
procedure CopyFieldStructure(Source: TDataSet; Target: TDataset);
{^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^}
var
Field: TField;
NewField: TField;
FieldDef: TFieldDef;
begin
Target.Fields.Clear;
Target.FieldDefs.Clear;
// Cannot perform the next operation on an opened dataset
if Target.State <> dsInactive then
Target.Close;
for Field in Source.Fields do
begin
// We are going to setup the first part in a FieldDef
// that will set us use the CreateField Call in order to
// get the correct subclass of TField created.
FieldDef := Target.FieldDefs.AddFieldDef;
FieldDef.DataType := Field.DataType;
FieldDef.Size := Field.Size;
FieldDef.Name := Field.FieldName;
NewField := FieldDef.CreateField(Target);
NewField.Visible := Field.Visible;
NewField.DisplayLabel := Field.DisplayLabel;
NewField.DisplayWidth := Field.DisplayWidth;
NewField.EditMask := Field.EditMask;
NewField.Calculated := Field.Calculated;
end;
end;
Here is a similar StackOverflow question. I think this is where I originally took my code from: Is there some better way to copy all DataSet Fields and their properties to another DataSet?
And here is one other blog post that uses a similar approach: How to: Clone TField and TDataset fields structure
Also don't get fooled by the TDataSet.CopyField method. The help makes it seem like it could copy the field structure. When really it copies the current field "values" for any matching field names.
I have a Field named "refFile" which may or may not have a Path description in it.
I need to go through the entire database and check whether Paths that are defined in "refFile" still actually exist.
Using Delphi (Pascal) this takes many, many minutes
bigDB.First;
while not(bigDB.EOF) do
begin
if Trim(bigDB.FieldByName('refFile').AsString) > '' then
begin
if not(FileExists(bigDB.FieldByName('refFile').AsString)) then
begin
bigDB.Edit;
bigDB.FieldByName('refFile').AsString:='';
bigDB.Post;
end;
end;
bigDB.Next;
end;
How do I do that in SQL?
Thank you.
You cannot check the validity of a path in SQLLite but you can filter records with something in the path and reduce the list of lines to check.
You can order the records on this field (if you have an index on it) and check only the paths you didn't checked before.
You also can use threads to do this long operation in background. Simply use TThread.Createanonymousthread(procedure begin end).Start;
You can't check the existence of a file in a plain SQLLite query. You could do that by using an UDF (User defined function) but it would be a little more complex and would requires some skills in other programming languages (Note that in that case your files should be accessible from the server, otherwise it wouldn't work).
If you are looking for a simpler solution, I think you can speed up your program by reducing the number of records resulted by the query and by improving your Delphi code in order to make it a little more efficient.
Select SQL:
Use length and trim functions due to reduce the number of records to be verified by your Delphi code.
select refFile
from myTable
where (refFile is not null) and (length(trim(refFile)) > 0)
Delphi:
Call TDataSet.FieldByName only once.
Try using TDataSet.DisableControls and TDataSet.EnableControls (In this way, some dataset's components are faster, even if the dataset component is not linked to any control).
var
Fld : TField;
begin
BigDB.DisableControls();
try
Fld := BigDB.FieldByName('refFile');
BigDB.First;
while not(BigDB.Eof) do
begin
if not(FileExists(Fld.AsString)) then
begin
BigDB.Edit;
Fld.AsString := '';
BigDB.Post;
end;
BigDB.Next;
end;
finally
BigDB.EnableControls();
end;
Furthermore, you could consider these other optimizations:
If the refFile field contains the same value multiple times, you could sort the query by the refFile field and change the Delphi code in order to verify each filename only once. (You can do that by storing the last value and the result of the FileExists function).
You can run your code asyncronusly by using the TThread class. In this way your application won't freeze and it could be faster.
For example with FireDAC it's extremely easy to create user defined functions. If you're using it, try something like this. It could save some time because the engine doesn't need to fetch the resultset to the client application:
uses
FireDAC.Phys.SQLiteWrapper;
type
TForm1 = class(TForm)
Button1: TButton;
FDQuery1: TFDQuery;
FDConnection1: TFDConnection;
FDGUIxWaitCursor1: TFDGUIxWaitCursor;
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
private
FValidator: TSQLiteFunction;
procedure ValidateFile(AFunc: TSQLiteFunctionData; AInputs: TSQLiteInputs;
AOutput: TSQLiteOutput; var AUserData: TObject);
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
FDConnection1.Open;
FValidator := TSQLiteFunction.Create((TObject(FDConnection1.CliObj) as TSQLiteDatabase).Lib);
FValidator.Args := 1;
FValidator.Name := 'FileExists';
FValidator.OnCalculate := ValidateFile;
FValidator.InstallAll;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
FDQuery1.SQL.Text :=
'UPDATE MyTable SET FileName = NULL WHERE ' +
'FileName IS NOT NULL AND NOT FileExists(FileName)';
FDQuery1.ExecSQL;
end;
procedure TForm1.ValidateFile(AFunc: TSQLiteFunctionData; AInputs: TSQLiteInputs;
AOutput: TSQLiteOutput; var AUserData: TObject);
begin
AOutput.AsBoolean := FileExists(AInputs[0].AsString);
end;
Or simply drop the TFDSQLiteFunction component, fill out the FunctionName property with name of the function, write OnCalculate event handler similar to the above and enable the component by setting the Active property.
I have a form that has two combo boxes, both of which contain the same list of items, and need to remain perfectly in sync with one another. (They represent the same list of options on two different tabs of a TPageControl.)
To make this work, I set up a LiveBinding to bind both controls to the same field of a ClientDataset that exists for no other purpose than to keep controls on this form synchronized. My TBindingsList has a TLinkControlToField for both combo boxes, linking them both to the same field.
Everything works fine, as long as I make all changes within the GUI.
However, if an unrelated action changes the selection state of one of the boxes, they get out of sync:
cboMainValue.Items.InsertObject(0, 'ALL', TObject(-1));
cboAltValue.Items.Clear;
cboAltValue.Items.Assign(cboMainValue.Items);
cboMainValue.ItemIndex := 0;
cboAltValue.ItemIndex := 0;
After this point, for whatever reason, cboMainValue shows the expected text, while cboAltValue remains blank (ie ItemIndex = -1).
I've tried setting the Text property rather than ItemIndex, and editing the value of the backing field on the ClientDataset, but none of these produces a different result.
Does anyone know how to programmatically change the state of one combo box and make the LiveBindings update the other one to match it?
I'm tried your code in Seattle in a newly created project and it doesn't exhibit
the problem you describe. I'm posting it as an answer as it's pretty minimal in the MCVE sense,
does most of its set-up in code (so there are no "funnies" lurking in the DFM), and might allow you to "spot the difference"
compared with yours.
So I think the literal answer to your q is "The way you're doing now." Good luck!
Btw, personally I think that Tom Brunberg's suggestion might be a better way to go, but obviously it would be good to get to the bottom of what's causing the problem in your project.
Code:
type
TForm1 = class(TForm)
CDS1: TClientDataSet;
DataSource1: TDataSource;
DBGrid1: TDBGrid;
CDS1ID: TIntegerField;
CDS1Value: TStringField; // String 20 field
cboMainValue: TComboBox;
BindSourceDB1: TBindSourceDB;
DBNavigator1: TDBNavigator;
cboAltValue: TComboBox;
Button1: TButton;
procedure Button1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
public
LinkControlToField1: TLinkControlToField;
LinkControlToField2: TLinkControlToField;
end;
[...]
procedure TForm1.Button1Click(Sender: TObject);
begin
cboMainValue.Items.InsertObject(0, 'ALL', TObject(-1));
cboMainValue.Items.InsertObject(1, 'Other', TObject(-1));
cboAltValue.Items.Clear;
cboAltValue.Items.Assign(cboMainValue.Items);
cboMainValue.ItemIndex := 0;
cboAltValue.ItemIndex := 0;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
cboMainValue.Items.Insert(0, 'apple');
cboMainValue.Items.Insert(1, 'orange');
cboMainValue.Items.Insert(2, 'banana');
cboAltValue.Items.Assign(cboMainValue.Items);
LinkControlToField1 := TLinkControlToField.Create(Self);
LinkControlToField1.DataSource := BindSourceDB1;
LinkControlToField1.FieldName := 'Value';
LinkControlToField1.Control := cboMainValue;
LinkControlToField2 := TLinkControlToField.Create(Self);
LinkControlToField2.DataSource := BindSourceDB1;
LinkControlToField2.FieldName := 'Value';
LinkControlToField2.Control := cboAltValue;
CDS1.CreateDataSet;
CDS1.InsertRecord([1, 'apple']);
CDS1.InsertRecord([2, 'banana']);
CDS1.InsertRecord([3, 'orange']);
end;
I have this items in my dbase file (.dbf)
INDICE NOME COR ESTILO ESCALA
100 SAOJOAO 18 0,00
I need to change column name of INDICE to ID, so I use this code:
while not ADOQuery1.Eof do
begin
Adoquery1.Edit;
ADOQuery1.FieldByName('NOME').TEXT:= 'ID';
Adoquery1.Post;
ADOQuery1.Next;
end;
When I run the above I get these results:
INDICE NOME COR ESTILO ESCALA
ID SAOJOAO 18 0,00
Connection string used:
Driver={Microsoft dBASE Driver (*.dbf)};DriverID=277;Dbq=C:_workspace\projects\DBFEditor\temp
I have a system that need import dbf file and only recognize a file which have id column name.
The demo project below shows a way to do what you seem to want. I don't claim that
it's the most efficient way or the best way, but it's probably as simple as you're likely to get.
If you were wanting just to change the displayed name of a field in a Delphi application, for example in the column header of a DBGrid, you could do that by changing the DisplayLabel property of the field in question (AdoQuery1.FieldByName('INDICE').DisplayLabel := 'ID'), as I said in a comment earlier. But in your latest edit, it seems that what you actually want to do is to change the name of the INDICE column as it seen by a program reading the datafile to ID. To do that, you have to make an alteration to the on-disk structure of your .DBF file. This is what my code below does.
It uses a User DSN set up for the MS ODBC driver for dBase files as the target of the AdoConnection's connection string.
Ideally, I would have liked the find a flavour of the ALTER TABLE Sql statement
which would simply rename the INDICE column, but the MS dBase driver doesn't seem
to support that, because it generated an exception when I tried. So instead, my code works by making a copy of the table and its contents, with the INDICE column renamed to ID.
In short, the program
Creates a table MATest with a first column named INDICE and a couple of other columns and inserts a single row into it. This is just to set up a table to work from.
Creates a second table MATest2 with the same structure as the MATest one, except that the first
column is named ID rather than INDICE.
Populates the MATest2 table by copying all the rows from MATest, using an INSERT INTO Sql statement.
The important steps for what you want to do are carried out in the btnCreateTableCopyClick
procedure. Note that you will have to comment out the first two lines, which drop the
table MATest2 the first time you run the app, otherwise it will complain, cryptically,
that MATest2 can't be dropped because it doesn't exist.
I leave it to you to adapt the code as necessary to your data.
Code:
type
TForm1 = class(TForm)
ADOConnection1: TADOConnection;
btnCreateSrcTable: TButton;
ADOQuery1: TADOQuery;
btnOpenSrcTable: TButton;
DataSource1: TDataSource;
DBGrid1: TDBGrid;
DBNavigator1: TDBNavigator;
btnDropTable: TButton;
btnCreateTableCopy: TButton;
procedure btnCreateSrcTableClick(Sender: TObject);
procedure btnDropTableClick(Sender: TObject);
procedure btnOpenSrcTableClick(Sender: TObject);
procedure btnCreateTableCopyClick(Sender: TObject);
private
protected
public
procedure CreateSourceTable;
end;
[...]
procedure TForm1.btnCreateTableCopyClick(Sender: TObject);
var
Sql : String;
begin
Sql := 'drop table MATest2';
AdoConnection1.Execute(Sql);
Sql := 'create table MATest2(ID int, AName char(20), AValue char(20))';
AdoConnection1.Execute(Sql);
Sql := 'insert into MATest2 select INDICE, AName, AValue from MATest';
AdoConnection1.Execute(Sql);
end;
procedure TForm1.btnCreateSrcTableClick(Sender: TObject);
begin
CreateSourceTable;
end;
procedure TForm1.btnDropTableClick(Sender: TObject);
var
Sql : String;
begin
// Sql := 'drop table MATest';
// AdoConnection1.Execute(Sql);
end;
procedure TForm1.btnOpenSrcTableClick(Sender: TObject);
begin
AdoQuery1.Open;
end;
procedure TForm1.btnCreateTableCopyClick(Sender: TObject);
var
Sql : String;
begin
Sql := 'drop table MATest2';
AdoConnection1.Execute(Sql);
Sql := 'create table MATest2(ID int, AName char(20), AValue char(20))';
AdoConnection1.Execute(Sql);
Sql := 'insert into MATest2 select INDICE, AName, AValue from MATest';
AdoConnection1.Execute(Sql);
end;
procedure TForm1.CreateSourceTable;
var
Sql : String;
begin
Sql := 'create table MATest(INDICE int, AName char(20), AValue char(20))';
AdoConnection1.Execute(Sql);
Sql := 'insert into MATest(INDICE, AName, AValue) values(1, ''aaa'', ''vvv'')';
AdoConnection1.Execute(Sql);
end;
Obviously it would be better to generate your data with the ID fieldname in the first place and avoid all this, but presumably there is a good reason why you can't.
I am working with FireDac under Delphi 10.1 Berlin.
For displaying data to the user i use data aware controls like TDBEdit.
I use TFDQuery and TDataSource to link them with the controls.
This works but long sql queries that take some time to exectute will freeze the GUI.
I am wondering how to stop the gui from freezing while performing those long running queries.
I was thinking about background threads.
On the wiki i read that FireDac can work with multithreads:
http://docwiki.embarcadero.com/RADStudio/XE6/en/Multithreading_(FireDAC)
However in embarcadero community forums thread Jeff Overcash writes:
One thing I didn't see asked or Dmitry mention is you can not have
TDataSource or LiveBindings against your background threaded queries.
If you are background threading a query that displays the results you
should disconnect the LB or DataSource, open and fetch all the data
then re establish the connection.
Those two will be trying to move the cursor on you or querying the
buffer for display while the buffer is very volatile being moved
around in a different thread.
I am wondering if someone that also uses FireDac and displays the values on a form can help me out here.
The code sample below shows one way to retrive records from an MSSql Server
in a background thread using FireDAC. This omits a few details. For example, in practice, rather than the TQueryThreads Execute opening the query only once and then terminating, you would probably want the thread's Execute to contain a while loop in which it waits on a semaphore after the call to Synchronize and then close/re-open the query to update the main thread as often as you want.
type
TForm1 = class;
TQueryThread = class(TThread)
private
FConnection: TFDConnection;
FQuery: TFDQuery;
FForm: TForm1;
published
constructor Create(AForm : TForm1);
destructor Destroy; override;
procedure Execute; override;
procedure TransferData;
property Query : TFDQuery read FQuery;
property Connection : TFDConnection read FConnection;
property Form : TForm1 read FForm;
end;
TForm1 = class(TForm)
FDConnection1: TFDConnection;
FDQuery1: TFDQuery;
DataSource1: TDataSource;
DBGrid1: TDBGrid;
DBNavigator1: TDBNavigator;
Button1: TButton;
procedure Button1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
public
QueryThread : TQueryThread;
end;
[...]
constructor TQueryThread.Create(AForm : TForm1);
begin
inherited Create(True);
FreeOnTerminate := True;
FForm := AForm;
FConnection := TFDConnection.Create(Nil);
FConnection.Params.Assign(Form.FDConnection1.Params);
FConnection.LoginPrompt := False;
FQuery := TFDQuery.Create(Nil);
FQuery.Connection := Connection;
FQuery.SQL.Text := Form.FDQuery1.SQL.Text;
end;
destructor TQueryThread.Destroy;
begin
FQuery.Free;
FConnection.Free;
inherited;
end;
procedure TQueryThread.Execute;
begin
Query.Open;
Synchronize(TransferData);
end;
procedure TQueryThread.TransferData;
begin
Form.FDQuery1.DisableControls;
Form.FDQuery1.Data := Query.Data;
Form.FDQuery1.EnableControls;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
QueryThread.Resume;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
QueryThread := TQueryThread.Create(Self);
end;
MJN's comment about bookmarks tells you how to preserve the current data row position in the gui.
Btw, although I've often done this with TClientDataSets, putting this answer together was the first time I'd tried it with FireDAC. In terms of configuring the components, all I did was to drag the components off the Palette, "wire them together" as you'd expect and then set the FDConnection's Params and the FDQuery's Sql.