{ An oop wrapper for the sqlite3 database library.

  Copyright (C) 2010 by Simon Ameis <simon (dot) ameis (at) web (dot) de>

  This library is free software; you can redistribute it and/or modify it
  under the terms of the GNU Library General Public License as published by
  the Free Software Foundation; either version 2 of the License, or (at your
  option) any later version with the following modification:

  As a special exception, the copyright holders of this library give you
  permission to link this library with independent modules to produce an
  executable, regardless of the license terms of these independent modules,and
  to copy and distribute the resulting executable under terms of your choice,
  provided that you also meet, for each linked independent module, the terms
  and conditions of the license of that module. An independent module is a
  module which is not derived from or based on this library. If you modify
  this library, you may extend this exception to your version of the library,
  but you are not obligated to do so. If you do not wish to do so, delete this
  exception statement from your version.

  This program is distributed in the hope that it will be useful, but WITHOUT
  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
  for more details.

  You should have received a copy of the GNU Library General Public License
  along with this library; if not, write to the Free Software Foundation,
  Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.


  Changelog:
  2010-08-10   Simon Ameis <simon.ameis@web.de>
  * Import of the old version

  2010-12-25   Simon Ameis <simon.ameis@web.de>
  + TSQLite3Value
  + TSQLite3BLOB
}

unit sqlite3obj;

{$mode objfpc}{$H+}
{$DEFINE DBGPROPSTOR}
interface

uses
  Classes, SysUtils, sqlite3, strutils
  {$IFDEF USELCL}, Forms
  {$IFDEF DBGPROPSTOR}, LCLProc{$ENDIF DBGPROPSTOR}
  {$ENDIF USELCL};

