Table locks in SQLite accessed by fireDAC - sqlite

I'm working on porting a set of paradox tables to SQLite. In order to do so, I created a test application that simulates (somewhat) the current usage scenario: multiple users accessing the same DB file and performing simultaneous read and writes.
The application is very simple: it will start several threads that each create a connection, opens a table and will randomly read, update or insert inside the table.
Almost immediately, The application encounters a "database table locked" error. I have tried several things to attempt to work around it but nothing seems to work. What am I doing wrong ?
Here is the code internal to the threads:
procedure testDB(TargetFolder: string);
var
Conn: TFDConnection;
Table: TFDTable;
i: Integer;
begin
randomize;
Conn := TFDConnection.Create(nil);
try
Conn.DriverName := 'SQLite';
Conn.LoginPrompt := false;
Conn.Params.clear;
Conn.Params.Database := TPath.Combine(TargetFolder, 'testDB.sdb');
Conn.Params.Add('DriverID=SQLite');
// all this is the result of several attemp to fix the table locking error. none worked
Conn.Params.Add('LockingMode=Normal');
Conn.Params.Add('Synchronous=Normal');
Conn.UpdateOptions.UpdateMode := TUpdateMode.upWhereAll;
Conn.UpdateOptions.LockWait := True;
Conn.UpdateOptions.LockMode := TFDLockMode.lmPessimistic;
Conn.UpdateOptions.LockPoint := TFDLockPoint.lpImmediate;
Conn.UpdateOptions.AssignedValues := [uvLockMode,uvLockPoint,uvLockWait];
Conn.Open();
Conn.ExecSQL('CREATE TABLE IF NOT EXISTS ''test'' (''ID'' INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,''data1'' TEXT NOT NULL,''data2'' INTEGER NOT NULL)');
Table := TFDTable.Create(nil);
try
table.Connection := Conn;
while True do
begin
case Trunc(Random(10)) of
0..3:
begin
table.Open('test');
try
if table.Locate('data1', 'name'+intToStr(Trunc(Random(10))),[TLocateOption.loCaseInsensitive]) then
begin
table.Edit;
table.FieldByName('data2').AsInteger := table.FieldByName('data2').AsInteger + 1;
table.Post;
end;
finally
table.close;
end;
end;
4..8:
begin
table.Open('test');
try
i := Trunc(Random(10));
if not table.Locate('data1', 'name'+ i.ToString,[TLocateOption.loCaseInsensitive]) then
begin
table.AppendRecord([null, 'name'+ i.ToString, 0]);
end;
finally
table.close;
end;
end
else
break;
end;
end;
finally
FreeAndNil(Table);
end;
finally
FreeAndNil(Conn);
end;
end;

Thanks to Victoria, I managed to find the right parameters.
Conn := TFDConnection.Create(nil);
try
Conn.DriverName := 'SQLite';
Conn.LoginPrompt := false;
Conn.Params.clear;
Conn.Params.Database := TPath.Combine(TargetFolder, 'testDB.sdb');
Conn.Params.Add('DriverID=SQLite');
Conn.Params.Add('SharedCache=False');
Conn.Params.Add('LockingMode=Normal');
Conn.Params.Add('Synchronous=Normal');
Conn.UpdateOptions.LockWait := True;
Conn.Open();
Thanks again

Related

Error using SQLite ATTACH - DETACH in Delphi

