unit SQLite3Classes;
{ *******************************************************

Original Code By:

  SQLite 3 Dynamic Library wrapper for Delphi

  2012 Ángel Fernández Pineda. Madrid. Spain.

  This work is licensed under the Creative Commons
  Attribution-ShareAlike 3.0 Unported License. To
  view a copy of this license,
  visit http://creativecommons.org/licenses/by-sa/3.0/
  or send a letter to Creative Commons,
  444 Castro Street, Suite 900,
  Mountain View, California, 94041, USA.

  *******************************************************

Updated by JoeJoeSoft:

    Fixed an issue setting a param[] using a Cardinal
    Added error detection for linking to DLL functions
    Changed the logic for loading the SQLite3.dll
    - look for "sqlite3.dll" in the current folder
    - try to extract in the current folder
    - use a temporary file in the Windows' temp folder as a fallback
    Added an error code to the ESQLiteError exception
    Replaced range check exceptions with ESQLiteError for better debugging
    Added transaction functions to TSQLiteDatabase
    Added workaround for GetAsText to return an empty string instead of erroring out
  ******************************************************* }

interface

uses
   System.SysUtils, SQLite3Lib, System.Classes, System.Variants;

type
 { ESQLiteError

    PURPOUSE: SQLite related exceptions
 }
  ESQLiteError = class(Exception)
  private
	code : integer;
  public
    constructor Create(s : string; ErrCode: integer);
    class function SQLiteErrMsg(ErrCode: integer): string;
    class procedure Test(ErrCode: integer);
	function getCode : integer;
  end;

 { ESQLiteSytanxError

    PURPOUSE: SQL syntax related exceptions
 }
  ESQLiteSyntaxError = class(ESQLiteError);

{$SCOPEDENUMS ON}

type
  // types required by TSQLiteDatabase.Create
  TSQLiteThreadMode = (singleThread, multiThread, serialized);
  TSQLiteOpenMode = (createAsNeeded, readWrite, readOnly);
  TSQLiteCacheMode = (sharedCache, privateCache);

