{ A DISQLite3 virtual table implementation using a TStringList for data storage. }
unit DISQLite3VtStringList;

{$I DICompilers.inc}
{$I DISQLite3.inc}

{$IFDEF DISQLite3_Personal}
!!! This project requires functionality unavailable in DISQLite3 Personal. !!!
!!! To compile, download DISQLite3 Pro from https://www.yunqa.de/delphi/   !!!
{$ENDIF DISQLite3_Personal}

{.$DEFINE DI_Debug}

interface

uses
  {$IFDEF HAS_UNITSCOPE}
  System.Classes, {$IFDEF DEPRECATED_SYSUTILS_ANSISTRINGS}System.AnsiStrings, {$ENDIF}
  {$ELSE HAS_UNITSCOPE}
  Classes,
  {$ENDIF HAS_UNITSCOPE}
  DISystemCompat, DISQLite3Api;

type
  TStringList_vtab = record
    Base: sqlite3_vtab;
    SL: TStrings;
    OwnsStringList: Boolean;
  end;
  PStringList_vtab = ^TStringList_vtab;

  TStringList_Cursor = record
    Base: PStringList_vtab;
    Idx: Integer;
  end;
  PStringList_Cursor = ^TStringList_Cursor;

  //------------------------------------------------------------------------------

  { }
function TStringList_Module_create(
  DB: sqlite3_ptr;
  pAux: Pointer;
  argc: Integer;
  const argv: PPUtf8CharArray;
  pVTab: sqlite3_vtab_ptr_ptr;
  pzErr: PPUtf8Char): Integer; {$IFDEF DISQLite3_CDECL}cdecl; {$ENDIF}

function TStringList_Module_Connect(
  DB: sqlite3_ptr;
  pAux: Pointer;
  argc: Integer;
  const argv: PPUtf8CharArray;
  pVTab: sqlite3_vtab_ptr_ptr;
  pzErr: PPUtf8Char): Integer; {$IFDEF DISQLite3_CDECL}cdecl; {$ENDIF}

function TStringList_Module_BestIndex(
  pVTab: sqlite3_vtab_ptr;
  Info: sqlite3_index_info_ptr): Integer; {$IFDEF DISQLite3_CDECL}cdecl; {$ENDIF}

function TStringList_Module_Destructor(
  pVTab: sqlite3_vtab_ptr): Integer; {$IFDEF DISQLite3_CDECL}cdecl; {$ENDIF}

function TStringList_Module_Disconnect(
  pVTab: sqlite3_vtab_ptr): Integer; {$IFDEF DISQLite3_CDECL}cdecl; {$ENDIF}

function TStringList_Module_Destroy(
  pVTab: sqlite3_vtab_ptr): Integer; {$IFDEF DISQLite3_CDECL}cdecl; {$ENDIF}

function TStringList_Module_Open(
  pVTab: sqlite3_vtab_ptr;
  ppCursor: sqlite3_vtab_cursor_ptr_ptr): Integer; {$IFDEF DISQLite3_CDECL}cdecl; {$ENDIF}

function TStringList_Module_Close(
  pCursor: sqlite3_vtab_cursor_ptr): Integer; {$IFDEF DISQLite3_CDECL}cdecl; {$ENDIF}

function TStringList_Module_Filter(
  pCursor: sqlite3_vtab_cursor_ptr;
  idxNum: Integer;
  const idxStr: PAnsiChar;
  argc: Integer;
  argv: sqlite3_value_ptr_array_ptr): Integer; {$IFDEF DISQLite3_CDECL}cdecl; {$ENDIF}

function TStringList_Module_Next(
  pCursor: sqlite3_vtab_cursor_ptr): Integer; {$IFDEF DISQLite3_CDECL}cdecl; {$ENDIF}

function TStringList_Module_EOF(
  pCursor: sqlite3_vtab_cursor_ptr): Integer; {$IFDEF DISQLite3_CDECL}cdecl; {$ENDIF}