I experience a, for me unsolvable, problem with ATTACH and DETACH in SQLite, using Delphi (Firedac).
I have one database file connected and attach a second one with:
FDConnection1.ExecSQL('ATTACH DATABASE "' + Import_DB_filename + '" AS IMPORTDB;');
Therein, the variable 'Import_DB_filename' contains the full path and filename of the database file.
This works OK and I can access both databases within the connection through the FireDac queries, and can do my coding without problems.
However, things go wrong upon detaching:
FDConnection1.ExecSQL('DETACH DATABASE IMPORTDB;');
In debugging mode, I always get the error:
Debugger Exception Notification
E Project My_Program.EXE raised exception class $C0000005 with message 'access violation at 0x00405d7b: read of address 0x00000000'.
Apparently something goes wrong with the memory assignments, since the debugger stops in a (assembly) function SysFreeMem(P:Pointer): Integer; in GETMEM.INC.
Whatever I try, the error persists and associates a memory leak that eventually leads to a crash of the compiler (Delphi Seattle Enterprise).
Even attaching and subsequent detaching of the database without passing any code results in the same error.
(FDconnection: locking mode = lmNormal;
JournalMode = jmOff or jmWALL or jmdelete)
I do hope that you can help me out on this lasting problem.
If you run the project below, you should find that:
You can access two Sqlite databases quite happily using separate FDConnections and FDTables.
You can move data from a table in one db to a table of the same name in the other using a FireDAC FDDataMove component.
Code:
unit BatchMoveu;
interface
[...]
type
TForm3 = class(TForm)
FDConnection1: TFDConnection;
DBGrid1: TDBGrid;
DataSource1: TDataSource;
DBNavigator1: TDBNavigator;
FDGUIxWaitCursor1: TFDGUIxWaitCursor;
FDPhysSQLiteDriverLink1: TFDPhysSQLiteDriverLink;
Button1: TButton;
FDTable1: TFDTable;
FDConnection2: TFDConnection;
DataSource2: TDataSource;
DBGrid2: TDBGrid;
btnBatchMove: TButton;
FDDataMove1: TFDDataMove;
FDTable2: TFDTable;
procedure btnBatchMoveClick(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure Button1Click(Sender: TObject);
private
procedure PopulateTable1;
procedure TestDataMove;
public
procedure CreateDatabase(DBName : String; FDConnection : TFDConnection;
FDTable : TFDTable);
end;
var
Form3: TForm3;
implementation
{$R *.dfm}
const
DBName1 = 'd:\delphi\code\sqlite\db1.sqlite';
DBName2 = 'd:\delphi\code\sqlite\db2.sqlite';
procedure TForm3.Button1Click(Sender: TObject);
begin
CreateDatabase(DBName1, FDConnection1, FDTable1);
CreateDatabase(DBName2, FDConnection2, FDTable2);
PopulateTable1;
FDTable2.Open;
end;
procedure TForm3.CreateDatabase(DBName : String; FDConnection : TFDConnection;
FDTable : TFDTable);
var
AField : TField;
i : Integer;
begin
if FileExists(DBName) then
DeleteFile(DBName);
AField := TLargeIntField.Create(Self);
AField.FieldName := 'ID';
AField.DataSet := FDTable;
AField.Name := AField.DataSet.Name + 'IDField';
AField := TWideStringField.Create(Self);
AField.Size := 80;
AField.FieldName := 'Name';
AField.DataSet := FDTable;
AField.Name := AField.DataSet.Name + 'NameField';
FDConnection.Params.Values['database'] := DBName;
FDConnection.Connected:= True;
FDTable.CreateTable(False, [tpTable]);
end;
procedure TForm3.PopulateTable1;
var
i : Integer;
begin
FDTable1.Open;
for i:= 1 to 1000 do begin
FDTable1.InsertRecord([i, 'Row ' + IntToStr(i)]);
end;
FDTable1.Close;
//FDConnection1.Commit;
FDTable1.Open;
end;
procedure TForm3.TestDataMove;
var
Item : TFdMappingItem;
begin
Item := FDDataMove1.Mappings.Add;
Item.SourceFieldName := 'ID';
Item.DestinationFieldName := 'ID';
Item := FDDataMove1.Mappings.Add;
Item.SourceFieldName := 'Name';
Item.DestinationFieldName := 'Name';
FDDataMove1.Source := FDTable1;
FDDataMove1.Destination := FDTable2;
FDDataMove1.Options := FDDataMove1.Options - [poOptimiseSrc];
FDDataMove1.Execute;
FDConnection2.Connected := False;
FDTable2.Open;
end;
procedure TForm3.btnBatchMoveClick(Sender: TObject);
begin
TestDataMove;
end;
procedure TForm3.FormDestroy(Sender: TObject);
begin
FDConnection1.Close;
end;
end.

open, fetch, into multiple variables

I am trying to get 2 variables out of a cursor without using a loop.
CREATE OR REPLACE PROCEDURE NAK.SET_ORDERS(P_ORDER_ID NAK.ORDER_ID%TYPE)
CURSOR C_GET_ORDER_NO IS
SELECT O.ORDER_ID, O.ORDER_MAL FROM NAK.ORDERS O WHERE O.ORDER_ID = P_ORDER_ID;
BEGIN
V_ORDER_SEQ := NULL;
V_ORDER_MAL := NULL;
OPEN C_GET_ORDER_NO;
FETCH C_GET_ORDER_NO VALUES(O.ORDER_ID, O.ORDER_MAL)
INTO (V_ORDER_ID, V_ORDER_MAL);
CLOSE C_GET_ORDER_NO;
END;
Do you really need an explicit cursor? You can simply do this:
CREATE OR REPLACE PROCEDURE NAK.SET_ORDERS(P_ORDER_ID IN NAK.ORDER_ID%TYPE)
V_ORDER_SEQ := NULL;
V_ORDER_MAL := NULL;
BEGIN
SELECT O.ORDER_ID,
O.ORDER_MAL
INTO V_ORDER_SEQ,
V_ORDER_MAL
FROM NAK.ORDERS O
WHERE O.ORDER_ID = P_ORDER_ID;
EXCEPTION
WHEN NO_DATA_FOUND THEN
dbms_output.put_line("No record found");
WHEN TOO_MANY_ROWS THEN
dbms_output.put_line("More than one record found");
WHEN OTHER THEN
dbms_output.put_line("Other problem happend");
END;
Important: this procedure will return a exception if the query doesn't return exactly one record. (ORA-01403: no data found or ORA-00913: too many values)
Alternatively you should be able to make something like:
CREATE OR REPLACE PROCEDURE NAK.SET_ORDERS(P_ORDER_ID NAK.ORDER_ID%TYPE)
CURSOR C_GET_ORDER_NO IS
SELECT O.ORDER_ID,
O.ORDER_MAL
FROM NAK.ORDERS O
WHERE O.ORDER_ID = P_ORDER_ID;
BEGIN
V_ORDER_SEQ := NULL;
V_ORDER_MAL := NULL;
OPEN C_GET_ORDER_NO;
FETCH C_GET_ORDER_NO INTO V_ORDER_ID, V_ORDER_MAL;
CLOSE C_GET_ORDER_NO;
END;

Cipher: failed to reserve an envelope space

I am attempting to encrypt/decrypt a SQLite database via FireDAC in a Delphi XE7 application running on Windows 7 (64 bit).
The code looks like this:
Procedure TMain.ActionBtnClick(Sender: TObject);
Begin
If ActionBtn.Caption = 'Encrypt' Then
Begin
SetPassword;
FDSQLiteSecurity.SetPassword;
End
Else
FDSQLiteSecurity.RemovePassword;
SetStatus;
End;
Procedure TMain.DBNamePropertiesButtonClick(Sender: TObject; AButtonIndex: Integer);
Begin
If OpenDialog.Execute Then
Begin
DBName.Text := OpenDialog.FileName;
SetStatus;
End;
End;
Procedure TMain.FormClose(Sender: TObject; Var Action: TCloseAction);
Var
Reg: TRegistry;
Begin
Reg := TRegistry.Create;
Try
Reg.OpenKey('\SQLiteSecurity', True);
Reg.WriteString('Database', DBName.Text);
Finally
Reg.CloseKey;
Reg.Free;
End;
End;
Procedure TMain.FormShow(Sender: TObject);
Var
Reg: TRegistry;
Begin
DBStatus.Caption := '';
Reg := TRegistry.Create;
Try
Reg.OpenKey('\SQLiteSecurity', True);
If Reg.ValueExists('Database') Then
Begin
DBName.Text := Reg.ReadString('Database');
SetStatus;
End;
Finally
Reg.CloseKey;
Reg.Free;
End;
End;
Procedure TMain.SetPassword;
Var
s: String;
Begin
FDSQLiteSecurity.Database := DBName.Text;
BEK(s);
FDSQLiteSecurity.Password := s;
End;
Procedure TMain.SetStatus;
Begin
DBStatus.Caption := FDSQLiteSecurity.CheckEncryption;
If DBStatus.Caption = '<unencrypted>' Then
ActionBtn.Caption := 'Encrypt'
Else
ActionBtn.Caption := 'Decrypt';
End;
When trying to encrypt, at the line that reads "FDSQLiteSecurity.SetPassword;", I get the following error message:
[FireDAC][Phys][SQLite] ERROR: Cipher: failed to reserve an envelope space.
I have tried to find the meaning of this error message without success. Does anyone know what the error message from SQLite is trying to tell me?
TFDSQLiteSecurityOptions FireDAC.Phys.SQLite.TFDSQLiteSecurity.Options
Have you set option soSetLargeCache ?
Use the Options property to specify the database encryption options.
Due to current SQLite encryption limitation the SetPassword / ChangePassword / RemovePassword calls will fail, if the database has blob fields with a value size greater than 1 DB page, and the database does not fit into the SQLite cache.
If soSetLargeCache is set, then SetPassword / ChangePassword / RemovePassword automatically set the cache size greater than the DB size, to fit the database into the memory in full.
If the DB size is greater than the accessible system memory, then the corresponding call fails.

SQLite insert on android works only once

I'm with a problem on Delphi, i create a simple app to test the mobile power of RadStudio, I created a simple app that put some data into some inputs and then add it to database when button is clicked. I followed this Embarcadero tutorial as starting point
The problem is that I only get one entry added, then no more entries are added or the list is not refreshed. Below some code:
Table creation:
procedure TTabbedForm.logAfterConnect(Sender: TObject);
begin
log.ExecuteDirect('CREATE TABLE IF NOT EXISTS lista (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,idCons INTEGER,nome TEXT,kms INTEGER,kmsAlarme INTEGER,quantidade INTEGER,quantidadeAlarme INTEGER,data INTEGER,dataAlarme INTEGER,alarmeMsg TEXT)');
end;
Add button code:
procedure TTabbedForm.btnGravarClick(Sender: TObject);
begin
try
SQLQueryInsert.ParamByName('idCons').AsInteger := PopupBoxTipo.ItemIndex;
SQLQueryInsert.ParamByName('nome').AsString := PopupBoxTipo.Text;
SQLQueryInsert.ParamByName('kms').AsInteger := StrToInt(kmsEdit.Text);
SQLQueryInsert.ParamByName('quantidade').AsInteger := StrToInt(qtdEdit.Text);
SQLQueryInsert.ParamByName('data').AsInteger := DateTimeToUnix(dtaEvento.Date);
SQLQueryInsert.ExecSQL();
lista.Refresh;
LinkFillControlToField1.BindList.FillList;
except
on e: Exception do
begin
ShowMessage(e.Message);
end;
end;
end;
If you need some more code snippet, please ask!
Thanks in advance for any reply!
Try this method it works for me
procedure TData.InsertItem(someObject: TSomeObjectClass);
var
qry: TFDQuery;
begin
qry := CreateQry( 'insert into SomeObject(id, description, something)'+
' values (:id, :description, :something);', false);
qry.Params.ParamByName('id').AsInteger := someObject.id;
qry.Params.ParamByName('description').asstring := someObject.description;
qry.Params.ParamByName('something').asstring := someObject.something;
qry.Prepare;
qry.execsql;
qry.Free;
end;
I've put and object in the parameter but you can also put the data you want to insert seperatly

PLSQL looping through hierarchy

I am trying to figure out how loop through a hierarchy, I don't know how to put in PLSQL. What I am trying to achieve: I want to know what department is 10 steps above me in a hierarchy:
Say I have a table with a department and a parent department. I want to perform this kind of operation:
select my_department from table_departments as v_department
FOR counter in 1...9
LOOP
v_department:=
(SELECT parent_department
FROM table_department_hierarchy
WHERE child_department=v_department)
END LOOP as top_department;
I can't figure out the correct syntax, is there a brave soul out there who can help me?
Your method with corrected PL/SQL syntax would be something like:
begin
select my_department into v_department from table_departments;
FOR counter in 1...9
LOOP
SELECT parent_department
INTO v_department
FROM table_department_hierarchy
WHERE child_department=v_department;
END LOOP:
END;
However you could perhaps get it all in one statement something like this:
SELECT parent_department
INTO v_department
FROM
( SELECT parent_department, level as lvl
FROM table_department_hierarchy
CONNECT BY child_department = PRIOR parent_department
START WITH child_department = v_department
)
WHERE lvl = 9;
See Oracle docs on hierarchical queries
This is a large pl/sql procedure that i wrote a long while ago that was meant to traverse a employee/boss reporting tree all the way to the top (CEO). This version was specific to Peoplesoft but it as long as your reading something that has a parent/child relationship in a record it will work on anything.... I have other more dynamic versions of this but this maybe the simplest to decipher. I removed some fluff stuff that you won't care about. Also this particular solution delivers to a table for many different reasons because reporting tools can consume it...
Also it determines levels dynamically so you don't have to know how many levels there are as you would with a connect by solution.
Hope it helps:
CREATE OR REPLACE PROCEDURE DW."SPW_T_RESOURCE_HIERARCHY" (iCommit In Integer Default 1000,
pdBegin In Date Default trunc(Sysdate) - 3,
pdEnd In Date Default trunc(Sysdate) + 1,
vTruncate In Varchar2 Default 'Y' ) Is
------------------------------------------------------
-- DECLARATIONS
------------------------------------------------------
Cursor curDataSource Is
---**********************************----
---****BEGIN CUSTOMIZE THIS BLOCK****----
---**********************************----
Select
Eh.empl_id F_DESCENDANT_ID
,Eh.Super_Empl_Id F_IMMEDIATE_ANCESTOR_ID
From
Employee_Header EH
Where
EH.SUPER_EMPL_ID IS NOT NULL OR EH.TERM_DATE IS NULL;
---**********************************----
---****END CUSTOMIZE THIS BLOCK******----
---**********************************----
dNow Date := Sysdate;
iTotalRows Integer := 0;
iTotalErrors Integer := 0;
---**********************************----
---****BEGIN CUSTOMIZE THIS BLOCK****----
---**********************************----
vDescendentID Varchar2(20);
iDescendentLevel Integer := 1;
iAncestorLevel Integer := 0;
vAncestorID Varchar2(20);
vTmpAncestorID Varchar2(20);
vTmpEmployeeID Varchar2(20);
---**********************************----
---****END CUSTOMIZE THIS BLOCK******----
---**********************************----
------------------------------------------------------
-- END DECLARATIONS
------------------------------------------------------
------------------------------------------------------
-- BEGIN MAIN
------------------------------------------------------
Begin
-- Loop over source records
For recDataSource In curDataSource
Loop
iDescendentLevel := 1;
vAncestorID := recDataSource.f_Immediate_Ancestor_Id;
-- Start Transaction
Begin
while (Trim(vAncestorID) is not null)
loop
Begin
-- Fetch Next Ancestor
Select EH.SUPER_EMPL_ID
Into vTmpAncestorID
From
EMPLOYEE_HEADER EH
Where
EH.EMPL_ID = vAncestorID;
Exception
When Others Then
vTmpAncestorID := null;
End;
If NVL(vTmpAncestorID,'-XYZ-') = NVL(vAncestorID,'-123-') Then
vTmpAncestorID := null;
End If;
vAncestorID := vTmpAncestorID;
iDescendentLevel := iDescendentLevel + 1;
end loop;
-- Insert Resource Base
Insert Into T_RESOURCE_HIERARCHY
(
T_RESOURCE_HIERARCHY.F_HIERARCHY_NAME,
T_RESOURCE_HIERARCHY.F_DESCENDANT_LEVEL,
T_RESOURCE_HIERARCHY.F_DESCENDANT_ID,
T_RESOURCE_HIERARCHY.F_ANCESTOR_LEVEL,
T_RESOURCE_HIERARCHY.F_ANCESTOR_ID
)
Values
(
'Physical Org Chart',
iDescendentLevel,
recDataSource.f_Descendant_Id,
To_Number(Decode(iDescendentLevel,1,2,iDescendentLevel) - 1),
NVL(recDataSource.f_Immediate_Ancestor_Id,'ROOT')
);
-- Insert MySelf Into Resource Base as well for full hierarchy research
Insert Into T_RESOURCE_HIERARCHY
(
T_RESOURCE_HIERARCHY.F_HIERARCHY_NAME,
T_RESOURCE_HIERARCHY.F_DESCENDANT_LEVEL,
T_RESOURCE_HIERARCHY.F_DESCENDANT_ID,
T_RESOURCE_HIERARCHY.F_ANCESTOR_LEVEL,
T_RESOURCE_HIERARCHY.F_ANCESTOR_ID
)
Values
(
'Physical Org Chart',
iDescendentLevel,
recDataSource.f_Descendant_Id,
iDescendentLevel,
NVL(recDataSource.f_Descendant_Id,'ROOT')
);
-- Now Its Time To Climb The Tree
-- For This Employee
vAncestorID := recDataSource.f_Immediate_Ancestor_Id;
iAncestorLevel := iDescendentLevel-1;
vTmpAncestorID := null;
-- Loop over parents
while (Trim(vAncestorID) is not null)
loop
Begin
-- Fetch Next Ancestor
Select EH.SUPER_EMPL_ID
Into vTmpAncestorID
From
EMPLOYEE_HEADER EH
Where
EH.EMPL_ID = vAncestorID;
Exception
When Others Then
vTmpAncestorID := null;
End;
If NVL(vTmpAncestorID,'-XYZ-') = '-XYZ-' Then
vTmpAncestorID := null;
End If;
vAncestorID := vTmpAncestorID;
iAncestorLevel := iAncestorLevel - 1;
If vAncestorID is not null Then
-- Insert Resource Base
Insert Into T_RESOURCE_HIERARCHY
(
T_RESOURCE_HIERARCHY.F_HIERARCHY_NAME,
T_RESOURCE_HIERARCHY.F_DESCENDANT_LEVEL,
T_RESOURCE_HIERARCHY.F_DESCENDANT_ID,
T_RESOURCE_HIERARCHY.F_ANCESTOR_LEVEL,
T_RESOURCE_HIERARCHY.F_ANCESTOR_ID
)
Values
(
'Physical Org Chart',
iDescendentLevel,
recDataSource.f_Descendant_Id,
iAncestorLevel,
vAncestorID
);
End If;
end loop;
-- TRANSACTION EXCEPTION HANDLING
Exception
When Others Then
End;
-- ASSIGN HOW MANY RECORDS PROCESSED
iTotalRows := curDataSource%Rowcount;
-- CONDITIONAL/INCREMENTAL TRANSACTION COMMIT
If Mod(iTotalRows, iCommit) = 0
Then
Commit;
End If;
End Loop;
-- FINAL COMMIT AND MD UPDATE
Commit;
-- MAIN EXCEPTION HANDLING
Exception
When Others Then
Begin
iExceptionCode := Sqlcode;
vExceptionMessage := Sqlerrm;
Raise_application_error(Sqlcode, Sqlerrm);
End;
------------------------------------------------------
-- END MAIN
------------------------------------------------------
End SPW_T_RESOURCE_HIERARCHY;
/
Please, check the following example. Not tested, but believe :)
DECLARE
G_EMPLOYEE_ID NUMBER:=1880;
FUNCTION GET_MANAGER(V_EMPLOYEE_ID NUMBER) RETURN NUMBER IS
V_MANAGER_ID NUMBER;
BEGIN
SELECT ID_MANAGER INTO V_MANAGER_ID FROM EMPLOYEES WHERE EMPLOYEE_ID = V_EMPLOYEE_ID;
RETURN V_MANAGER_ID;
EXCEPTION
WHEN OTHERS THEN
RETURN NULL;
END;
BEGIN
LOOP
DBMS_OUTPUT.PUT_LINE('EMPLOYEE:' || G_EMPLOYEE_ID);
G_EMPLOYEE_ID := GET_MANAGER(G_EMPLOYEE_ID);
DBMS_OUTPUT.PUT_LINE('MANAGER:' || G_EMPLOYEE_ID);
EXIT WHEN G_EMPLOYEE_ID IS NULL;
END LOOP;
END;
Another great option (primary) is CONNECT BY, START WITH

Resources