type

  TSQLiteCursor = class
  protected
    FHandle: Psqlite3_stmt;
    FHasData: boolean;
    FRowCount: int64;
    procedure TestColumnError(index: integer);
    function GetColumnName(index: integer): string;
    function GetColumnType(index: integer): TVarType;
    function GetIsNull(index: integer): boolean;
    function GetisBlob(index: integer): boolean;
    function GetAsInteger(index: integer): integer;
    function GetAsInt64(index: integer): int64;
    function GetAsString(index: integer): string;
    function GetAsDouble(index: integer): double;
    function GetAsBlob(index: integer): Pointer;
    function GetColumnSize(index: integer): integer;
    function GetColumnCount: integer;
    function GetSourceSchema(index: integer): string;
    function GetSourceTable(index: integer): string;
    function GetSourceColumn(index: integer): string;
  public
    constructor Create(Handle: Psqlite3_stmt);
    procedure Reset;
    procedure First;
    function Next: boolean;
    procedure ColumnToStream(index: integer; Stream: TStream);

    property CurrentRowCount: int64 read FRowCount;
    property ColumnCount: integer read GetColumnCount;
    property HasData: boolean read FHasData;
    property ColumnName[index: integer]: string read GetColumnName;
    property ColumnType[index: integer]: TVarType read GetColumnType;
    property ColumnIsNull[Index: integer]: boolean read GetIsNull;
    property ColumnIsBlob[Index: integer]: boolean read GetisBlob;
    property ColumnAsInteger[Index: integer]: integer read GetAsInteger;
    property ColumnAsInt64[Index: integer]: int64 read GetAsInt64;
    property ColumnAsDouble[Index: integer]: double read GetAsDouble;
    property ColumnAsString[Index: integer]: string read GetAsString;
    property ColumnAsBlob[Index: integer]: Pointer read GetAsBlob;
    property ColumnSize[Index: integer]: integer read GetColumnSize;
    property ColumnSchema[Index: integer]: string read GetSourceSchema;
    property ColumnTable[Index: integer]: string read GetSourceTable;
    property ColumnSource[Index: integer]: string read GetSourceColumn;
  end;

 { TSQLiteStatement

   REMARKS:

   - TSQLiteStatement instances are not destroyed by TSQLiteDatabase.
     You must call "myStatement.Free". However,
     you must not call "myStatement.Cursor.Free".

   - Do not share TSQLiteStatement instances between threads, even
     if multi-threading is enabled at SQLite. Otherwise,
     you need to synchronize access to instances.
     See SQLite's documentation on multi-threading.

   - Note that parameter names will start with ":" or "@" and are
     case-sensitive.

   - Do not rely on ParamCount property if "?NNN" parameters are used.

   - "Execute" will return zero if the query does not contain a result set.
     In other case, it will return the number of rows in the result set.
     Check isReadOnly property to know the case.

   - There are three versions of SetParamBlob:
     a) The first uses data from a Buffer. That buffer should not be
     disposed until TSQLiteStatement instance is destroyed,
     unless "mayChange" is true. In that case, buffer contents are first
     copied to a private buffer. Use wisely.

     b) The second one copies an entire stream content into a private buffer,
     so it is safe to dispose that stream. However, it may consume a lot
     of memory. Use wisely.

     c) The third one creates a blob filled with zeroes of the given size.
     Actual blob contents should be writen later. TSQLiteBlobStream may help.
 }
  TSQLiteStatement = class
  protected
    FCursor: TSQLiteCursor;
    constructor Create(db: PSQLite3; const SQL: string);
    function GetParamCount: integer;
    function GetIsReadOnly: boolean;
    procedure SetParamByIndex(index: integer; const Value: variant);
    procedure SetParamByName(Name: string; const Value: variant);
  public
    destructor Destroy; override;
    function IndexOf(const ParamName: string): integer;
    function NameOf(index: integer): string;
    procedure SetParamBlob(index: integer; const Buffer: Pointer; size: integer;
      mayChange: boolean = false); overload;
    procedure SetParamBlob(index: integer; Stream: TStream); overload;
    procedure SetParamBlob(index: integer; zerosCount: integer); overload;
    function Execute: int64;
    property isReadOnly: boolean read GetIsReadOnly;

    property ParamCount: integer read GetParamCount;
    property Param[index: integer]: variant write SetParamByIndex;
    property ParamByName[Name: string]: variant write SetParamByName;
    property Cursor: TSQLiteCursor read FCursor;


  end;

 { TSQLiteDatabase
   REMARKS:
   - Several database files may be opened simultaneously by the
     means of an "ATTACH" query. See SQLite documentation.
 }
  TSQLiteDatabase = class
  protected
    FHandle: PSQLite3;
    fURI : string;
  public
    constructor Create(URI: string = ':memory:';
      OpenMode: TSQLiteOpenMode = TSQLiteOpenMode.createAsNeeded;
      ThreadMode: TSQLiteThreadMode = TSQLiteThreadMode.multiThread;
      CacheMode: TSQLiteCacheMode = TSQLiteCacheMode.privateCache);

    function ReadOnly : boolean;

    function GetLastInsertRowID: int64;
    function GetLastErrorMessage: string;
    function Query(const SQL: string): TSQLiteStatement;
    function Execute(const SQL: string): int64;
	
	procedure StartTransaction;
	procedure CancelTransaction;
	procedure EndTransaction;
    destructor Destroy; override;
  end;

 { TSQLiteBlobStream

   REMARKS:

   - "Schema" parameter to Create is the internal alias given to a database
     file in an "ATTACH" query. Default schema is "main". The schema for
     temporary tables is "temp".

   - TSQLiteBlobStream is not strictly required, since there are other methods
     in TSQLiteCursor which do the job. However, they are not practical when
     dealing with large amounts of data. On the counterpart, it is really
     annoying to obtain a rowid. Ask SQLite's designers.

   - Trying to change stream size will raise an exception. This is a
     SQLite restriction.
 }
  TSQLiteBlobStream = class(TStream)
  protected
    FHandle: PSQLite3_blob;
    FCurrentOffset: Longint;
    FSize: Longint;
    procedure SetSize(NewSize: Longint); override;
  public
    constructor Create(db: TSQLiteDatabase; const Schema, Table, Column: string;
      RowID: int64; readOnly: boolean = true); overload;
    constructor Create(db: TSQLiteDatabase; const Table, Column: string;
      RowID: int64; readOnly: boolean = true); overload;
    destructor Destroy; override;
    function Seek(Offset: Longint; Origin: Word): Longint; override;
    function Read(var Buffer; Count: Longint): Longint; override;
    function Write(const Buffer; Count: Longint): Longint; override;
  end;