type
  sqlite3_int64 = type sqlite3.sqlite3_int64;

  ESQLiteError = class(Exception);

  { ESQLiteCloseError }

  ESQLiteCloseError = class(ESQLiteError)
  private
    FHandle: psqlite3;
  public
    constructor Create(const msg: string; aHandle: psqlite3);
    property DatabaseHandle: psqlite3 read FHandle write FHandle;
  end;

  TSQLite3Database  = class;
  TSQLite3Statement = class;
  TSQLite3Value     = class;
  TSQLite3BLOB      = class;

  { TSQLite3Database }

  TSQLite3Database = class(TObject)
  private
    FHandle: psqlite3;
    FTransactionBeginStmt, FTransactionCommitStmt, FTransactionRollbackStmt: TSQLite3Statement;
  public
    constructor Create(const aDatabaseFile: string);
    destructor Destroy; override;
    // class function => independent of database connection/need only library
    class function IsThreadsafe: boolean;
    class function LibVersion: String;
    class function CheckTableName(const aName: String): Boolean;
    class function LibName: String;
    function LastError: string;
    procedure BeginTransaction;
    procedure CommitTransaction;
    procedure RollbackTranscation;
    function ExecuteSQL(const aSQL: String): Longint;
    function BLOBOpen(const zDB, zTable, zColumn: String; const iRow: sqlite3_int64; flags: longint): TSQLite3BLOB;
    property Handle: psqlite3 read FHandle;
  end;

  { TSQLite3Statement }

  TSQLite3Statement = class(TObject)
  private
    FHandle: psqlite3_stmt;
    FDatabase: TSQLite3Database;
    FValues: array of TSQLite3Value;
    function GetValue(aColumn: longint): TSQLite3Value;
    procedure ClearValues;
  public
    constructor Create(aDatabase: TSQLite3Database);
    destructor Destroy; override;
    function Prepare(const aSQL: string): boolean;
    function PrepareFmt(const aSQL: String; const Args: array of const): boolean;
    procedure Finalize;
    function Step: Longint;
    function Reset: boolean;
    function ClearBindings: boolean;
    function BindBLOB(Index: longint; const Buffer; count: longint): boolean;
    function BindString(Index: longint; const aStr: string): boolean;
    function BindInt(Index: longint; aValue: longint): boolean;
    function BindInt64(Index: longint; const aValue: int64): boolean;
    function BindDouble(Index: longint; const aValue: double): boolean;
    function BindZeroBLOB(Index: longint; aLength: longint): boolean;
    function ColumnCount: longint;
    function ColumnString(aCol: longint): string;
    function ColumnInt(aCol: longint): longint;
    function ColumnInt64(aCol: longint): int64;
    function ColumnDouble(aCol: longint): double;
    function ColumnIsNULL(aCol: longint): Boolean;
    property Handle: psqlite3_stmt read FHandle;
    property Database: TSQLite3Database read FDatabase;
    property Values[aColumn: longint]: TSQLite3Value read GetValue;
  end;

  { TSQLite3Value }

  TSQLite3Value = class(TObject)
  private
    FHandle: psqlite3_value;
    function GetIsNULL: Boolean;
  protected
    function GetBLOB: Pointer;
    function GetNumericType: longint;
    function GetType: longint;
    function GetWideString: WideString;
    function GetWideStringBE: WideString;
    function GetWideStringLE: WideString;
    function GetDouble: Double;
    function GetBytes: longint;
    function GetBytes16: longint;
    function GetInt64: sqlite3_int64;
    function GetInt: longint;
    function GetString: String;
  public
    constructor Create(aValueHandle: psqlite3_value);
    property AsBLOB: Pointer read GetBlob;
    property AsDouble: Double read GetDouble;
    property Bytes: longint read GetBytes;
    property Bytes16: longint read GetBytes16;
    property AsInt64: sqlite3_int64 read GetInt64;
    property AsInt: longint read GetInt;
    property AsString: String read GetString;
    property AsText: String read GetString;
    property AsWideString: WideString read GetWideString;
    property AsWideStringLittleEndian: WideString read GetWideStringLE;
    property AsWideStringBigEndian: WideString read GetWideStringBE;
    property ValueType: longint read GetType;
    property NumericType: longint read GetNumericType;
    property IsNULL: Boolean read GetIsNULL;
    property Handle: psqlite3_value read FHandle;
  end;

  { TSQLite3BLOB }

  TSQLite3BLOB = class(TObject)
  private
    FHandle: psqlite3_blob;
  public
    constructor Create(aDatabase: TSQLite3Database; const zDB, zTable, zColumn: String; const iRow: sqlite3_int64; flags: longint);
    constructor Create(aDatabase: psqlite3; const zDB, zTable, zColumn: String; const iRow: sqlite3_int64; flags: longint);
    constructor Create(aBLOBHandle: psqlite3_blob);
    destructor Destroy; override;
    function Write(const Buffer; count, iOffset: longint): longint;
    function Read(out Buffer; count, iOffset: longint): longint;
    function Bytes: longint;
    property Handle: psqlite3_blob read FHandle;
  end;

  {$IFDEF USELCL}
  { TSQLite3PropertyStorage }

  TSQLite3PropertyStorage = class(TFormPropertyStorage)
  private
    FDatabase: TSQLite3Database;
    FReadStmt, FWriteStmt: TSQLite3Statement;
    FTable: string;
    FDBOCount: integer;
    procedure SetDatabase(const AValue: TSQLite3Database);
    procedure SetTable(const AValue: string);
    procedure CreateTable;
  protected
    class function QuoteStr(const aStr: string): string;
    procedure AcquireStmtObj(aRead, aWrite: boolean);
    procedure FreeStmtObj(aRead, aWrite: boolean);
    procedure DoEraseSections(const ARootSection: string); override;
    function DoReadString(const Section, Ident, DefaultValue: string): string;
      override;
    procedure DoWriteString(const Section, Ident, Value: string); override;
  public
    {$IFDEF DBGPROPSTOR}
    procedure Restore; override;
    procedure Save; override;
    {$ENDIF DBGPROPSTOR}
    procedure StorageNeeded(ReadOnly: boolean); override;
    procedure FreeStorage; override;
    property Table: string read FTable write SetTable;
    property Database: TSQLite3Database read FDatabase write SetDatabase;
  end;
  {$ENDIF USELCL}

implementation

{ TSQLite3BLOB }

constructor TSQLite3BLOB.Create(aDatabase: TSQLite3Database; const zDB, zTable,
  zColumn: String; const iRow: sqlite3_int64; flags: longint);
begin
  Create(aDatabase.Handle, zDB, zTable, zColumn, iRow, flags);
end;

constructor TSQLite3BLOB.Create(aDatabase: psqlite3; const zDB, zTable,
  zColumn: String; const iRow: sqlite3_int64; flags: longint);
