A simple Delphi wrapper for Sqlite 3

Most applications use a database, and there are many excellent database engines to choose from, both free and commercial. SQLite is a small C library that has several advantages. It is open source, free, cross-platform, fast, reliable, and well supported. I had a Delphi 7 application using Sqlite 2.0. There are various wrappers available for Delphi, and around 18 months ago I tried all the ones I could get my hands on. Although several of them were of good quality, I found myself running into bugs caused by the complexity of implementing Borland’s TDataset and related database components. Since I didn’t require databinding, I chose a wrapper that implemented very simple access to Sqlite – it was written by Ben Hochstrasser and amended by Pablo Pissanetzky. An advantage for me was that I could easily see what the wrapper did and make my own amendments. I actually made rather a lot of changes, adding basic transaction support and implementing a crude dataset based on a TList. Despite its simplicity, I found it effective and reliable.

The main author of Sqlite, Dr D Richard Hipp, has since released Sqlite 3.0. This adds some useful features, including BLOB support and the ability to create tables that support case-insensitive comparisons. I decided to update my wrapper for Sqlite 3.0. This meant changing the code from using SQLite’s callback interface to use Sqlite3_Prepare and Sqlite3_Step instead (see the description of the Sqlite C interface). Most of the code written by Ben and Pablo has now gone, which I say not to demean their efforts, but to emphasise their innocence. I’ve also had a go at adding BLOB support. The new wrapper is not yet extensively tested, but so far it is working well.

I’m now offering the wrapper for download. You’re welcome to use it, although naturally it comes with no warranty. I’d be grateful for any comments, bug reports or improvements, though I’d like to keep the wrapper simple. Note this is for Delphi 7, not Delphi .NET (though I’ve also used Sqlite with .NET – see here).

More about the wrapper

This wrapper has two units and three main classes. Sqlite3.pas has the external declarations for sqlite3.dll. I’ve included a binary build of sqlite3.dll, made with Visual C++ 2003. It should work with other builds, so you can upgrade to later versions of the DLL without making changes to the wrapper (unless the Sqlite API itself changes).

Sqlitetable3.pas implements three classes, ESqliteException, TSqliteDatabase and TSqliteTable. Currently TSqliteDatabase has the following methods:

GetTable: execute an SQL query and return a resultset as a TSqliteTable.

ExecSQL: execute an SQL query that does not return data.

UpdateBlob: Update a blob field with data from a TStream object.

BeginTransaction, Commit, Rollback: sends SQL statements for transaction support.

TableExists: Returns true if the specified table exists in the database.

There is also an IsTransactionOpen property.

TSqliteTable represents a resultset. It maintains no link to the source database, so it is disconnected: you can keep a TSqliteTable in memory after freeing the source TSqliteDatabase. When created it is set to the first row. Navigate the resultset using Next and Previous, until EOF is True. At BOF the resultset is on the first row, but at EOF there is no valid row. RowCount retrieves the number of rows, which may be zero. To retrieve data, first use FieldIndex to get the index number of a particular field. Then use the appropriate Field… method to get the value: FieldAsString, FieldAsInteger, FieldAsDouble, FieldAsBlob or FieldAsBlobText. For other datatypes such as Currency or TDateTime, you currently need to convert to String or one of the other types – I’m planning to add some more types soon. I’ve not tested the Blob functionality extensively. Since the entire resultset must fit in memory, be cautious about retrieving large resultsets or resultsets with large amounts of Blob data.

Currently the only way to determine if a field contains a null value is with the FieldIsNull method. The other methods return zero, false or empty strings for null values.

I will be publising a basic tutorial on using the wrapper in the UK magazine PC Plus. I will also keep this page up-to-date with the latest version.

Update 19 February 2005: I’ve updated the wrapper for Sqlite 3.1.2. This changes the way column names are returned, so I’ve added a call to set the Pragma full_column_names on. I’ve also amended the field type detection to use the actual type when the declared type is not available, and added the utility function TableExists. The test application now shows a possible way to load, save and display images in a Sqlite database.