implementation

uses SQLite3Constants;

const
  WRONG_INDEX_MSG = 'Wrong parameter index to SQL statement';
  EMPTY_CURSOR_MSG = 'Cursor is empty';
  OPEN_ERROR_MSG = 'Unable to open database';
  TYPE_CONVERSION_ERROR = 'Unable to perform data type conversion';
  INVALID_OFFSET_MSG = 'Trying to read/write outside blob bounds';
  NO_SIZE_CHANGE_MSG = 'Size of Blob stream can not be changed';

// -------------------------------------------------------------------------
// ESQLiteError
// -------------------------------------------------------------------------

class function ESQLiteError.SQLiteErrMsg(ErrCode: integer): String;
begin
  case ErrCode of
    SQLITE_OK:
      Result := 'No error';
    SQLITE_ERROR:
      Result := 'SQL error or missing database';
    SQLITE_INTERNAL:
      Result := 'Internal SQLite error';
    SQLITE_PERM:
      Result := 'Permission denied';
    SQLITE_ABORT:
      Result := 'Aborted by Callback routine';
    SQLITE_BUSY:
      Result := 'Database is locked or there are pending operations';
    SQLITE_LOCKED:
      Result := 'Table is locked';
    SQLITE_NOMEM:
      Result := 'Not enought memory';
    SQLITE_READONLY:
      Result := 'Attempt to write a readonly database';
    SQLITE_INTERRUPT:
      Result := 'Operation terminated by sqlite3_interrupt()';
    SQLITE_IOERR:
      Result := 'Failed I/O operation';
    SQLITE_CORRUPT:
      Result := 'The database disk image is malformed';
    SQLITE_NOTFOUND:
      Result := 'Table or record not found';
    SQLITE_FULL:
      Result := 'Database is full';
    SQLITE_CANTOPEN:
      Result := 'Unable to open database file';
    SQLITE_PROTOCOL:
      Result := 'Database lock protocol error';
    SQLITE_EMPTY:
      Result := 'Database is empty';
    SQLITE_SCHEMA:
      Result := 'Database schema changed';
    SQLITE_TOOBIG:
      Result := 'Too much data for one row of a table';
    SQLITE_CONSTRAINT:
      Result := 'Constraint violation';
    SQLITE_MISMATCH:
      Result := 'Data type mismatch';
    SQLITE_MISUSE:
      Result := 'Library misused';
    SQLITE_NOLFS:
      Result := 'Use of OS features not supported on host';
    SQLITE_AUTH:
      Result := 'Authorization denied';
    SQLITE_FORMAT:
      Result := 'Format error';
    SQLITE_RANGE:
      Result := 'Parameter out of range';
    SQLITE_NOTADB:
      Result := 'Not a database file';
    SQLITE_ROW:
      Result := 'Row ready';
    SQLITE_DONE:
      Result := 'No more rows';
  else
    Result := 'Unexpected Error Code "' + IntToStr(ErrCode) + '"';
  end;
end;

class procedure ESQLiteError.Test(ErrCode: integer);
begin
  if (ErrCode = SQLITE_RANGE) then begin
    raise ESQLiteError.Create(SQLiteErrMsg(ErrCode), ErrCode);
  end;
  if (ErrCode <> SQLITE_OK) and (ErrCode < SQLITE_ROW) then begin
    raise ESQLiteError.Create(SQLiteErrMsg(ErrCode), ErrCode);
  end;
end;
function ESQLiteError.getCode : integer;
begin
	result := code;
end;
constructor ESQLiteError.Create(s : string; ErrCode : integer);
begin
    inherited Create(s);
    code := ErrCode;