begin
  if sqlite3_blob_open(aDatabase, PChar(zDB), PChar(zTable), PChar(zColumn), iRow, flags, @FHandle) <> SQLITE_OK then
    raise ESQLiteError.CreateFmt('Couldnot open BLOB handle: %s', [sqlite3_errmsg(aDatabase)]);
end;

constructor TSQLite3BLOB.Create(aBLOBHandle: psqlite3_blob);
begin
  FHandle := aBLOBHandle;
end;

destructor TSQLite3BLOB.Destroy;
begin
  sqlite3_blob_close(FHandle);
  inherited Destroy;
end;

function TSQLite3BLOB.Write(const Buffer; count, iOffset: longint): longint;
begin
  Result := sqlite3_blob_write(FHandle, @Buffer, count, iOffset);
end;

function TSQLite3BLOB.Read(out Buffer; count, iOffset: longint): longint;
begin
  Result :=  sqlite3_blob_read(FHandle, @Buffer, count, iOffset);
end;

function TSQLite3BLOB.Bytes: longint;
begin
  Result := sqlite3_blob_bytes(FHandle);
end;

{ TSQLite3Value }

function TSQLite3Value.GetWideString: WideString;
begin
  Result := sqlite3_value_text16(FHandle);
end;

function TSQLite3Value.GetNumericType: longint;
begin
  Result := sqlite3_value_numeric_type(FHandle);
end;

function TSQLite3Value.GetIsNULL: Boolean;
begin
  Result := ValueType = SQLITE_NULL;
end;

function TSQLite3Value.GetBLOB: Pointer;
begin
  Result := sqlite3_value_blob(FHandle);
end;

function TSQLite3Value.GetType: longint;
begin
  Result := sqlite3_value_type(FHandle);
end;

function TSQLite3Value.GetWideStringBE: WideString;
begin
  Result := sqlite3_value_text16be(FHandle);
end;

function TSQLite3Value.GetWideStringLE: WideString;
begin
  Result := sqlite3_value_text16le(FHandle);
end;

function TSQLite3Value.GetDouble: Double;
begin
  Result := sqlite3_value_double(FHandle);
end;

function TSQLite3Value.GetBytes: longint;
begin
  Result := sqlite3_value_bytes(FHandle);
end;

function TSQLite3Value.GetBytes16: longint;
begin
  Result := sqlite3_value_bytes16(FHandle);
end;

function TSQLite3Value.GetInt64: sqlite3_int64;
begin
  Result := sqlite3_value_int64(FHandle);
end;

function TSQLite3Value.GetInt: longint;
begin
  Result := sqlite3_value_int(FHandle);
end;

function TSQLite3Value.GetString: String;
begin
  Result := sqlite3_value_text(FHandle);
end;

constructor TSQLite3Value.Create(aValueHandle: psqlite3_value);
begin
  FHandle := aValueHandle;
end;

{ TSQLite3Value }

{ TSQLite3Database }

constructor TSQLite3Database.Create(const aDatabaseFile: string);
begin
  if sqlite3_open(PChar(aDatabaseFile), @FHandle) <> SQLITE_OK then
  begin
    if FHandle = nil then
      OutOfMemoryError
    else
      raise ESQLiteError.Create(sqlite3_errmsg(FHandle));
  end;
end;

class function TSQLite3Database.IsThreadsafe: boolean;
begin
  Result := longbool(sqlite3_threadsafe());
end;

class function TSQLite3Database.LibVersion: String;
begin
  Result := sqlite3_libversion();
end;

class function TSQLite3Database.CheckTableName(const aName: String): Boolean;
begin
  // table name may not ...
  // be empty
  // start with 'sqlite_'
  // contain quote signs (")
  Result := (aName <> '') and (LeftStr(aName, 7) <> 'sqlite_') and (Pos(aName, '"') < 1);
end;

class function TSQLite3Database.LibName: String;
begin
  Result := sqlite3lib;
end;

function TSQLite3Database.LastError: string;
begin
  Result := sqlite3_errmsg(FHandle);
end;

procedure TSQLite3Database.BeginTransaction;
begin
  if FTransactionBeginStmt = nil then
  begin
    FTransactionBeginStmt := TSQLite3Statement.Create(Self);
    FTransactionBeginStmt.Prepare('BEGIN');
  end;
  FTransactionBeginStmt.Step;
  FTransactionBeginStmt.Reset;