Update 15 August 2005: Thanks to Lukas Gebauer who has made the wrapper compatible with Delphi 4+ and added some new methods. See the readme for details. I’ve also followed Lukas’s suggestion in removing FieldAsBool – he points out that it is not a natural sqlite3 type. If this causes problems for anyone, let me know. I’ve left the previous version available for download just in case. Finally, I’ve included a Visual C++ 2003 release build of Sqlite3 version 3.2.2.

Update 27 August 2007: Thanks to Marek Janáč who emailed me to say that the wrapper did not work with the latest Sqlite3 dll (3.4.2). The problem was that Sqlite now requires pathnames to be in UTF8 format when the path contains accented characters. I’ve made a small change to fix this. I’ve also included a new MSVC 6.0 build of the DLL. Finally, I’ve created a repository for the wrapper here:

https://www.itwriting.com/repos/sqlitewrapper/trunk

Update 16 October 2008: Quick update to get Delphi 2009 compatibility – not properly Unicode-enabled though, yet.

Update 4 February 2011: Added support for SQLite backup API. Updated DLL to Sqlite 3.7.5. Compiled with VC++ 10 but with static linking to avoid runtime dependencies.

Update 10 February 2011: Created new Unicode version. This has not been extensively tested, and requires Delphi 2009 or higher on Windows. Need to make this a single code base across all versions. Removed BindData method pending review for Unicode. Modified demo project to add simple navigation. You can download the Unicode version here.

image

Helpful project? Sponsor ITWriting.com for ad-free access to the site

Links

Download the Simple Delphi Wrapper

Download the Unicode version

Sqlite home page

Other Sqlite wrappers including some for Delphi

My notes on using Sqlite 2 with Delphi, .NET and Java

My interview with the main author of Sqlite, Dr D Richard Hipp