function TStringList_Module_Column(
  pCursor: sqlite3_vtab_cursor_ptr;
  pCtx: sqlite3_context_ptr;
  i: Integer): Integer; {$IFDEF DISQLite3_CDECL}cdecl; {$ENDIF}

function TStringList_Module_RowID(
  pCursor: sqlite3_vtab_cursor_ptr;
  pRowID: PInt64): Integer; {$IFDEF DISQLite3_CDECL}cdecl; {$ENDIF}

function TStringList_Module_Update(
  pVTab: sqlite3_vtab_ptr;
  argc: Integer;
  argv: sqlite3_value_ptr_array_ptr;
  pRowID: PInt64): Integer; {$IFDEF DISQLite3_CDECL}cdecl; {$ENDIF}

function TStringList_Module_Begin(
  pVTab: sqlite3_vtab_ptr): Integer; {$IFDEF DISQLite3_CDECL}cdecl; {$ENDIF}

function TStringList_Module_Sync(
  pVTab: sqlite3_vtab_ptr): Integer; {$IFDEF DISQLite3_CDECL}cdecl; {$ENDIF}

function TStringList_Module_Commit(
  pVTab: sqlite3_vtab_ptr): Integer; {$IFDEF DISQLite3_CDECL}cdecl; {$ENDIF}

function TStringList_Module_Rollback(
  pVTab: sqlite3_vtab_ptr): Integer; {$IFDEF DISQLite3_CDECL}cdecl; {$ENDIF}

const
  TStringList_Module: sqlite3_module = (
    iVersion: 1;
    xCreate: TStringList_Module_create;
    xConnect: TStringList_Module_Connect;
    xBestIndex: TStringList_Module_BestIndex;
    xDisconnect: TStringList_Module_Disconnect;
    xDestroy: TStringList_Module_Destroy;
    xOpen: TStringList_Module_Open;
    xClose: TStringList_Module_Close;
    xFilter: TStringList_Module_Filter;
    xNext: TStringList_Module_Next;
    xEof: TStringList_Module_EOF;
    xColumn: TStringList_Module_Column;
    xRowID: TStringList_Module_RowID;
    xUpdate: TStringList_Module_Update;
    xBegin: TStringList_Module_Begin;
    xSync: nil; // TStringList_Module_Sync;
    xCommit: TStringList_Module_Commit;
    xRollback: TStringList_Module_Rollback;
    xFindFunction: nil // TStringList_Module_FindFunction
    );

implementation

uses
  {$IFDEF HAS_UNITSCOPE}System.SysUtils{$ELSE}SysUtils{$ENDIF};

{$IFNDEF COMPILER_6_UP}
function AnsiDequotedStr(const s: string; AQuote: Char): string;
var
  p: PChar;
begin
  p := PChar(s);
  Result := AnsiExtractQuotedStr(p, AQuote);
  if Result = '' then
    Result := s;
end;
{$ENDIF !COMPILER_6_UP}

function TStringList_Module_Constructor(
  DB: sqlite3_ptr;
  pAux: Pointer;
  argc: Integer;
  argv: PPUtf8CharArray;
  ppvtab: sqlite3_vtab_ptr_ptr;
  pzErr: PPUtf8Char): Integer;
var
  i: Integer;
  OwnsStringList: Boolean;
  s: string;
  s8: RawByteString;
  SL: TStringList;
  v: PStringList_vtab;