end;

procedure TSQLite3Database.CommitTransaction;
begin
  if FTransactionCommitStmt = nil then
  begin
    FTransactionCommitStmt := TSQLite3Statement.Create(Self);
    FTransactionCommitStmt.Prepare('COMMIT');
  end;
  FTransactionCommitStmt.Step;
  FTransactionCommitStmt.Reset;
end;

procedure TSQLite3Database.RollbackTranscation;
begin
  if FTransactionRollbackStmt = nil then
  begin
    FTransactionRollbackStmt := TSQLite3Statement.Create(Self);
    FTransactionRollbackStmt.Prepare('ROLLBACK');
  end;
  FTransactionRollbackStmt.Step;
  FTransactionRollbackStmt.Reset;
end;

function TSQLite3Database.ExecuteSQL(const aSQL: String): Longint;
var
  stmt: TSQLite3Statement;
begin
  stmt := TSQLite3Statement.Create(Self);
  stmt.Prepare(aSQL);
  Result := Longint(stmt.Step);
  stmt.Free;
end;

function TSQLite3Database.BLOBOpen(const zDB, zTable, zColumn: String;
  const iRow: sqlite3_int64; flags: longint): TSQLite3BLOB;
begin
  Result := TSQLite3BLOB.Create(Self, zDB, zTable, zColumn, iRow, flags);
end;

destructor TSQLite3Database.Destroy;
begin
  // free transaction objects
  FreeAndNil(FTransactionBeginStmt);
  FreeAndNil(FTransactionCommitStmt);
  FreeAndNil(FTransactionRollbackStmt);

  // checking for nil is not required
  if sqlite3_close(FHandle) <> SQLITE_OK then
    raise ESQLiteCloseError.Create(
      'Database is busy. Please close database handle manually.', FHandle);
  inherited Destroy;
end;

{ ESQLiteCloseError }

constructor ESQLiteCloseError.Create(const msg: string; aHandle: psqlite3);
begin
  inherited Create(msg);
  FHandle := aHandle;
end;

{ TSQLite3Statement }

function TSQLite3Statement.GetValue(aColumn: longint): TSQLite3Value;
begin
  if FValues[aColumn] = nil then
    FValues[aColumn] := TSQLite3Value.Create(sqlite3_column_value(FHandle, aColumn));
  Result := FValues[aColumn];
end;

procedure TSQLite3Statement.ClearValues;
var
  i: Longint;
begin
  for i := Low(FValues) to High(FValues) do
    FreeAndNil(FValues[i]);
  // SetLength() fills the array with nil, but this is not documented
  if Length(FValues) <> ColumnCount then
    SetLength(FValues, ColumnCount);
end;

constructor TSQLite3Statement.Create(aDatabase: TSQLite3Database);
begin
  FDatabase := aDatabase;
end;

destructor TSQLite3Statement.Destroy;
begin
  if FHandle <> nil then
    Finalize;
  inherited Destroy;
end;

function TSQLite3Statement.Prepare(const aSQL: string): boolean;
begin
  Result := sqlite3_prepare_v2(FDatabase.Handle, PChar(aSQL), Length(
    aSQL) + 1, @FHandle, nil) = SQLITE_OK;
end;

function TSQLite3Statement.PrepareFmt(const aSQL: String;
  const Args: array of const): Boolean;
begin
  Result := Prepare(Format(aSQL, Args));
end;

procedure TSQLite3Statement.Finalize;
begin
  sqlite3_finalize(FHandle);
  FHandle := nil;
end;

function TSQLite3Statement.Step: Longint;
begin
  Result := sqlite3_step(FHandle);
  ClearValues;
end;

function TSQLite3Statement.Reset: boolean;
begin
  Result := sqlite3_reset(FHandle) = SQLITE_OK;
end;

function TSQLite3Statement.ClearBindings: boolean;
begin
  Result := sqlite3_clear_bindings(FHandle) = SQLITE_OK;
end;

function TSQLite3Statement.BindBLOB(Index: longint; const Buffer; count: longint
  ): boolean;
begin
  Result := sqlite3_bind_blob(FHandle, Index, @Buffer, count, SQLITE_TRANSIENT) = SQLITE_OK;
end;