end;

// -------------------------------------------------------------------------
// TSQLite3Database
// -------------------------------------------------------------------------


function TSQLiteDatabase.ReadOnly : boolean;
begin
    result := sqlite3_db_readonly(FHandle, UTF8String(fURI)) <> 0;
end;

constructor TSQLiteDatabase.Create(URI: string = ':memory:';
  OpenMode: TSQLiteOpenMode = TSQLiteOpenMode.createAsNeeded;
  ThreadMode: TSQLiteThreadMode = TSQLiteThreadMode.multiThread;
  CacheMode: TSQLiteCacheMode = TSQLiteCacheMode.privateCache);
var
  ErrCode: integer;
  flags: integer;
begin
  case OpenMode of
    TSQLiteOpenMode.createAsNeeded:
      flags := SQLITE_OPEN_READWRITE or SQLITE_OPEN_CREATE;
    TSQLiteOpenMode.readWrite:
      flags := SQLITE_OPEN_READWRITE;
    TSQLiteOpenMode.readOnly:
      flags := SQLITE_OPEN_READONLY;
  else
    flags := 0;
  end;

  case ThreadMode of
    TSQLiteThreadMode.multiThread:
      flags := flags or SQLITE_OPEN_NOMUTEX;
    TSQLiteThreadMode.serialized:
      flags := flags or SQLITE_OPEN_FULLMUTEX;
  end;

  case CacheMode of
    TSQLiteCacheMode.sharedCache:
      flags := flags or SQLITE_OPEN_SHAREDCACHE;
    TSQLiteCacheMode.privateCache:
      flags := flags or SQLITE_OPEN_PRIVATECACHE;
  end;

  fURI := URI;
  ErrCode := sqlite3_open_v2(UTF8String(fURI), FHandle, flags, nil);
  ESQLiteError.Test(ErrCode);
  if (FHandle = nil) then
    raise ESQLiteError.Create(OPEN_ERROR_MSG, ErrCode);
end;

destructor TSQLiteDatabase.Destroy;
begin
  if (FHandle <> nil) then
    ESQLiteError.Test(sqlite3_close(FHandle));
end;

function TSQLiteDatabase.Query(const SQL: string): TSQLiteStatement;
begin
  Result := TSQLiteStatement.Create(FHandle, SQL);
end;

function rowCountCallback(UserData: Pointer; NumCols: integer;
  ColValues: PUTF8CharArray; ColNames: PUTF8CharArray): integer; cdecl;
begin
  inc(PInt64(UserData)^);
  Result := SQLITE_OK;
end;

function TSQLiteDatabase.Execute(const SQL: string): int64;
var
  ErrCode: integer;
  perrmsg: PUTF8Char;
  errmsg: string;
begin
  Result := 0;
  perrmsg := nil;
  ErrCode := sqlite3_exec(FHandle, UTF8String(SQL), rowCountCallback,
    @Result, perrmsg);
  if (perrmsg <> nil) then
    errmsg := string(perrmsg)
  else
    errmsg := '';
  sqlite3_free(perrmsg);
  if (ErrCode = SQLITE_ERROR) then
    raise ESQLiteSyntaxError.Create(errmsg, ErrCode);
  ESQLiteError.Test(ErrCode);
end;

function TSQLiteDatabase.GetLastInsertRowID: int64;
begin
  Result := sqlite3_last_insert_rowid(FHandle);
end;

function TSQLiteDatabase.GetLastErrorMessage: string;
var
  perrmsg: PUTF8Char;
begin
  perrmsg := Sqlite3_errmsg(FHandle);
  if (perrmsg <> nil) then
    Result := string(perrmsg)
  else
    Result := '';
end;

procedure TSQLiteDatabase.StartTransaction;
begin
	execute('BEGIN TRANSACTION');
end;
procedure TSQLiteDatabase.CancelTransaction;
begin
	execute('ROLLBACK');
end;
procedure TSQLiteDatabase.EndTransaction;
begin
	execute('END TRANSACTION');
end;


// -------------------------------------------------------------------------
// TSQLite3ResultSet
// -------------------------------------------------------------------------