begin
  {$IFDEF DI_Debug}
  WriteLn('Argument Count: ', argc);
  for i := 0 to argc - 1 do
    WriteLn(argv[i]);
  {$ENDIF}

  { Try to prepare the stringlist. We catch all errors and report problems
    in the except block below. }
  SL := TStringList(pAux);
  OwnsStringList := not Assigned(SL);
  try
    if OwnsStringList then
      SL := TStringList.Create;

    { Check for optional filename parameter to read into the stringlist. }
    if argc > 3 then
      begin
        s := sqlite3_decode_utf8(argv^[3]);
        s := AnsiDequotedStr(s, '''');
        SL.LoadFromFile(s);
        { Assign our type of "RowIDs" to all strings in the list.
          We need them further down for inserts and deletes. }
        for i := 0 to SL.Count - 1 do
          SL.Objects[i] := TObject(i);
      end;
  except
    on e: Exception do
      begin
        if OwnsStringList then
          SL.Free;
        s8 := sqlite3_encode_utf8(e.Message);
        GetMem(pzErr^, Length(s8) + 1);
        {$IFDEF DEPRECATED_SYSUTILS_ANSISTRINGS}System.AnsiStrings.{$ENDIF}StrPCopy(pzErr^, s8);
        Result := SQLITE_ERROR;
        Exit;
      end;
  end;

  { If everything worked fine, allocate and declare the virtual table module. }
  sqlite3_declare_vtab(DB, 'CREATE TABLE x (Value TEXT);');
  v := AllocMem(SizeOf(v^));
  v.SL := SL;
  v.OwnsStringList := OwnsStringList;
  ppvtab^ := sqlite3_vtab_ptr(v);

  Result := SQLITE_OK;
end;

//------------------------------------------------------------------------------

function TStringList_Module_create(
  DB: sqlite3_ptr;
  pAux: Pointer;
  argc: Integer;
  const argv: PPUtf8CharArray;
  pVTab: sqlite3_vtab_ptr_ptr;
  pzErr: PPUtf8Char): Integer;
begin
  {$IFDEF DI_Debug}
  WriteLn('--- TStringList_Module_Create');
  {$ENDIF}

  Result := TStringList_Module_Constructor(DB, pAux, argc, argv, pVTab, pzErr);
end;

//------------------------------------------------------------------------------

function TStringList_Module_Connect(
  DB: sqlite3_ptr;
  pAux: Pointer;
  argc: Integer;
  const argv: PPUtf8CharArray;
  pVTab: sqlite3_vtab_ptr_ptr;
  pzErr: PPUtf8Char): Integer;
begin
  {$IFDEF DI_Debug}
  WriteLn('--- TStringList_Module_Connect');
  {$ENDIF}

  Result := TStringList_Module_Constructor(DB, pAux, argc, argv, pVTab, pzErr);
end;

//------------------------------------------------------------------------------

function TStringList_Module_BestIndex(
  pVTab: sqlite3_vtab_ptr;
  Info: sqlite3_index_info_ptr): Integer;
var
  v: PStringList_vtab;
begin
  {$IFDEF DI_Debug}
  WriteLn('--- TStringList_Module_BestIndex');
  {$ENDIF}

  v := PStringList_vtab(pVTab);
  Info^.estimatedCost := v^.SL.Count;
  Result := SQLITE_OK;
end;

//------------------------------------------------------------------------------

function TStringList_Module_Destructor(
  pVTab: sqlite3_vtab_ptr): Integer;
var
  v: PStringList_vtab;
begin
  {$IFDEF DI_Debug}
  WriteLn('--- TStringList_Module_Destructor');
  {$ENDIF}

  v := PStringList_vtab(pVTab);
  if v^.OwnsStringList then
    v.SL.Free;

  FreeMem(pVTab);
  Result := SQLITE_OK;
end;

//------------------------------------------------------------------------------

function TStringList_Module_Disconnect(
  pVTab: sqlite3_vtab_ptr): Integer;
begin
  {$IFDEF DI_Debug}
  WriteLn('--- TStringList_Module_Disconnect');
  {$ENDIF}

  Result := TStringList_Module_Destructor(pVTab);
end;

//------------------------------------------------------------------------------

function TStringList_Module_Destroy(
  pVTab: sqlite3_vtab_ptr): Integer;
begin
  {$IFDEF DI_Debug}
  WriteLn('--- TStringList_Module_Destroy');
  {$ENDIF}

  Result := TStringList_Module_Destructor(pVTab);
end;

//------------------------------------------------------------------------------

function TStringList_Module_Open(
  pVTab: sqlite3_vtab_ptr;
  ppCursor: sqlite3_vtab_cursor_ptr_ptr): Integer;
var
  c: PStringList_Cursor;
begin
  {$IFDEF DI_Debug}
  WriteLn('--- TStringList_Module_Open');
  {$ENDIF}

  c := AllocMem(SizeOf(c^));
  ppCursor^ := sqlite3_vtab_cursor_ptr(c);

  Result := SQLITE_OK;
end;

//------------------------------------------------------------------------------

function TStringList_Module_Close(
  pCursor: sqlite3_vtab_cursor_ptr): Integer;
begin
  {$IFDEF DI_Debug}
  WriteLn('--- TStringList_Module_Close');
  {$ENDIF}

  FreeMem(pCursor);
  Result := SQLITE_OK;
end;

//------------------------------------------------------------------------------

function TStringList_Module_Filter(
  pCursor: sqlite3_vtab_cursor_ptr;
  idxNum: Integer;
  const idxStr: PAnsiChar;
  argc: Integer;
  argv: sqlite3_value_ptr_array_ptr): Integer;
var
  c: PStringList_Cursor;
begin
  {$IFDEF DI_Debug}
  WriteLn('--- TStringList_Module_Filter');
  {$ENDIF}

  c := PStringList_Cursor(pCursor);
  c^.Idx := 0;
  Result := SQLITE_OK;
end;

//------------------------------------------------------------------------------

function TStringList_Module_Next(
  pCursor: sqlite3_vtab_cursor_ptr): Integer;
var
  c: PStringList_Cursor;
begin
  {$IFDEF DI_Debug}
  WriteLn('--- TStringList_Module_Next');
  {$ENDIF}

  c := PStringList_Cursor(pCursor);
  Inc(c^.Idx);
  // This function was successfull. Eof will be called for further checks.
  Result := SQLITE_OK;
end;

//------------------------------------------------------------------------------

function TStringList_Module_EOF(
  pCursor: sqlite3_vtab_cursor_ptr): Integer;
var
  c: PStringList_Cursor;
  v: PStringList_vtab;
begin
  {$IFDEF DI_Debug}
  WriteLn('--- TStringList_Module_EOF');
  {$ENDIF}

  c := PStringList_Cursor(pCursor);

  v := c^.Base;
  if c^.Idx < v.SL.Count then
    Result := 0
  else
    Result := 1;
end;

//------------------------------------------------------------------------------

function TStringList_Module_Column(
  pCursor: sqlite3_vtab_cursor_ptr;
  pCtx: sqlite3_context_ptr;
  i: Integer): Integer;
var
  c: PStringList_Cursor;
  v: PStringList_vtab;
  s8: Utf8String;
begin
  {$IFDEF DI_Debug}
  WriteLn('--- TStringList_Module_Column');
  {$ENDIF}

  c := PStringList_Cursor(pCursor);
  v := c^.Base;
  s8 := sqlite3_encode_utf8(v^.SL.Strings[c^.Idx]);

  sqlite3_result_text(
    pCtx,
    PUtf8Char(s8), Length(s8),
    SQLITE_TRANSIENT);

  Result := SQLITE_OK;
end;

//------------------------------------------------------------------------------

function TStringList_Module_RowID(
  pCursor: sqlite3_vtab_cursor_ptr;
  pRowID: PInt64): Integer;
var
  c: PStringList_Cursor;
begin
  {$IFDEF DI_Debug}
  WriteLn('--- TStringList_Module_RowID');
  {$ENDIF}

  c := PStringList_Cursor(pCursor);
  if c^.Idx < c^.Base^.SL.Count then
    begin
      pRowID^ := Integer(c^.Base^.SL.Objects[c^.Idx]);
      Result := SQLITE_OK;
    end
  else
    Result := SQLITE_ERROR;
end;

//------------------------------------------------------------------------------

function TStringList_Module_Update(
  pVTab: sqlite3_vtab_ptr;
  argc: Integer;
  argv: sqlite3_value_ptr_array_ptr;
  pRowID: PInt64): Integer;
var
  v: PStringList_vtab;
  i, RowID_0, RowID_1: Integer;
  s: string;
begin
  {$IFDEF DI_Debug}
  WriteLn('--- TStringList_Module_Update');
  {$ENDIF}

  v := PStringList_vtab(pVTab);

  if argc = 1 then
    begin
      // Delete single row with RowID argv[0].
      RowID_0 := sqlite3_value_int(argv[0]);
      i := v^.SL.IndexOfObject(TObject(RowID_0));
      if i >= 0 then
        v^.SL.Delete(i);
    end
  else
    if argc > 1 then
      if sqlite3_value_type(argv[1]) = SQLITE_NULL then
        begin
          // Insert new row, choose new RowID ourselves.
          RowID_1 := 0;
          while v^.SL.IndexOfObject(TObject(RowID_1)) >= 0 do
            Inc(RowID_1);
          s := sqlite3_decode_utf8(sqlite3_value_str(argv[2]));
          v^.SL.AddObject(s, TObject(RowID_1));
          pRowID^ := RowID_1;
        end
      else
        begin
          RowID_1 := sqlite3_value_int(argv[1]);
          if sqlite3_value_type(argv[0]) = SQLITE_NULL then
            begin
              // Insert new row with RowID argv[1].
              s := sqlite3_decode_utf8(sqlite3_value_str(argv[2]));
              v^.SL.AddObject(s, TObject(RowID_1));
              pRowID^ := RowID_1;
            end
          else
            begin
              RowID_0 := sqlite3_value_int(argv[0]);

              if RowID_0 = RowID_1 then
                begin
                  // Update existing row RowID with new values.
                  i := v^.SL.IndexOfObject(TObject(RowID_0));
                  if i >= 0 then
                    begin
                      s := sqlite3_decode_utf8(sqlite3_value_str(argv[2]));
                      v^.SL.Strings[i] := s;
                    end;
                end
              else
                begin
                  // Update existing row RowID with new RowID and new values.
                  i := v^.SL.IndexOfObject(TObject(RowID_0));
                  if i >= 0 then
                    begin
                      s := sqlite3_decode_utf8(sqlite3_value_str(argv[2]));
                      v^.SL.Strings[i] := s;
                      v^.SL.Objects[i] := TObject(RowID_1);
                    end;
                end;
            end;
        end;
  Result := SQLITE_OK;
end;

//------------------------------------------------------------------------------

{ TStrings does not have transactions, but we can call BeginUpdate to
  speed up operations on some slow string lists. }
function TStringList_Module_Begin(
  pVTab: sqlite3_vtab_ptr): Integer;
var
  v: PStringList_vtab;
begin
  {$IFDEF DI_Debug}
  WriteLn('--- TStringList_Module_Begin');
  {$ENDIF}

  v := PStringList_vtab(pVTab);
  v^.SL.BeginUpdate;
  Result := SQLITE_OK;
end;

//------------------------------------------------------------------------------

function TStringList_Module_Sync(
  pVTab: sqlite3_vtab_ptr): Integer;
begin
  {$IFDEF DI_Debug}
  WriteLn('--- TStringList_Module_Sync');
  {$ENDIF}

  Result := SQLITE_OK;
end;

//------------------------------------------------------------------------------

function TStringList_Module_Commit(
  pVTab: sqlite3_vtab_ptr): Integer;
var
  v: PStringList_vtab;
begin
  {$IFDEF DI_Debug}
  WriteLn('--- TStringList_Module_Commit');
  {$ENDIF}

  v := PStringList_vtab(pVTab);
  v^.SL.EndUpdate;
  Result := SQLITE_OK;
end;

//------------------------------------------------------------------------------

function TStringList_Module_Rollback(
  pVTab: sqlite3_vtab_ptr): Integer;
var
  v: PStringList_vtab;
begin
  {$IFDEF DI_Debug}
  WriteLn('--- TStringList_Module_Rollback');
  {$ENDIF}

  v := PStringList_vtab(pVTab);
  v^.SL.EndUpdate;
  Result := SQLITE_ERROR;
end;

//------------------------------------------------------------------------------

end.