function TSQLite3Statement.BindString(Index: longint; const aStr: string): boolean;
begin
  Result := sqlite3_bind_text(FHandle, Index, PChar(aStr), Length(aStr) + 1,
    SQLITE_TRANSIENT) = SQLITE_OK;
end;

function TSQLite3Statement.BindInt(Index: longint; aValue: longint): boolean;
begin
  Result := sqlite3_bind_int(FHandle, Index, aValue) = SQLITE_OK;
end;

function TSQLite3Statement.BindInt64(Index: longint; const aValue: int64): boolean;
begin
  Result := sqlite3_bind_int64(FHandle, Index, aValue) = SQLITE_OK;
end;

function TSQLite3Statement.BindDouble(Index: longint; const aValue: double): boolean;
begin
  Result := sqlite3_bind_double(FHandle, Index, aValue) = SQLITE_OK;
end;

function TSQLite3Statement.BindZeroBLOB(Index: longint; aLength: longint
  ): boolean;
begin
  Result := sqlite3_bind_zeroblob(FHandle, Index, aLength) = SQLITE_OK;
end;

function TSQLite3Statement.ColumnCount: longint;
begin
  Result := sqlite3_column_count(FHandle);
end;

function TSQLite3Statement.ColumnString(aCol: longint): string;
begin
  Result := sqlite3_column_text(FHandle, aCol);
end;

function TSQLite3Statement.ColumnInt(aCol: longint): longint;
begin
  Result := sqlite3_column_int(FHandle, aCol);
end;

function TSQLite3Statement.ColumnInt64(aCol: longint): int64;
begin
  Result := sqlite3_column_int64(FHandle, aCol);
end;

function TSQLite3Statement.ColumnDouble(aCol: longint): double;
begin
  Result := sqlite3_column_double(FHandle, aCol);
end;

function TSQLite3Statement.ColumnIsNULL(aCol: longint): Boolean;
begin
  Result := Values[aCol].IsNULL;
end;

{$IFDEF USELCL}
{ TSQLite3PropertyStorage }

procedure TSQLite3PropertyStorage.SetDatabase(const AValue: TSQLite3Database);
begin
  if FDatabase = AValue then
    exit;
  if FDBOCount > 0 then
    raise EInvalidOperation.Create(
      'Can''t change database while accessing the old one.');
  FDatabase := AValue;
  CreateTable;
end;

procedure TSQLite3PropertyStorage.SetTable(const AValue: string);
begin
  if FTable = AValue then
    exit;
  if FDBOCount > 0 then
    raise EInvalidOperation.Create('Can''t change table name during database access.');
  // a empty table name is allowd => diables database access
  if (AValue <> '') and not FDatabase.CheckTableName(AValue) then
    raise ESQLiteError.Create('"' + AValue + '" is an invalid table name.');
  FTable := AValue;
  CreateTable;
end;

procedure TSQLite3PropertyStorage.CreateTable;
var
  stmt: TSQLite3Statement;
begin
  // create table if needed
  if (FDatabase <> nil) and FDatabase.CheckTableName(FTable) then
    try
      stmt := TSQLite3Statement.Create(FDatabase);
      stmt.PrepareFmt(
        'CREATE TABLE IF NOT EXISTS "%s" ( '
       +'"section" TEXT, '
       +'"ident" TEXT, '
       +'"value" TEXT, '
       +'UNIQUE ("section","ident") ON CONFLICT REPLACE '
       +');',
        [FTable]
        );
      if stmt.Step <> SQLITE_OK then
        raise ESQLiteError.CreateFmt('Could not create table "%s": %s',
          [FTable, FDatabase.LastError]);
    finally
      stmt.Free;
    end;
end;