constructor TSQLiteStatement.Create(db: PSQLite3; const SQL: string);
var
  ErrCode: integer;
  dummy: PUnicodeString;
  Handle: Psqlite3_stmt;
begin
  dummy := nil;
  ErrCode := sqlite3_prepare16_v2(db, SQL, ByteLength(SQL), Handle, dummy);
  if (ErrCode = SQLITE_ERROR) then
    raise ESQLiteSyntaxError.Create(ESQLiteError.SQLiteErrMsg(ErrCode), ErrCode);
  ESQLiteError.Test(ErrCode);
  if (Handle = nil) then
    raise ESQLiteSyntaxError.Create('Invalid SQL query', ErrCode);
  FCursor := TSQLiteCursor.Create(Handle);
end;

destructor TSQLiteStatement.Destroy;
begin
  inherited Destroy;
  if (FCursor <> nil) then
    ESQLiteError.Test(sqlite3_finalize(FCursor.FHandle));
  FCursor.Free;
end;

function TSQLiteStatement.IndexOf(const ParamName: string): integer;
begin
  Result := sqlite3_bind_parameter_index(FCursor.FHandle,
    UTF8String(ParamName));
end;

function TSQLiteStatement.NameOf(index: integer): string;
var
  pname: PUTF8Char;
begin
  pname := sqlite3_bind_parameter_name(FCursor.FHandle, Index);
  if (pname <> nil) then
    Result := string(pname)
  else
    Result := '';
end;

function TSQLiteStatement.GetParamCount: integer;
begin
  Result := sqlite3_bind_parameter_count(FCursor.FHandle);
end;

function TSQLiteStatement.GetIsReadOnly: boolean;
begin
  Result := (sqlite3_stmt_readonly(FCursor.FHandle) > 0);
end;

function TSQLiteStatement.Execute: int64;
begin
  while FCursor.Next do;
  if (isReadOnly) then
    Result := 0
  else
    Result := FCursor.CurrentRowCount;
  FCursor.Reset;
end;

procedure TSQLiteStatement.SetParamByIndex(index: integer;
  const Value: variant);
var
  ErrCode: integer;
  auxstr: string;
begin
    FCursor.Reset;
// Cardinals throw a range check error when the high bit is set
{$IFOPT R+}
    {$DEFINE RANGEON}
    {$R-}
{$ELSE}
    {$UNDEF RANGEON}
{$ENDIF}
  case VarType(Value) of
    varNull:
      ErrCode := sqlite3_bind_null(FCursor.FHandle, Index);
    varSmallint, varInteger, varBoolean, varShortInt, varByte, varWord,
      varLongWord:
      ErrCode := sqlite3_bind_int(FCursor.FHandle, Index, Value);
    varInt64:
      ErrCode := sqlite3_bind_int64(FCursor.FHandle, Index, Value);
    varSingle, varDouble, varCurrency:
      ErrCode := sqlite3_bind_double(FCursor.FHandle, Index, Value);
  else
    auxstr := Value;
    ErrCode := sqlite3_bind_text16(FCursor.FHandle, Index, auxstr,
      ByteLength(auxstr), nil);
  end;
{$IFDEF RANGEON}
    {$R+}
    {$UNDEF RANGEON}
{$ENDIF}

  ESQLiteError.Test(ErrCode);
end;

procedure TSQLiteStatement.SetParamByName(Name: string; const Value: variant);
begin
  SetParamByIndex(IndexOf(Name), Value);
end;

procedure TSQLiteStatement.SetParamBlob(index: integer; const Buffer: Pointer;
  size: integer; mayChange: boolean = false);
var
  flag: Pointer;
begin
  FCursor.Reset;
  if mayChange then
    flag := SQLITE_TRANSIENT
  else
    flag := SQLITE_STATIC;
  ESQLiteError.Test(sqlite3_bind_blob(FCursor.FHandle, Index, Buffer,
    size, flag));
end;

procedure TSQLiteStatement.SetParamBlob(index: integer; zerosCount: integer);
begin
  ESQLiteError.Test(sqlite3_bind_zeroblob(FCursor.FHandle, index, zerosCount));
end;