172 thoughts on “A simple Delphi wrapper for Sqlite 3”

  1. Hi,

    How is it possible to bind two databases at the same time with the sqlite wrapper? I found something with an attach statement on the sqlite website. But I cannot call it from TSQLiteDatabase with the wrapper. Later I would like to use union over these two databases.

    Thank you very much

    André

  2. Hi,

    Anyone can show some codes how to STORE and RETRIEVE TStrings type of data?

    For example, I have a TStrings variable “SOMETHING”.

    Thanks.

  3. hey Tim,

    your wrapper works perfect! Thanks for that! 🙂

    My problem is value binding as described in the readme.txt.
    First I tried this:

    sSQL := ‘INSERT INTO addressbook( prename,lastname,street,’ +
    ‘housenr,plz,city,telhome,’ +
    ‘handyprivate,telwork,handywork) ‘ +
    ‘VALUES(?,?,?,?,?,?,?,?,?,?) ‘ +
    [edtPrename.Text+’,’+edtLastname.Text+’,’+edtStreet.Text+’,’
    +edtHousenr.Text+’,’+edtPLZ.Text+’,’+edtCity.Text+’,’
    +edtTelHome.Text+’,’+edtHandyPrivate.Text+’,’
    +edtTelwork.Text+’,’+edtHandyWork.Text];
    db.ExcecSQL(sSQL);

    This hasn’t worked, there were a syntax failure.

    Than I tried this, following the example in readme.txt:

    db.ExecSQL(‘INSERT INTO addressbook( prename,lastname,street,’ +
    ‘housenr,plz,city,telhome,’ +
    ‘handyprivate,telwork,handywork) ‘ +
    ‘VALUES(?,?,?,?,?,?,?,?,?,?) ‘ +
    [edtPrename.Text, edtLastname.Text, edtStreet.Text,
    edtHousenr.Text, edtPLZ.Text, edtCity.Text,
    edtTelHome.Text, edtHandyPrivate.Text,
    edtTelwork.Text,edtHandyWork.Text]);
    Unfortunatly this way doesn’t work either! So can you help me? Probably it’s just a little failure… Thanks a lot!

    Greetings!

  4. It can’t be Unicode because I still use Turbo Delphi. 😀

    No, not really I think. Maybe that’s the mistake? I will search in the comments for the fix mentioned thing… Maybe you also have another idea?

    Erik

  5. I try to compile in lazarus/fpc and get thses errors:

    /usr/bin/ld: warning: link.res contains output sections; did you forget -T?
    /usr/bin/ld: cannot find -lsqlite3
    project1.lpr(19,1) Error: Error while linking
    project1.lpr(19,1) Fatal: There were 1 errors compiling module, stopping

    I then copy the sqlite3.so to folder of the project and then retry to compile and get the same error, How I can solve this?

    thank’S

  6. Very nice tool. I’m testing it under Delphi 7.

    Question: How to get a list of all tablenames in a given DB?

    Thanks in advance,
    Werner

  7. List of Tablenames:

    tab:=db.GetTable(‘SELECT name from SQLITE_MASTER where type=”table” order by name’);
    while not tab.EOF do begin
    ListBox1.Items.Add(tab.Fields[0]);
    tab.Next;
    end;
    tab.Free;

  8. @Armando
    you are missing the sqlite devel packages
    copying the sqlite3.so to project folder helps nothing.Linux is not windows

    Simple Delphi Wrapper works well with Lazarus 1.1,the Unicode Version not
    because differences between Delphi and FPC.

  9. Hi Tim,

    I have been fiddling with this wrapper for a while now, and it has been very useful.
    However, I see that you have updated the Unicode version to add the 16 suffix to the DLL mapping for sqlite3_bind_text16 but you have missed the mapping for SQLite3_create_collation16 which I also pointed out on August 29, 2011.
    Recently I discovered a bug in SQLiteTable3.pas in SetParams() on line 719 where the expression
    “length(par.valuedata)” should be “length(par.valuedata) * SizeOf(char)” as my strings were only half the length they should have been 🙂

    Len.

  10. Hey Tim,

    I found a bug as demonstrated in the following code. Basically, the ColType that is being set is preventing me from accessing the data if the first row of the table has a NULL value in that column.

    unit uMain;

    interface

    uses
    Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
    Dialogs, StdCtrls, Grids,

    // For this demo, we need access to the private fColTypes in TSQLiteTable.
    // Added the following property for this purpose:
    // property ColTypes: TList read fColTypes;
    SQLiteTable3;

    const
    g_DATABASE = ‘Sample.db’;

    type
    TForm1 = class(TForm)
    StringGrid1: TStringGrid; // Set Align to alTop
    Button1: TButton;
    Button2: TButton;
    procedure FormCreate(Sender: TObject);
    procedure FormResize(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    private
    procedure LoadDataInGrid;
    procedure SetColWidths;
    end;

    var
    Form1: TForm1;
    SLDB: TSQLiteDatabase;

    implementation

    {$R *.dfm}

    procedure TForm1.FormCreate(Sender: TObject);
    begin
    SetColWidths;

    SLDB := TSQLiteDatabase.Create(g_DataBase);
    try
    SLDB.ExecSql(‘DROP TABLE IF EXISTS “Test”‘);
    SLDB.ExecSql(‘CREATE TABLE “Test” ‘ +
    ‘(“IDKey” INTEGER NOT NULL, ‘ +
    ‘ “Name” Text, ‘ +
    ‘ “Date1” Integer,’ +
    ‘ “Date2” Integer,’ +
    ‘ CONSTRAINT “PK_Test” PRIMARY KEY (“IDKey”) );’);

    SLDB.ExecSql(‘Insert into test (Name, Date1, Date2) ‘ +
    ‘values (“Adam”, DateTime(”Now”), null);’);

    SLDB.ExecSql(‘Insert into test (Name, Date1, Date2) ‘ +
    ‘values (“Bart”, DateTime(”Now”), null);’);

    // Has a date in the third column
    SLDB.ExecSql(‘Insert into test (Name, Date1, Date2) ‘ +
    ‘values (“Carl”, DateTime(”Now”), Date(”Now”));’);

    SLDB.ExecSql(‘Insert into test (Name, Date1, Date2) ‘ +
    ‘values (“Dave”, null, null);’);

    SLDB.ExecSql(‘Insert into test (Name, Date1, Date2) ‘ +
    ‘values (“Eric”, DateTime(”Now”), null);’);

    SLDB.ExecSql(‘Insert into test (Name, Date1, Date2) ‘ +
    ‘values (“Fred”, null, null);’);

    LoadDataInGrid;
    finally
    SLDB.Destroy;
    end;
    end;

    procedure TForm1.FormResize(Sender: TObject);
    begin
    SetColWidths;
    end;

    procedure TForm1.SetColWidths;
    var
    I: integer;
    begin
    for I := 0 to StringGrid1.ColCount – 1 do
    StringGrid1.ColWidths[i] := (Self.ClientWidth – 7) div StringGrid1.ColCount;
    end;

    procedure TForm1.LoadDataInGrid;
    var
    sltb: TSQLiteTable;
    ARow : integer;
    ACol : integer;
    begin
    SLTB := SLDB.GetTable(
    ‘ SELECT ‘ +
    ‘ IDKey, ‘ +
    ‘ Name, ‘ +
    ‘ DateTime(Date1, “LocalTime”) as DateA, ‘ +
    ‘ DateTime(Date2, “LocalTime”) as DateB, ‘ + // ColTypes = dtNull when first row contains NULL
    ‘ coalesce(DateTime(Date1, “LocalTime”), “”) as DateC, ‘ +
    ‘ coalesce(DateTime(Date2, “LocalTime”), “”) as DateD ‘ + // ColTypes = dtStr
    ‘ FROM Test; ‘);

    try
    StringGrid1.Cells[0, 0] := format(‘IDKey (%d)’,[pInteger(sltb.ColTypes[0])^]);
    StringGrid1.Cells[1, 0] := format(‘Name (%d)’,[pInteger(sltb.ColTypes[1])^]);
    StringGrid1.Cells[2, 0] := format(‘DateA (%d)’,[pInteger(sltb.ColTypes[2])^]);
    StringGrid1.Cells[3, 0] := format(‘DateB (%d)’,[pInteger(sltb.ColTypes[3])^]);
    StringGrid1.Cells[4, 0] := format(‘DateC (%d)’,[pInteger(sltb.ColTypes[4])^]);
    StringGrid1.Cells[5, 0] := format(‘DateD (%d)’,[pInteger(sltb.ColTypes[5])^]);

    while not sltb.EOF do
    begin
    ARow := sltb.FieldAsInteger(sltb.FieldIndex[‘IDKey’]);
    for ACol := 0 to sltb.ColCount – 1 do
    StringGrid1.Cells[ACol, ARow] := sltb.FieldAsString(ACol);
    sltb.Next;
    end
    finally
    sltb.Free;
    end;
    end;

    procedure TForm1.Button1Click(Sender: TObject);
    begin
    SLDB := TSQLiteDatabase.Create(g_DataBase);
    try
    SLDB.ExecSql(‘Update Test Set Date2 = DateTime(”Now”) Where IDKey = 1;’);
    LoadDataInGrid;
    finally
    SLDB.Destroy;
    end;
    end;

    procedure TForm1.Button2Click(Sender: TObject);
    begin
    SLDB := TSQLiteDatabase.Create(g_DataBase);
    try
    SLDB.ExecSql(‘Update Test Set Date2 = NULL Where IDKey = 1;’);
    LoadDataInGrid;
    finally
    SLDB.Destroy;
    end;
    end;

    end.

  11. Hi Tim and all,

    I have found that the following line in SQLiteTable3.pas, TSQLiteDatabase.Create constructor does not work for Cyrillic filenames:

    utf8FileName := UTF8String(FileName); // does not work

    My fix is:

    var
    WideStringFileName : WideString;

    WideStringFileName:=FileName;
    utf8FileName:=UTF8Encode(WideStringFileName);

    Not thoroughly tested yet, but now I am able to open a database with Cyrillic filename.

    Great wrapper for the great database system. Thank you.
    Best regards,
    Dmitry.

  12. I was able to get inserts to work, but I am unable to get delete or updates to work

    example:

    procedure TfrmMain.actDeleteExecute(Sender: TObject);
    begin
    DB.AddParamText(‘AItem’, lvwMain.Selected.Caption);
    DB.ExecSql(‘DELETE FROM Inventory WHERE Item = :AItem;’);
    end;

    I even tried hard coding it

    DB.AddParamText(‘AItem’, ‘ab’);
    DB.ExecSql(‘DELETE FROM Inventory WHERE Item = :AItem;’);

    Shane

  13. OK, I got them to work! Get seem to get Dates inserted though and Blobs. Can I see an example of a Date and Blob insert? – Thanx!

  14. Ich habe da eine Fehlermeldung. In der Unit SQLiteTable3 ist angeblich die erste Zeile zu land (>1023 Zeichen). Das ganze unter Delphi 5 standard. Woran kann das liegen?
    Danke.

  15. Just found a small bug related to blob fields when reading values (setting values is working fine). I got the error ‘Not a Blob field’. And I did not get what I’m doing wrong for hours.. But the fix is an easy one once I found the reason. In the (SQLiteTable3 unit) constructor TSQLiteTable.Create the column is set as dtBlob when DeclaredColType = ‘BLOB’ (uppercase). In my case, for whichever reason, the value was ‘blob’ (lowercase). Thus, I needed to replace “if DeclaredColType = ‘BLOB’ then” with “if uppercase(DeclaredColType) = ‘BLOB’ then”. And it works!

  16. In: SQLiteTable3.pas the prototype is:
    procedure TSQLiteDatabase.ExecSQL(const SQL: String); handling 1 Parm
    procedure ExecSQL(Query: TSQLiteQuery); overload; 1 Parm
    The sample in the Readme for value binding shows; 2 parms.
    DB.ExecSQL(‘INSERT INTO [‘+COVER_TBL+’] ‘ +
    ‘([Filename], [Date], [Width], [Height]) VALUES’ +
    ‘(?, ?, ?, ?)’, //This kicks off Parm.2
    [ UTF8Encode(Filename), DateTimeToUnixTime(FileDate),
    Info.CoverWidth, Info.CoverHeight ] );
    My XE2 compiler refuses the compile. (E2034 Too many actual parms) How can I fix this and use the very much required: value binding?

  17. Hi how can i convert my querys from Adoquery to Sql

    with frmDados.TUsuariosLogin do //Laço de consulta por codigo
    Begin
    Close;
    SQL.Clear;
    SQL.Add(‘SELECT * from Usuarios’);
    SQL.Add(‘Where nome LIKE ”%’+ user.text+’%”’);
    SQL.Add(‘order by nome asc’);
    Open;
    end;

  18. Hi, I found a little bug.

    When executing “PRAGMA integrity_check;” to ensure the opened database is an actual valid SQL database, SQLiteTable3 raises an error. This is false and occurs only because the value returned by sqlite.dll is 101 (SQLITE_ROW) instead of 101 (SQLITE_DONE).

    What is wrong is not the 100 answer (it is set on purpose to allow the programmer to read the answered row and get the operation result (1 row with ok value = integrity check succesful, other results or errors = integrity check failed), but it should not raise an error at all.

    My suggestion would be to implement a TSQLiteDatabase.isDatabaseValid function that intercepts the 101 value, reads it and acts accordingly. (it is what I’m going to do on my own.

    Thanks

  19. Dear Tim,
    Thank you for offering an easy-to-use wrapper to access SQLite databases from Delphi. I’m still having problems getting the example of user-defined function to work (unit sqlite3udf). I’m sorry: the example is not complete and is present only as comments. Please: why do you use a procedure and why is it declared as cdecl? And why does it have so many (input) arguments? The first argument is the context – how does it get its value? I only want to define a function called LCASE that will give me the result in lower case. This is to implement compatibility with SQL written for MS Access. Thanks in advance!
    Kind regards,
    Steven

Comments are closed.

Tech Writing