image

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

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

  1. Hi!

    I need to insert a duplicate record into a table. But how to get index of a added record?
    I add the duplicate using this code:
    ExecSQL(‘INSERT INTO Table(*) VALUES (*);’);
    Thank you!

  2. Hi!
    Please tell how to resolve this problem. I can not insert this text:
    ——————-
    Title:=’Some “Text”‘;
    Sqlite.ExecSQL(‘INSERT INTO TableName(title) VALUES (“‘+ Title +'”);’);
    ——————-
    The problem is in this symbol – “

  3. Hi Tim, my name is Pasquale and I have been using your wrapper in my software for several months. I have to say that I really love SQLITE. It’s extremely easy to use and powerful at the same time.

    Unfortunately, I have encountered a problem that is driving me crazy and I really hope you can help me.

    I have developed a piece of software to manage a warehouse. I create the SQLite database within the Form_ Show procedure using the following code:

    ======== START OF CODE ========
    procedure TfrmWarehouse.FormShow(Sender: TObject);
    var
    sSQL: String;
    begin

    DBFile := Trim(ExtractFilePath(application.exename) + ‘Warehouse.db’);
    sl3db.Free;
    sl3db := TSQLiteDatabase.Create(DBFile);

    if not sl3db.TableExists(‘warehouse’) then
    begin

    sSQL := ‘CREATE TABLE warehouse (ID INTEGER PRIMARY KEY,varCode BLOB,’;
    sSQL := sSQL + ‘varCategory BLOB,’;
    sSQL := sSQL + ‘varTrademark BLOB,’;
    sSQL := sSQL + ‘varDescription BLOB,’;
    sSQL := sSQL + ‘varPrice BLOB,’;
    sSQL := sSQL + ‘varLocation BLOB,Picture BLOB COLLATE NOCASE,Ext TEXT)’;

    sl3db.execsql(sSQL);

    end else
    begin
    sl3tb.Free;
    sl3tb := sl3db.GetTable(‘SELECT * FROM warehouse’);
    sl3tb.MoveFirst;
    if sl3tb.Count > 0 then
    DisplayRecord;
    end;

    end;
    ======== END OF CODE ========

    Now, to search for a record in the database, I use a new form (frmSearchArticle) in which I insert exactly the same code as above within the TfrmSearchArticle.FormShow procedure.

    The problem is, now that my database contains more than 2000 records, when I load the search form, I get an error message telling me that the memory is insufficient and, consequently, the records are not loaded.

    I think this depends on the fact that, when I load the search form, I create the database a second time, but if I deleted the line:

    sl3db := TSQLiteDatabase.Create(DBFile);

    the search form could not have access to the database.

    Do you know how I could solve my problem? I will appreciate any help you may want to give me.

    Thanks in advance.

    Pasquale

  4. I have noticed that the problem happens when the code I posted in my previous mail is repeated in a DIFFERENT form. In fact, when I duplicated the search code in a button contained in the main form (so, whithout opening a new form), the program worked fine.

    How is it possible that the memory available becomes insufficient only when the database and table are loaded again in a new form and not in the main form?

    I’m probably missing something elementary. Can anyone help me?

    Thanks again.

    Pasquale

  5. I am new to Delphi, learning it for 2 month now.
    This wrapper is genius, i got it directly to work, without any problems i am able to add very gig data to a db. Also retriving the data is easy. The only problem i have is to fill a list like “Name=Miller” if there are 8 “Miller” in the Database i can only get the first. I can see that the fRowCount is 8 but i cont get that into a list, i cant increase fRow.
    Has anyone a solution?

    Regards

    Deali

  6. Hi All,

    Is this sqlite wrapper built for multi-threading? I’ve read on the sqlite website that either sqlite is compiled for multi-threading or it can be changed at runtime; but I belive it requires either sql_open_v2 or the sql_config interfaces, neither of which appear to be available from this component.

    An excellent component!

    Thanks,

    Paul

  7. Hi!

    Thank you very much for your SQLite wrapper! It’s simple and runs very good regarding my few tests.
    I’m quite new to SQL and SQLite so I’m still playing around and learning. So as I understand it’s “good taste” to use UPPERCASE for SQL identifiers, but it’s not required. Is this correct?
    At least any SQL shell I tested had no problems with lowercase statements.

    The column type detection of this wrapper is case sensitive.
    A column declared as “ID INTEGER NOT NULL” is an integer column, but “ID integer not null” is reported as NOT being integer and FieldAsInteger() fails.

    Should this be fixed inside the wrapper or am I wrong?

    As I’m using some “old” tables I created by hand in lowercase, I’m going to recreate them in uppercase as a workaround.

    Thanks anyway for your nice work!

    //arcus

  8. Marcus

    Probably all you need to do is to amend this line in TSQLiteTable.Create, in SQLiteTable3.pas:

    DeclaredColType := Sqlite3_ColumnDeclType16(stmt, i);

    to

    DeclaredColType := UpperCase(Sqlite3_ColumnDeclType16(stmt, i));

    If you are not using the Unicode version, amend accordingly.

    SQL is normally not case-senstive.

    Tim

  9. Code:
    slDBPath := ‘c:pasdiplomdiplom.db’;
    sldb := TSQLiteDatabase.Create(slDBPath);
    sldb.ExecSQL(‘update D_PROF set NAME = :NAME where ID = :ID;’,[‘soft’,767]);

    or

    sldb.AddParamText(‘NAME’,’soft’));
    sldb.AddParamInt(‘ID’,767);
    sldb.ExecSQL(‘update D_PROF set NAME = :NAME where ID = :ID;’);

    Error executing SQL statement
    Error[1]: SQL error or missing database.

    Help me.

  10. Hi thank’s for this great sqlite wrapper,

    i hade somme errors while freeing tables and databases in loops:

    using
    sltb.free;
    SLdb.free;

    so i use
    FreeAndNil(sltb);
    FreeAndNil(sldb);

  11. Hi there,

    Just getting started with SQLite using this wrapper, and it seems to me that a couple of the ’16’ suffixes have been left off the DLL references in the Unicode version, these being

    sqlite3_bind_text16 => external SQLiteDLL name ‘sqlite3_bind_text’;
    SQLite3_create_collation16 => external SQLiteDLL name ‘sqlite3_create_collation’;

    Thanks for the wrapper, Len.

  12. Hi Tim,

    I’m checking out your wrapper (the unicode version), thanks for posting it.

    Re:
    “TSqliteTable represents a resultset. It maintains no link to the source database”

    Is there anyway that a linked table can be created?
    If I update the database, I have to rerun the query and then locate the record I was editing to make the updates visible.

    thanks, Steven

  13. >That’s not within the scope of this wrapper unfortunately – hence the “simple”!
    Thanks for the reply, No problem, I’ll work around it.

    A note about the Unicode version (http://www.itwriting.com/blog/3822-using-sqlite-3-with-unicode-in-delphi.html)

    I’m using it with Delphi2010 and I’m having the what appears to be same memory leak issues as reported by John
    on August 18, 2008
    http://www.itwriting.com/blog/articles/a-simple-delphi-wrapper-for-sqlite-3/comment-page-1#comment-105702

    When I had all the code in a single procedure or function – no reported leaks.
    Now that I declared the TSQLiteDatabase object as a global variable it looks like I’m having a leak whenever I access the database object – for example calling TableExists(‘sometable’).

    Eurekalog reports the leaks at:
    SQLiteTable3.pas Class: TSQLiteDatbase Method: GetTable Line: 531
    SQLiteTable3.pas Class: TSQLiteTable Method: Create Line: 809
    SQLiteTable3.pas Class: TSQLiteDatbase Method: TableExists Line: 622
    etc.
    Be happy to send you the report if you wish.

    Best regards,
    …Steven

  14. Email sent to tim(at)itwriting.com gets returned as undeliverable.
    If you wish you can use the email I logged this comment with to send me your email.

  15. Looks like the email was being rejected because of the attachment.
    I put a zip containing the demo file package on one my websites and sent you an email with a link so that you can download it.

    Best regards,
    …Steven

    1. Steven

      Whenever you create a TSqliteTable you have to free it. I found one instance in your code where this is not done. Then the project runs without leaks for me (I don’t have Eureka but using ReportMemoryLeaksOnShutdown).

      Tim

  16. Hi Tim,

    Appreciate you checking it out.

    Frankly I’m stumped.
    There are 4 procedures in the demo where I’m creating a TSqliteTable table and I’m freeing the table on each of those 4 areas before leaving the procedure.
    It must be in front of my face and I’m just not seeing it.

    procedure TmmCAT_SQLiteDB.BackupCats;
    var
    sl3tbl: TSqliteTable;

    begin
    fsldb.execsql(‘DROP TABLE IF EXISTS ‘ + cTblCatBU);
    CreateTable_CATBU;

    try
    sl3tbl := fsldb.GetTable(‘SELECT * FROM ‘ + cTblCategories);
    ….
    ….
    finally
    sl3tbl.Free;
    end;
    end;

    Sorry to be such a bother.

    1. Did you get my email? It’s in the routine LoadDB

      procedure TmmCAT_SQLiteDB.LoadDB();
      var
      sl3tbl: TSqliteTable;
      sTmp :string;
      begin
      if fDBsLoaded then
      Exit;

      fsldb := TSQLiteDatabase.Create(fDBFilePath);
      CreateTables;

      try
      sl3tbl := fsldb.GetTable(‘SELECT * FROM ‘ + cTblCategories);
      fNumCatsDefined := sl3tbl.Count;

      //tim added to fix leak
      sl3tbl.Free;

      sl3tbl := fsldb.GetTable(‘SELECT * FROM ‘ + cTblStats + ‘ WHERE SType = “PRIMARY”‘);

      fLUPDate := 0;
      if sl3tbl.Count = 0 then
      Exit;

      sl3tbl.MoveFirst;
      try
      sTmp := sl3tbl.FieldAsString(sl3tbl.FieldIndex[‘LUPDate’]);
      fLUPDate := StrToFloat(sTmp);
      except
      Exit;
      end;

      finally
      sl3tbl.Free;
      end;
      fDBsLoaded := True;
      end;

  17. Tim,

    Thanks so much for your help.
    Your fix solved the problem.

    I was making a wrong assumption about reusing the TSqliteTable object.
    Now that you’ve pointed it out it seem pretty obvious.

    Apologies about missing your email.

    Best regards,

    Steven Brenner

  18. Hi,
    I’m using this wrapper and I like it. But what about multiuser access?
    What’s happens if I will use one DB file by 2 threads?

  19. Hi,
    I’m using the wrapper and I think I have spotted an error.
    When the database is locked and I call TSqliteTable.Create the database returns SQLITE_BUSY.
    But the code here raises en exception with the text ‘Could not prepare SQL statement’. I have tried to call TSQLiteDatabase.RaiseError instead and that gives a proper message including the error code indicating that the database is in fact locked!
    Can you confirm my view (or tell me what I don’t understand!)

    Olle

  20. I encountered an unexpected “constraint failed” error when using a parameterized INSERT command. Here is a minimal code example:


    procedure Test(const F: TFilename);
    var
    db: TSQLiteDatabase;
    sql: string;
    begin
    db := TSQLiteDatabase.Create(F);
    try
    sql := 'CREATE TABLE test ( foo TEXT NOT NULL, CHECK (foo = ''bar'') )';
    db.ExecSQL(sql);
    sql := 'INSERT INTO test (foo) VALUES (@param)';
    db.AddParamText('@param', 'bar');
    db.ExecSQL(sql);
    finally
    db.Free;
    end;
    end;

    I would expect that the INSERT command is successful, but I recieve an error message stating a violation of the CHECK constraint. A direct INSERT command without a parameter works as expected:


    sql := 'INSERT INTO test (foo) VALUES (''bar'')';

    I am using Delphi XE 2 and Sqlite 3.7.10.

    Dieter

  21. Dieter

    This is an error in SQLite3.pas. The declaration of sqlite3_bind_text16 is wrong. Should be:


    function sqlite3_bind_text16(hStmt: TSqliteStmt; ParamNum: integer;
    Text: PChar; numBytes: integer; ptrDestructor: TSQLite3Destructor): integer;
    cdecl; external SQLiteDLL name 'sqlite3_bind_text16';

    I have updated the unicode zip.

    Tim

Leave a Reply

Your email address will not be published. Required fields are marked *

Tech Writing