procedure FreeMemoryDestructor(Buffer: Pointer); cdecl;
begin
 // Auxiliary memory destructor for sqlite3_bind_blob
  FreeMemory(Buffer);
end;

procedure TSQLiteStatement.SetParamBlob(index: integer; Stream: TStream);
var
  Buffer: Pointer;
  rc: int64;
begin
  if (Stream <> nil) then
  begin
    rc := Stream.size;
    Buffer := GetMemory(rc);
    try
      Stream.ReadBuffer(Pointer(Buffer)^, rc);
    except
      FreeMemory(Buffer);
      raise;
    end;
    ESQLiteError.Test(sqlite3_bind_blob(FCursor.FHandle, Index, Pointer(Buffer),
      rc, FreeMemoryDestructor));
  end
  else
    SetParamBlob(index, 0);
end;


// -------------------------------------------------------------------------
// TSQLiteCursor
// -------------------------------------------------------------------------

constructor TSQLiteCursor.Create(Handle: Psqlite3_stmt);
begin
  FHandle := Handle;
  FHasData := false;
  FRowCount := 0;
end;

procedure TSQLiteCursor.Reset;
begin
  ESQLiteError.Test(sqlite3_reset(FHandle));
  FHasData := false;
  FRowCount := 0;
end;

procedure TSQLiteCursor.First;
begin
  Reset;
  Next;
end;

function TSQLiteCursor.Next: boolean;
var
  code: integer;
begin
  code := sqlite3_step(FHandle);
  ESQLiteError.Test(code);
  inc(FRowCount);
  Result := (code = SQLITE_ROW);
  FHasData := Result;
end;

function TSQLiteCursor.GetColumnCount: integer;
begin
  Result := sqlite3_data_count(FHandle);
end;

procedure TSQLiteCursor.TestColumnError(index: integer);
begin
  if (not HasData) then
    raise ESQLiteError.Create(EMPTY_CURSOR_MSG, 0)
  else if (index < 0) or (index >= GetColumnCount) then
    raise ESQLiteError.Create(WRONG_INDEX_MSG, 0);
end;

function TSQLiteCursor.GetColumnName(index: integer): string;
var
  pname: PWideChar;
begin
  TestColumnError(index);
  pname := sqlite3_column_name16(FHandle, index);
  if (pname = nil) then
    Result := ''
  else
    Result := string(pname);
end;

function TSQLiteCursor.GetColumnType(index: integer): TVarType;
var
  sqlitetype: integer;
begin
  TestColumnError(index);
  sqlitetype := sqlite3_column_type(FHandle, index);
  case sqlitetype of
    SQLITE_INTEGER:
      Result := varInteger;
    SQLITE_FLOAT:
      Result := varDouble;
    SQLITE_TEXT:
      Result := varString;
    SQLITE_BLOB:
      Result := varArray;
    SQLITE_NULL:
      Result := Null;
  else
    Result := varUnknown;
  end;
end;

function TSQLiteCursor.GetIsNull(index: integer): boolean;
begin
  Result := (GetColumnType(Index) = Null);
end;

function TSQLiteCursor.GetisBlob(index: integer): boolean;
begin
  Result := (GetColumnType(Index) = varArray);
end;

function TSQLiteCursor.GetAsInteger(index: integer): integer;
begin
  TestColumnError(index);
  Result := sqlite3_column_int(FHandle, index);
end;

function TSQLiteCursor.GetAsInt64(index: integer): int64;
begin
  TestColumnError(index);
  Result := sqlite3_column_int64(FHandle, index);
end;

function TSQLiteCursor.GetAsDouble(index: integer): double;
begin
  TestColumnError(index);
  Result := sqlite3_column_double(FHandle, index);
end;

function TSQLiteCursor.GetAsString(index: integer): string;
var
  pvalue: PWideChar;
begin
  TestColumnError(index);

  pvalue := sqlite3_column_text16(FHandle, index);
  if (pvalue <> nil) then
    result := string(pvalue)
  else
    result := '';
//    raise EConvertError.Create(TYPE_CONVERSION_ERROR);
end;