class function TSQLite3PropertyStorage.QuoteStr(const aStr: string): string;
begin
  Result := StringReplace(aStr, '''', '''''', [rfReplaceAll]);
end;

procedure TSQLite3PropertyStorage.AcquireStmtObj(aRead, aWrite: boolean);
begin
  if (FDatabase = nil) or not FDatabase.CheckTableName(FTable) then
    exit;
  if aRead and (FReadStmt = nil) then
  begin
    FReadStmt := TSQLite3Statement.Create(FDatabase);
    FReadStmt.PrepareFmt('SELECT * FROM "%s" WHERE "section"=?1 AND "ident"=?2;', [FTable]);
  end;
  if aWrite and (FWriteStmt = nil) then
  begin
    FWriteStmt := TSQLite3Statement.Create(FDatabase);
    FWriteStmt.PrepareFmt('INSERT OR REPLACE INTO "%s" VALUES (?1,?2,?3);', [FTable]);
  end;
end;

procedure TSQLite3PropertyStorage.FreeStmtObj(aRead, aWrite: boolean);
begin
  // statements are automatically finalized within the destructor
  if aRead then
  begin
    FReadStmt.Free;
    FReadStmt := nil;
  end;
  if aWrite then
  begin
    FWriteStmt.Free;
    FWriteStmt := nil;
  end;
end;

procedure TSQLite3PropertyStorage.StorageNeeded(ReadOnly: boolean);
begin
  AcquireStmtObj(True, not ReadOnly);
  Inc(FDBOCount);
  if FDBOCount = 1 then
    FDatabase.BeginTransaction;
  {$IFDEF DBGPROPSTOR}
  DebugLn('Increased storage count: ',DbgS(FDBOCount),' Readonly: ',DbgS(ReadOnly));
  {$ENDIF DBGPROPSTOR}
end;

procedure TSQLite3PropertyStorage.FreeStorage;
begin
  Dec(FDBOCount);
  {$IFDEF DBGPROPSTOR}
  DebugLn('Decreased storage count: ',DbgS(FDBOCount));
  {$ENDIF DBGPROPSTOR}
  if FDBOCount = 0 then
  begin
    FDatabase.CommitTransaction;
    FreeStmtObj(True, True);
  end;
end;

procedure TSQLite3PropertyStorage.DoEraseSections(const ARootSection: string);
var
  stmt: TSQLite3Statement;
begin
  {$IFDEF DBGPROPSTOR}
  DebugLn('Erase config section ...');
  DebugLn('  Section: ', ARootSection);
  {$ENDIF DBGPROPSTOR}
  if (FDatabase = nil) or not FDatabase.CheckTableName(FTable) then
    exit;
  stmt := TSQLite3Statement.Create(FDatabase);
  try
    stmt.Prepare('DELETE FROM "' + FTable + '" WHERE "section"=''' +
      QuoteStr(ARootSection) + ''';');
    stmt.Step;
  finally
    stmt.Free;
  end;
end;

function TSQLite3PropertyStorage.DoReadString(
  const Section, Ident, DefaultValue: string): string;
begin
  FReadStmt.BindString(1, Section);
  FReadStmt.BindString(2, Ident);
  if FReadStmt.Step = SQLITE_ROW then
    Result := FReadStmt.ColumnString(2)
  else
    Result := DefaultValue;
  FReadStmt.Reset;
  {$IFDEF DBGPROPSTOR}
  DebugLn('Read config value ...');
  DebugLn('  Section: ', Section);
  Debugln('  Ident  : ', Ident);
  Debugln('  Default: ', DefaultValue);
  Debugln('  Result : ', Result);
  {$ENDIF DBGPROPSTOR}
end;

procedure TSQLite3PropertyStorage.DoWriteString(const Section, Ident, Value: string);
var
  qr: TSimpleStepResult;
begin
  {$IFDEF DBGPROPSTOR}
  DebugLn('Write config value ...');
  DebugLn('  Section: ', Section);
  Debugln('  Ident  : ', Ident);
  Debugln('  Value  : ', Value);
  {$ENDIF DBGPROPSTOR}
  FWriteStmt.BindString(1, Section);
  FWriteStmt.BindString(2, Ident);
  FWriteStmt.BindString(3, Value);
  qr := FWriteStmt.Step;
  FWriteStmt.Reset;
  if qr <> SQLITE_DONE then
    raise ESQLiteError.Create('Could not save property: ' + FDatabase.LastError);
end;

{$IFDEF DBGPROPSTOR}
procedure TSQLite3PropertyStorage.Restore;
var
  t: TTime;
begin
  t := Now;
  inherited Restore;
  DebugLn('Restoring time: ',Dbgs((Now-t)*24*60*60*1000),'ms');
end;

procedure TSQLite3PropertyStorage.Save;
var
  t: TTime;
begin
  t := Now;
  inherited Save;
  DebugLn('Saving time: ',Dbgs((Now-t)*24*60*60*1000),'ms');
end;

{$ENDIF DBGPROPSTOR}
{$ENDIF USELCL}
end.