function TSQLiteCursor.GetAsBlob(index: integer): Pointer;
begin
  TestColumnError(index);
  Result := sqlite3_column_blob(FHandle, index);
end;

function TSQLiteCursor.GetColumnSize(index: integer): integer;
begin
  TestColumnError(index);
  Result := sqlite3_column_bytes16(FHandle, index);
end;

procedure TSQLiteCursor.ColumnToStream(index: integer; Stream: TStream);
var
  Buffer: Pointer;
  size: integer;
begin
  if (Stream <> nil) then
  begin
    Buffer := GetAsBlob(index);
    size := GetColumnSize(index);
    Stream.Write(Buffer^, size);
  end;
end;

function TSQLiteCursor.GetSourceSchema(index: integer): string;
var
  pname: PWideChar;
begin
  TestColumnError(index);
  pname := sqlite3_column_database_name16(FHandle, index);
  if (pname <> nil) then
    Result := string(pname)
  else
    Result := '';
end;

function TSQLiteCursor.GetSourceTable(index: integer): string;
var
  pname: PWideChar;
begin
  TestColumnError(index);
  pname := sqlite3_column_table_name16(FHandle, index);
  if (pname <> nil) then
    Result := string(pname)
  else
    Result := '';
end;

function TSQLiteCursor.GetSourceColumn(index: integer): string;
var
  pname: PWideChar;
begin
  TestColumnError(index);
  pname := sqlite3_column_origin_name16(FHandle, index);
  if (pname <> nil) then
    Result := string(pname)
  else
    Result := '';
end;

// -------------------------------------------------------------------------
// TSQLiteBlobStream
// -------------------------------------------------------------------------

constructor TSQLiteBlobStream.Create(db: TSQLiteDatabase;
  const Schema, Table, Column: string; RowID: int64; readOnly: boolean = true);
var
  flags: integer;
begin
  if readOnly then
    flags := 1
  else
    flags := 0;

  ESQLiteError.Test(sqlite3_blob_open(db.FHandle, UTF8String(Schema),
    UTF8String(Table), UTF8String(Column), RowID, flags, FHandle));
  if (FHandle = nil) then
    raise ESQLiteError.Create(OPEN_ERROR_MSG, 0);
  FSize := sqlite3_blob_bytes(FHandle);
  FCurrentOffset := 0;
end;

constructor TSQLiteBlobStream.Create(db: TSQLiteDatabase;
  const Table, Column: string; RowID: int64; readOnly: boolean = true);
begin
  Create(db, 'main', Table, Column, RowID, readOnly);
end;

destructor TSQLiteBlobStream.Destroy;
begin
  if (FHandle <> nil) then
    ESQLiteError.Test(sqlite3_blob_close(FHandle));
end;

procedure TSQLiteBlobStream.SetSize(NewSize: Longint);
begin
  raise ESQLiteError.Create(NO_SIZE_CHANGE_MSG,0);
end;

function TSQLiteBlobStream.Read(var Buffer; Count: Longint): Longint;
begin
  if (FCurrentOffset + Count > FSize) then
    Count := FSize - FCurrentOffset;
  ESQLiteError.Test(sqlite3_blob_read(FHandle, Pointer(@Buffer), Count,
    FCurrentOffset));
  Result := Count;
end;

function TSQLiteBlobStream.Write(const Buffer; Count: Longint): Longint;
begin
  if (FCurrentOffset + Count > FSize) then
    Count := size - FCurrentOffset;
  ESQLiteError.Test(sqlite3_blob_write(FHandle, Pointer(@Buffer), Count,
    FCurrentOffset));
  Result := Count;
end;

function TSQLiteBlobStream.Seek(Offset: Longint; Origin: Word): Longint;
begin
{$WARNINGS OFF}
  if (Origin = ord(soCurrent)) then
    Offset := Offset + FCurrentOffset
  else if (Origin = ord(soEnd)) then
    Offset := FSize + Offset;
{$WARNINGS ON}
  if (Offset >= 0) and (Offset <= FSize) then
    FCurrentOffset := Offset
  else
    raise ESQLiteError.Create(INVALID_OFFSET_MSG, 0);
  Result := FCurrentOffset;
end;

end.
