/// SQLite3 embedded Database engine used as the mORMot SQL kernel
// - this unit is a part of the freeware Synopse mORMot framework,
// licensed under a MPL/GPL/LGPL tri-license; version 1.18
unit mORMotSQLite3;

{
    This file is part of Synopse mORMot framework.

    Synopse mORMot framework. Copyright (C) 2022 Arnaud Bouchez
      Synopse Informatique - https://synopse.info

  *** BEGIN LICENSE BLOCK *****
  Version: MPL 1.1/GPL 2.0/LGPL 2.1

  The contents of this file are subject to the Mozilla Public License Version
  1.1 (the "License"); you may not use this file except in compliance with
  the License. You may obtain a copy of the License at
  http://www.mozilla.org/MPL

  Software distributed under the License is distributed on an "AS IS" basis,
  WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  for the specific language governing rights and limitations under the License.

  The Original Code is Synopse mORMot framework.

  The Initial Developer of the Original Code is Arnaud Bouchez.

  Portions created by the Initial Developer are Copyright (C) 2022
  the Initial Developer. All Rights Reserved.

  Contributor(s):
  - Ondrej
  - Mario Moretti

  Alternatively, the contents of this file may be used under the terms of
  either the GNU General Public License Version 2 or later (the "GPL"), or
  the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  in which case the provisions of the GPL or the LGPL are applicable instead
  of those above. If you wish to allow use of your version of this file only
  under the terms of either the GPL or the LGPL, and not to allow others to
  use your version of this file under the terms of the MPL, indicate your
  decision by deleting the provisions above and replace them with the notice
  and other provisions required by the GPL or the LGPL. If you do not delete
  the provisions above, a recipient may use your version of this file under
  the terms of any one of the MPL, the GPL or the LGPL.

  ***** END LICENSE BLOCK *****
}

interface

{$I Synopse.inc} // define HASINLINE CPU32 CPU64 WITHLOG

uses
  {$ifdef MSWINDOWS}
  Windows,
  {$else}
  {$ifdef FPC}
  SynFPCLinux,
  BaseUnix,
  {$endif}
  {$ifdef KYLIX3}
  Types,
  LibC,
  SynKylix,
  {$endif}
  {$endif}
  SysUtils,
  Classes,
  {$ifndef LVCL}
  SyncObjs, // for TCriticalSection inlining
  Contnrs,
  {$endif}
  SynZip,
  SynCommons,
  SynLog,
  SynSQLite3,
  SynTable,
  mORMot;

{.$define WITHUNSAFEBACKUP}
{ define this if you really need the old blocking TSQLRestServerDB backup methods
  - those methods are deprecated - you should use DB.BackupBackground() instead }


{ ****************** SQLite3 database used as kernel of our mORMot framework }

type
  /// Execute a SQL statement in the local SQLite3 database engine, and get
  // result in memory
  // - all DATA (even the BLOB fields) is converted into UTF-8 TEXT
  // - uses a TSQLTableJSON internaly: faster than sqlite3_get_table()
  // (less memory allocation/fragmentation) and allows efficient caching
  TSQLTableDB = class(TSQLTableJSON)
  private
  public
    /// Execute a SQL statement, and init TSQLTable fields
    // - FieldCount=0 if no result is returned
    // - the BLOB data is converted into TEXT: you have to retrieve it with
    //  a special request explicitely (note that JSON format returns BLOB data)
    // - uses a TSQLTableJSON internaly: all currency is transformed to its floating
    //   point TEXT representation, and allows efficient caching
    // - if the SQL statement is in the DB cache, it's retrieved from its cached
    //   value: our JSON parsing is a lot faster than SQLite3 engine itself,
    //   and uses less memory
    // - will raise an ESQLException on any error
    constructor Create(aDB: TSQLDatabase; const Tables: array of TSQLRecordClass;
      const aSQL: RawUTF8; Expand: boolean); reintroduce;
  end;

  /// class-reference type (metaclass) of a REST server using SQLite3 as main engine
  TSQLRestServerDBClass = class of TSQLRestServerDB;

  TSQLVirtualTableModuleServerDB = class;

  /// REST server with direct access to a SQLite3 database
  // - caching is handled at TSQLDatabase level
  // - SQL statements for record retrieval from ID are prepared for speed
  TSQLRestServerDB = class(TSQLRestServer)
  private
    /// access to the associated SQLite3 database engine
    fDB: TSQLDataBase;
    /// initialized by Create(aModel,aDBFileName)
    fOwnedDB: TSQLDataBase;
    /// prepared statements with parameters for faster SQLite3 execution
    // - used for SQL code with :(%): internal parameters
    fStatementCache: TSQLStatementCached;
    /// used during GetAndPrepareStatement() execution (run in global lock)
    fStatement: PSQLRequest;
    fStaticStatement: TSQLRequest;
    fStatementTimer: PPrecisionTimer;
    fStatementMonitor: TSynMonitor;
    fStaticStatementTimer: TPrecisionTimer;
    fStatementSQL: RawUTF8;
    fStatementGenericSQL: RawUTF8;
    fStatementMaxParam: integer;
    fStatementLastException: RawUTF8;
    fStatementTruncateSQLLogLen: integer;
    fStatementPreparedSelectQueryPlan: boolean;
    /// check if a VACUUM statement is possible
    // - VACUUM in fact DISCONNECT all virtual modules (sounds like a SQLite3
    // design problem), so calling it during process could break the engine
    // - if you can safely run VACUUM, returns TRUE and release all active
    // SQL statements (otherwise VACUUM will fail)
    // - if there are some static virtual tables, returns FALSE and do nothing:
    // in this case, VACUUM will be a no-op
    function PrepareVacuum(const aSQL: RawUTF8): boolean;
  protected
    fBatchMethod: TSQLURIMethod;
    fBatchOptions: TSQLRestBatchOptions;
    fBatchTableIndex: integer;
    fBatchID: TIDDynArray;
    fBatchIDCount: integer;
    fBatchIDMax: TID;
    fBatchValues: TRawUTF8DynArray;
    fBatchValuesCount: integer;
    constructor RegisteredClassCreateFrom(aModel: TSQLModel;
      aServerHandleAuthentication: boolean; aDefinition: TSynConnectionDefinition); override;
    /// retrieve a TSQLRequest instance in fStatement
    // - will set @fStaticStatement if no :(%): internal parameters appear:
    // in this case, the TSQLRequest.Close method must be called
    // - will set a @fStatementCache[].Statement, after having bounded the
    // :(%): parameter values; in this case, TSQLRequest.Close must not be called
    // - expect sftBlob, sftBlobDynArray and sftBlobRecord properties
    // to be encoded as ':("\uFFF0base64encodedbinary"):'
    procedure GetAndPrepareStatement(const SQL: RawUTF8; ForceCacheStatement: boolean);
    /// free a static prepared statement on success or from except on E: Exception block
    procedure GetAndPrepareStatementRelease(E: Exception=nil; const Msg: ShortString='';
      ForceBindReset: boolean=false);
    /// create or retrieve from the cache a TSQLRequest instance in fStatement
    // - called e.g. by GetAndPrepareStatement()
    procedure PrepareStatement(Cached: boolean);
    /// reset the cache if necessary
    procedure SetNoAJAXJSON(const Value: boolean); override;
    {$ifdef WITHLOG}
    /// overriden method which will also set the DB.LogClass
    procedure SetLogClass(aClass: TSynLogClass); override;
    {$endif}
    /// overridden methods for direct sqlite3 database engine call:
    function MainEngineList(const SQL: RawUTF8; ForceAJAX: Boolean; ReturnedRowCount: PPtrInt): RawUTF8; override;
    function MainEngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; override;
    function MainEngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; override;
    function MainEngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; override;
    function MainEngineDelete(TableModelIndex: integer; ID: TID): boolean; override;
    function MainEngineDeleteWhere(TableModelIndex: Integer; const SQLWhere: RawUTF8;
      const IDs: TIDDynArray): boolean; override;
    function MainEngineRetrieveBlob(TableModelIndex: integer; aID: TID;
      BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean; override;
    function MainEngineUpdateBlob(TableModelIndex: integer; aID: TID;
      BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean; override;
    function MainEngineUpdateField(TableModelIndex: integer;
      const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; override;
    function MainEngineUpdateFieldIncrement(TableModelIndex: integer; ID: TID;
      const FieldName: RawUTF8; Increment: Int64): boolean; override;
    function EngineExecute(const aSQL: RawUTF8): boolean; override;
    procedure InternalStat(Ctxt: TSQLRestServerURIContext; W: TTextWriter); override;
    procedure InternalInfo(var info: TDocVariantData); override;
    /// execute one SQL statement
    // - intercept any DB exception and return false on error, true on success
    // - optional LastInsertedID can be set (if ValueInt/ValueUTF8 are nil) to
    // retrieve the proper ID when aSQL is an INSERT statement (thread safe)
    // - optional LastChangeCount can be set (if ValueInt/ValueUTF8 are nil) to
    // retrieve the modified row count when aSQL is an UPDATE statement (thread safe)
    function InternalExecute(const aSQL: RawUTF8; ForceCacheStatement: boolean;
      ValueInt: PInt64=nil; ValueUTF8: PRawUTF8=nil; ValueInts: PInt64DynArray=nil;
      LastInsertedID: PInt64=nil; LastChangeCount: PInteger=nil): boolean;
    // overridden method returning TRUE for next calls to EngineAdd
    // will properly handle operations until InternalBatchStop is called
    function InternalBatchStart(Method: TSQLURIMethod;
      BatchOptions: TSQLRestBatchOptions): boolean; override;
    // internal method called by TSQLRestServer.RunBatch() to process fast
    // multi-INSERT statements to the SQLite3 engine
    procedure InternalBatchStop; override;
  public
    /// begin a transaction (implements REST BEGIN Member)
    // - to be used to speed up some SQL statements like Insert/Update/Delete
    // - must be ended with Commit on success
    // - must be aborted with Rollback if any SQL statement failed
    // - return true if no transaction is active, false otherwise
    function TransactionBegin(aTable: TSQLRecordClass; SessionID: cardinal=1): boolean; override;
    /// end a transaction (implements REST END Member)
    // - write all pending SQL statements to the disk
    procedure Commit(SessionID: cardinal=1; RaiseException: boolean=false); override;
    /// abort a transaction (implements REST ABORT Member)
    // - restore the previous state of the database, before the call to TransactionBegin
    procedure RollBack(SessionID: cardinal=1); override;

     /// overridden method for direct SQLite3 database engine call
     // - it will update all BLOB fields at once, in one SQL statement
    function UpdateBlobFields(Value: TSQLRecord): boolean; override;
     /// overridden method for direct SQLite3 database engine call
     // - it will retrieve all BLOB fields at once, in one SQL statement
    function RetrieveBlobFields(Value: TSQLRecord): boolean; override;

    {$ifndef KYLIX3}
    /// backup of the opened Database into an external stream (e.g. a file,
    //  compressed or not)
    // - DEPRECATED: use DB.BackupBackground() instead
    // - this method doesn't use the SQLite Online Backup API, but low-level
    // database file copy which may lock the database process if the data
    // is consistent - consider using DB.BackupBackground() method instead
    // - database is closed, VACCUUMed, copied, then reopened
    function Backup(Dest: TStream): boolean; deprecated;
    /// backup of the opened Database into a .gz compressed file
    // - DEPRECATED: use DB.BackupBackground() instead
    // - this method doesn't use the SQLite Online Backup API, but low-level
    //   database file copy which may lock the database process if the data
    //   is consistent - consider using DB.BackupBackground() method instead
    // - database is closed, VACCUUMed, compressed into .gz file, then reopened
    // - default compression level is 2, which is very fast, and good enough for
    //   a database file content: you may change it into the default 6 level
    function BackupGZ(const DestFileName: TFileName;
      CompressionLevel: integer=2): boolean; deprecated;
    {$endif}

    /// restore a database content on the fly
    // - database is closed, source DB file is replaced by the supplied content,
    //   then reopened
    // - there are cases where this method will fail and return FALSE: consider
    //   shuting down the server, replace the file, then relaunch the server instead
    function Restore(const ContentToRestore: RawByteString): boolean;
    /// restore a database content on the fly, from a .gz compressed file
    // - database is closed, source DB file is replaced by the supplied content,
    //  then reopened
    // - there are cases where this method will fail and return FALSE: consider
    //   shuting down the server, replace the file, then relaunch the server instead
    function RestoreGZ(const BackupFileName: TFileName): boolean;
    /// used e.g. by IAdministratedDaemon to implement "pseudo-SQL" commands
    procedure AdministrationExecute(const DatabaseName,SQL: RawUTF8;
      var result: TServiceCustomAnswer); override;
    /// retrieves the per-statement detailed timing, as a TDocVariantData
    procedure ComputeDBStats(out result: variant); overload;
    /// retrieves the per-statement detailed timing, as a TDocVariantData
    function ComputeDBStats: variant; overload;

    /// initialize the associated DB connection
    // - called by Create and on Backup/Restore just after DB.DBOpen
    // - will register all *_in() functions for available TSQLRecordRTree
    // - will register all modules for available TSQLRecordVirtualTable*ID
    // with already registered modules via RegisterVirtualTableModule()
    // - you can override this method to call e.g. DB.RegisterSQLFunction()
    procedure InitializeEngine; virtual;
    /// call this method when the internal DB content is known to be invalid
    // - by default, all REST/CRUD requests and direct SQL statements are
    // scanned and identified as potentially able to change the internal SQL/JSON
    // cache used at SQLite3 database level; but some virtual tables (e.g.
    // TSQLRestStorageExternal classes defined in SQLite3DB) could flush
    // the database content without proper notification
    // - this overridden implementation will call TSQLDataBase.CacheFlush method
    procedure FlushInternalDBCache; override;
    /// call this method to flush the internal SQL prepared statements cache
    // - you should not have to flush the cache, only e.g. before a DROP TABLE
    // - in all cases, running this method would never harm, nor be slow
    procedure FlushStatementCache;
    /// execute one SQL statement, and apply an Event to every record
    // - lock the database during the run
    // - call a fast "stored procedure"-like method for each row of the request;
    // this method must use low-level DB access in any attempt to modify the
    // database (e.g. a prepared TSQLRequest with Reset+Bind+Step), and not
    // the TSQLRestServerDB.Engine*() methods which include a Lock(): this Lock()
    // is performed by the main loop in EngineExecute() and any attempt to
    // such high-level call will fail into an endless loop
    // - caller may use a transaction in order to speed up StoredProc() writing
    // - intercept any DB exception and return false on error, true on success
    function StoredProcExecute(const aSQL: RawUTF8; StoredProc: TOnSQLStoredProc): boolean;
  public
    /// initialize a REST server with a SQLite3 database
    // - any needed TSQLVirtualTable class should have been already registered
    // via the RegisterVirtualTableModule() method
    constructor Create(aModel: TSQLModel; aDB: TSQLDataBase;
      aHandleUserAuthentication: boolean=false; aOwnDB: boolean=false); reintroduce; overload; virtual;
    /// initialize a REST server with a database, by specifying its filename
    // - TSQLRestServerDB will initialize a owned TSQLDataBase, and free it on Destroy
    // - if specified, the password will be used to cypher this file on disk
    // (the main SQLite3 database file is encrypted, not the wal file during run)
    // - it will then call the other overloaded constructor to initialize the server
    constructor Create(aModel: TSQLModel; const aDBFileName: TFileName;
      aHandleUserAuthentication: boolean=false; const aPassword: RawUTF8='';
      aDefaultCacheSize: integer=10000; aDefaultPageSize: integer=4096); reintroduce; overload;
    /// initialize a REST server with a database, and a temporary Database Model
    // - a Model will be created with supplied tables, and owned by the server
    // - if you instantiate a TSQLRestServerFullMemory or TSQLRestServerDB
    // with this constructor, an in-memory engine will be created, with
    // enough abilities to run regression tests, for instance
    constructor CreateWithOwnModel(const aTables: array of TSQLRecordClass;
      const aDBFileName: TFileName; aHandleUserAuthentication: boolean=false;
      const aRoot: RawUTF8='root'; const aPassword: RawUTF8='';
      aDefaultCacheSize: integer=10000; aDefaultPageSize: integer=4096); overload;
    /// initialize a REST server with an in-memory SQLite3 database
    // - could be used for test purposes
    constructor Create(aModel: TSQLModel; aHandleUserAuthentication: boolean=false); overload; override;
    /// initialize a REST server with an in-memory SQLite3 database and a
    // temporary Database Model
    // - could be used for test purposes
    constructor CreateWithOwnModel(const aTables: array of TSQLRecordClass;
      aHandleUserAuthentication: boolean=false); overload;
    /// close database and free used memory
    destructor Destroy; override;
    /// save the TSQLRestServerDB properties into a persistent storage object
    // - RegisteredClassCreateFrom() will expect Definition.DatabaseName to store
    // the DBFileName, and optionally encrypt the file using Definition.Password
    procedure DefinitionTo(Definition: TSynConnectionDefinition); override;
    /// Missing tables are created if they don't exist yet for every TSQLRecord
    // class of the Database Model
    // - you must call explicitely this before having called StaticDataCreate()
    // - all table description (even Unique feature) is retrieved from the Model
    // - this method also create additional fields, if the TSQLRecord definition
    // has been modified; only field adding is available, field renaming or
    // field deleting are not allowed in the FrameWork (in such cases, you must
    // create a new TSQLRecord type)
    procedure CreateMissingTables(user_version: cardinal=0;
      Options: TSQLInitializeTableOptions=[]); override;
    /// search for the last inserted ID in a table
    // - will execute not default select max(rowid) from Table, but faster
    // $ select rowid from Table order by rowid desc limit 1
    function TableMaxID(Table: TSQLRecordClass): TID; override;
    /// after how many bytes a sllSQL statement log entry should be truncated
    // - default is 0, meaning no truncation
    // - typical value is 2048 (2KB), which will avoid any heap allocation
    property StatementTruncateSQLLogLen: integer read fStatementTruncateSQLLogLen
      write fStatementTruncateSQLLogLen;
    /// executes (therefore log) the QUERY PLAN for each prepared statement
    property StatementPreparedSelectQueryPlan: boolean
      read fStatementPreparedSelectQueryPlan write fStatementPreparedSelectQueryPlan;
  published
    /// associated database
    property DB: TSQLDataBase read fDB;
    /// contains some textual information about the latest Exception raised
    // during SQL statement execution
    property StatementLastException: RawUTF8 read fStatementLastException;
  end;

  /// REST client with direct access to a SQLite3 database
  // - a hidden TSQLRestServerDB server is created and called internaly
  TSQLRestClientDB = class(TSQLRestClientURI)
  private
    // use internaly a TSQLRestServerDB to access data in the proper JSON format
    fServer: TSQLRestServerDB;
    fOwnedServer: TSQLRestServerDB;
    fOwnedDB: TSQLDataBase;
    fInternalHeader: RawUTF8;
    function getDB: TSQLDataBase;
  protected
    /// method calling the RESTful server fServer
    procedure InternalURI(var Call: TSQLRestURIParams); override;
    /// overridden protected method do nothing (direct DB access has no connection)
    function InternalCheckOpen: boolean; override;
    /// overridden protected method do nothing (direct DB access has no connection)
    procedure InternalClose; override;
  public
    /// initializes the class, and creates an internal TSQLRestServerDB to
    // internaly answer to the REST queries
    // - aServerClass could be TSQLRestServerDB by default
    constructor Create(aClientModel, aServerModel: TSQLModel; aDB: TSQLDataBase;
      aServerClass: TSQLRestServerDBClass;
      aHandleUserAuthentication: boolean=false); reintroduce; overload;
    /// same as above, from a SQLite3 filename specified
    // - an internal TSQLDataBase will be created internaly and freed on Destroy
    // - aServerClass could be TSQLRestServerDB by default
    // - if specified, the password will be used to cypher this file on disk
    // (the main SQLite3 database file is encrypted, not the wal file during run)
    constructor Create(aClientModel, aServerModel: TSQLModel; const aDBFileName: TFileName;
      aServerClass: TSQLRestServerDBClass; aHandleUserAuthentication: boolean=false;
      const aPassword: RawUTF8=''; aDefaultCacheSize: integer=10000); reintroduce; overload;
    /// initialize the class, for an existing TSQLRestServerDB
    // - the client TSQLModel will be cloned from the server's one
    // - the TSQLRestServerDB and TSQLDatabase instances won't be managed by the
    // client, but will access directly to the server
    constructor Create(aRunningServer: TSQLRestServerDB); reintroduce; overload;
    /// release the server
    destructor Destroy; override;

    /// retrieve a list of members as a TSQLTable (implements REST GET Collection)
    // - this overridden method call directly the database to get its result,
    // without any URI() call, but with use of DB JSON cache if available
    // - other TSQLRestClientDB methods use URI() function and JSON conversion
    // of only one record properties values, which is very fast
    function List(const Tables: array of TSQLRecordClass; const SQLSelect: RawUTF8='ID';
      const SQLWhere: RawUTF8=''): TSQLTableJSON; override;
    /// associated Server
    property Server: TSQLRestServerDB read fServer;
    /// associated database
    property DB: TSQLDataBase read getDB;
  end;

  /// define a Virtual Table module for a stand-alone SQLite3 engine
  // - it's not needed to free this instance: it will be destroyed by the SQLite3
  // engine together with the DB connection
  TSQLVirtualTableModuleSQLite3 = class(TSQLVirtualTableModule)
  protected
    fDB: TSQLDataBase;
    /// used internaly to register the module to the SQLite3 engine
    fModule: TSQLite3Module;
  public
    /// initialize the module for a given DB connection
    // - internally set fModule and call sqlite3_create_module_v2(fModule)
    // - will raise EBusinessLayerException if aDB is incorrect, or SetDB() has
    // already been called for this module
    // - will call sqlite3_check() to raise the corresponding ESQLite3Exception
    // - in case of success (no exception), the SQLite3 engine will release the
    // module by itself; but in case of error (an exception is raised), it is
    // up to the caller to intercept it via a try..except and free the
    // TSQLVirtualTableModuleSQLite3 instance
    procedure Attach(aDB: TSQLDataBase);
    /// retrieve the file name to be used for a specific Virtual Table
    // - overridden method returning a file located in the DB file folder, and
    // '' if the main DB was created as SQLITE_MEMORY_DATABASE_NAME (i.e.
    // ':memory:' so that no file should be written)
    // - of course, if a custom FilePath property value is specified, it will be
    // used, even if the DB is created as SQLITE_MEMORY_DATABASE_NAME
    function FileName(const aTableName: RawUTF8): TFileName; override;
    /// the associated SQLite3 database connection
    property DB: TSQLDataBase read fDB;
  end;

  /// define a Virtual Table module for a TSQLRestServerDB SQLite3 engine
  TSQLVirtualTableModuleServerDB = class(TSQLVirtualTableModuleSQLite3)
  public
    /// register the Virtual Table to the database connection of a TSQLRestServerDB server
    // - in case of an error, an excepton will be raised
    constructor Create(aClass: TSQLVirtualTableClass; aServer: TSQLRestServer); override;
  end;


  /// REST storage sharded over several SQlite3 instances
  // - numerotated '*0000.dbs' SQLite3 files would contain the sharded data
  // - here *.dbs is used as extension, to avoid any confusion with regular
  // SQLite3 database files (*.db or *.db3)
  // - when the server is off (e.g. on periodic version upgrade), you may safely
  // delete/archive some oldest *.dbs files, for easy and immediate purge of
  // your database content: such process would be much faster and cleaner than
  // regular "DELETE FROM TABLE WHERE ID < ?" + "VACUUM" commands
  TSQLRestStorageShardDB = class(TSQLRestStorageShard)
  protected
    fShardRootFileName: TFileName;
    fSynchronous: TSQLSynchronousMode;
    fInitShardsIsLast: boolean;
    fCacheSizePrevious, fCacheSizeLast: integer;
    procedure InitShards; override;
    function InitNewShard: TSQLRest; override;
    function DBFileName(ShardIndex: Integer): TFileName; virtual;
  public
    /// initialize the table storage redirection for sharding over SQLite3 DB
    // - if no aShardRootFileName is set, the executable folder and stored class
    // table name would be used
    // - typical use may be:
    // ! Server.StaticDataAdd(TSQLRestStorageShardDB.Create(TSQLRecordSharded,Server,500000))
    // - you may define some low-level tuning of SQLite3 process via aSynchronous
    // / aCacheSizePrevious / aCacheSizeLast / aMaxShardCount parameters, if
    // the default smOff / 1MB / 2MB / 100 values are not enough
    constructor Create(aClass: TSQLRecordClass; aServer: TSQLRestServer;
      aShardRange: TID; aOptions: TSQLRestStorageShardOptions=[];
      const aShardRootFileName: TFileName=''; aMaxShardCount: integer=100;
      aSynchronous: TSQLSynchronousMode=smOff;
      aCacheSizePrevious: integer=250; aCacheSizeLast: integer=500); reintroduce; virtual;
  published
    /// associated file name for the SQLite3 database files
    // - contains the folder, and root file name for the storage
    // - each shard would end with its 4 digits index: actual file name would
    // append '0000.dbs' to this ShardRootFileName
    property ShardRootFileName: TFileName read fShardRootFileName;
  end;



/// initialize a Virtual Table Module for a specified database
// - to be used for low-level access to a virtual module, e.g. with
// TSQLVirtualTableLog
// - when using our ORM, you should call TSQLModel.VirtualTableRegister()
// instead to associate a TSQLRecordVirtual class to a module
// - returns the created TSQLVirtualTableModule instance (which will be a
// TSQLVirtualTableModuleSQLite3 instance in fact)
// - will raise an exception of failure
function RegisterVirtualTableModule(aModule: TSQLVirtualTableClass;
  aDatabase: TSQLDataBase): TSQLVirtualTableModule;


implementation

{$ifdef SQLVIRTUALLOGS}
uses
  mORMotDB;
{$endif SQLVIRTUALLOGS}


{ TSQLTableDB }

constructor TSQLTableDB.Create(aDB: TSQLDataBase; const Tables: array of TSQLRecordClass;
  const aSQL: RawUTF8; Expand: boolean);
var JSONCached: RawUTF8;
    R: TSQLRequest;
    n: PtrInt;
begin
  if aDB=nil then
    exit;
  JSONCached := aDB.LockJSON(aSQL,@n);
  if JSONCached='' then // not retrieved from cache -> call SQLite3 engine
    try // faster than sqlite3_get_table(): memory is allocated as a whole
      n := 0;
      JSONCached := R.ExecuteJSON(aDB.DB,aSQL,Expand,@n); // Expand=true for AJAX
      inherited CreateFromTables(Tables,aSQL,JSONCached);
      Assert(n=fRowCount);
    finally
      aDB.UnLockJSON(JSONCached,n);
    end
  else begin
    inherited CreateFromTables(Tables,aSQL,JSONCached);
    Assert(n=fRowCount);
  end;
end;


{ TSQLRestServerDB }

procedure TSQLRestServerDB.PrepareStatement(Cached: boolean);
var wasPrepared: boolean;
    timer: PPPrecisionTimer;
begin
  fStaticStatementTimer.Start;
  if not Cached then begin
    fStaticStatement.Prepare(DB.DB,fStatementGenericSQL);
    fStatementGenericSQL := '';
    fStatement := @fStaticStatement;
    fStatementTimer := @fStaticStatementTimer;
    fStatementMonitor := nil;
    exit;
  end;
  if mlSQLite3 in StatLevels then
    timer := @fStatementTimer else
    timer := nil;
  fStatement := fStatementCache.Prepare(fStatementGenericSQL,@wasPrepared,timer,@fStatementMonitor);
  if wasPrepared then begin
    InternalLog('prepared % % %', [fStaticStatementTimer.Stop,
      DB.FileNameWithoutPath,fStatementGenericSQL],sllDB);
    if fStatementPreparedSelectQueryPlan then
      DB.ExecuteJSON('explain query plan '+
        StringReplaceChars(fStatementGenericSQL,'?','1'), {expand=}true);
  end;
  if timer=nil then begin
    fStaticStatementTimer.Start;
    fStatementTimer := @fStaticStatementTimer;
    fStatementMonitor := nil;
  end;
end;

procedure TSQLRestServerDB.GetAndPrepareStatement(const SQL: RawUTF8;
  ForceCacheStatement: boolean);
var i, sqlite3param: integer;
    Types: TSQLParamTypeDynArray;
    Nulls: TSQLFieldBits;
    Values: TRawUTF8DynArray;
begin
  // prepare statement
  fStatementSQL := SQL;
  fStatementGenericSQL := ExtractInlineParameters(SQL,Types,Values,fStatementMaxParam,Nulls);
  PrepareStatement(ForceCacheStatement or (fStatementMaxParam<>0));
  // bind parameters
  if fStatementMaxParam=0 then
    exit; // no valid :(...): inlined parameter found -> manual bind
  sqlite3param := sqlite3.bind_parameter_count(fStatement^.Request);
  if sqlite3param<>fStatementMaxParam then
    raise EORMException.CreateUTF8(
      '%.GetAndPrepareStatement(%) recognized % params, and % for SQLite3',
      [self,fStatementGenericSQL,fStatementMaxParam,sqlite3param]);
  for i := 0 to fStatementMaxParam-1 do
    if i in Nulls then
      fStatement^.BindNull(i+1) else
      case Types[i] of
        sptDateTime, // date/time are stored as ISO-8601 TEXT in SQLite3
        sptText:    fStatement^.Bind(i+1,Values[i]);
        sptBlob:    fStatement^.BindBlob(i+1,Values[i]);
        sptInteger: fStatement^.Bind(i+1,GetInt64(pointer(Values[i])));
        sptFloat:   fStatement^.Bind(i+1,GetExtended(pointer(Values[i])));
      end;
end;

procedure TSQLRestServerDB.GetAndPrepareStatementRelease(E: Exception;
  const Msg: ShortString; ForceBindReset: boolean);
var
  tmp: TSynTempBuffer;
  P: PAnsiChar;
begin
  try
    if fStatementTimer<>nil then begin
      if fStatementMonitor<>nil then
        fStatementMonitor.ProcessEnd else
        fStatementTimer^.Pause;
      if E=nil then
        if (fStatementTruncateSQLLogLen > 0) and
           (length(fStatementSQL) > fStatementTruncateSQLLogLen) then begin
          tmp.Init(pointer(fStatementSQL),fStatementTruncateSQLLogLen);
          P := tmp.buf;
          PCardinal(P+fStatementTruncateSQLLogLen-3)^ := ord('.')+ord('.')shl 8+ord('.')shl 16;
          InternalLog('% % % len=%',[fStatementTimer^.LastTime,Msg,P,length(fStatementSQL)],sllSQL);
          tmp.Done;
        end else
          InternalLog('% % %',[fStatementTimer^.LastTime,Msg,fStatementSQL],sllSQL) else
        InternalLog('% for % // %',[E,fStatementSQL,fStatementGenericSQL],sllError);
      fStatementTimer := nil;
    end;
    fStatementMonitor := nil;
  finally
    if fStatement<>nil then begin
      if fStatement=@fStaticStatement then
        fStaticStatement.Close else
        if (fStatementMaxParam<>0) or ForceBindReset then
          fStatement^.BindReset; // release bound RawUTF8 ASAP
      fStatement := nil;
    end;
    fStatementSQL := '';
    fStatementGenericSQL := '';
    fStatementMaxParam := 0;
    if E<>nil then
      FormatUTF8('% %',[E,ObjectToJSONDebug(E)],fStatementLastException);
  end;
end;

procedure TSQLRestServerDB.FlushStatementCache;
begin
  DB.Lock;
  try
    fStatementCache.ReleaseAllDBStatements;
  finally
    DB.Unlock;
  end;
end;

function TSQLRestServerDB.TableMaxID(Table: TSQLRecordClass): TID;
var SQL: RawUTF8;
begin
  if StaticTable[Table]<>nil then
    result := inherited TableMaxID(Table) else begin
    SQL := 'select rowid from '+Table.SQLTableName+' order by rowid desc limit 1';
    if not InternalExecute(SQL,true,PInt64(@result)) then
      result := 0;
  end;
end;

function TSQLRestServerDB.MainEngineAdd(TableModelIndex: integer;
  const SentData: RawUTF8): TID;
var Props: TSQLRecordProperties;
    SQL: RawUTF8;
    Decoder: TJSONObjectDecoder;
begin
  result := 0;
  if TableModelIndex<0 then
    exit;
  Props := fModel.TableProps[TableModelIndex].Props;
  SQL := Props.SQLTableName;
  if fBatchMethod<>mNone then begin
    result := 0; // indicates error
    if SentData='' then
      InternalLog('BATCH with MainEngineAdd(%,SentData="") -> '+
        'DEFAULT VALUES not implemented',[SQL],sllError) else
    if (fBatchMethod=mPOST) and (fBatchIDMax>=0) and
       ((fBatchTableIndex<0) or (fBatchTableIndex=TableModelIndex)) then begin
      fBatchTableIndex := TableModelIndex;
      if JSONGetID(pointer(SentData),result) then begin
        if result>fBatchIDMax then
          fBatchIDMax := result;
      end else begin
        if fBatchIDMax=0 then begin
          fBatchIDMax := TableMaxID(Props.Table);
          if fBatchIDMax<0 then
            exit; // will force error for whole BATCH block
        end;
        inc(fBatchIDMax);
        result := fBatchIDMax;
      end;
      AddID(fBatchID,fBatchIDCount,result);
      AddRawUTF8(fBatchValues,fBatchValuesCount,SentData);
    end;
    exit;
  end;
  SQL := 'INSERT INTO '+SQL;
  if trim(SentData)='' then
    SQL := SQL+' DEFAULT VALUES;' else begin
    JSONGetID(pointer(SentData),result);
    Decoder.Decode(SentData,nil,pInlined,result,false);
    if Props.RecordVersionField<>nil then
      InternalRecordVersionHandle(
        soInsert,TableModelIndex,decoder,Props.RecordVersionField);
    SQL := SQL+Decoder.EncodeAsSQL(false)+';';
  end;
  if InternalExecute(SQL,true,nil,nil,nil,PInt64(@result)) then
    InternalUpdateEvent(seAdd,TableModelIndex,result,SentData,nil);
end;

procedure InternalRTreeIn(Context: TSQLite3FunctionContext;
  argc: integer; var argv: TSQLite3ValueArray); cdecl;
var aRTree: TSQLRecordRTreeClass;
    BlobA, BlobB: pointer;
begin
  if argc<>2 then begin
    ErrorWrongNumberOfArgs(Context);
    exit;
  end;
  aRTree := sqlite3.user_data(Context);
  BlobA := sqlite3.value_blob(argv[0]);
  BlobB := sqlite3.value_blob(argv[1]);
  if (aRTree=nil) or (BlobA=nil) or (BlobB=nil) then
    sqlite3.result_error(Context,'invalid call') else
    sqlite3.result_int64(Context,byte(aRTree.ContainedIn(BlobA^,BlobB^)));
end;

constructor TSQLRestServerDB.Create(aModel: TSQLModel;
  aHandleUserAuthentication: boolean);
begin
  Create(aModel,SQLITE_MEMORY_DATABASE_NAME,aHandleUserAuthentication);
end;

constructor TSQLRestServerDB.Create(aModel: TSQLModel; aDB: TSQLDataBase;
  aHandleUserAuthentication, aOwnDB: boolean);
begin
  fStatementCache.Init(aDB.DB);
  aDB.UseCache := true; // we better use caching in this JSON oriented use
  fDB := aDB;
  if aOwnDB then
    fOwnedDB := fDB;
  if fDB.InternalState=nil then begin // should be done once
    InternalState := 1;
    fDB.InternalState := @InternalState; // to update TSQLRestServerDB.InternalState
  end;
  inherited Create(aModel,aHandleUserAuthentication);
  InitializeEngine;
end;

{$ifdef WITHLOG}
procedure TSQLRestServerDB.SetLogClass(aClass: TSynLogClass);
begin
  inherited;
  if DB<>nil then
    DB.Log := aClass; // ensure low-level SQLite3 engine will share the same log
end;
{$endif}

procedure TSQLRestServerDB.InitializeEngine;
var i: integer;
    module: TSQLVirtualTableClass;
    registered: array of TSQLVirtualTableClass;
begin
  for i := 0 to high(Model.TableProps) do
    case Model.TableProps[i].Kind of
    rRTree, rRTreeInteger: // register all *_in() SQL functions
      sqlite3_check(DB.DB,sqlite3.create_function_v2(DB.DB,
        pointer(TSQLRecordRTreeClass(Model.Tables[i]).RTreeSQLFunctionName),
        2,SQLITE_ANY,Model.Tables[i],InternalRTreeIn,nil,nil,nil));
    rCustomForcedID, rCustomAutoID: begin
      module := Model.VirtualTableModule(Model.Tables[i]);
      if (module<>nil) and (PtrArrayFind(registered,module)<0) then begin
        TSQLVirtualTableModuleServerDB.Create(module,self);
        PtrArrayAdd(registered,module); // register it once for this DB
      end;
    end;
    end;
end;

constructor TSQLRestServerDB.Create(aModel: TSQLModel; const aDBFileName: TFileName;
  aHandleUserAuthentication: boolean; const aPassword: RawUTF8;
  aDefaultCacheSize, aDefaultPageSize: integer);
begin
  fOwnedDB := TSQLDataBase.Create(aDBFileName,aPassword,0,aDefaultCacheSize,aDefaultPageSize);
  // fOwnedDB.Free done in Destroy
  Create(aModel,fOwnedDB,aHandleUserAuthentication);
end;

constructor TSQLRestServerDB.CreateWithOwnModel(const aTables: array of TSQLRecordClass;
  const aDBFileName: TFileName; aHandleUserAuthentication: boolean;
  const aRoot, aPassword: RawUTF8; aDefaultCacheSize, aDefaultPageSize: integer);
begin
  Create(TSQLModel.Create(aTables,aRoot),aDBFileName,aHandleUserAuthentication,
    aPassword,aDefaultCacheSize,aDefaultPageSize);
  fModel.Owner := self;
end;

constructor TSQLRestServerDB.CreateWithOwnModel(const aTables: array of TSQLRecordClass;
  aHandleUserAuthentication: boolean);
begin
  Create(TSQLModel.Create(aTables),aHandleUserAuthentication);
  fModel.Owner := self;
end;

procedure TSQLRestServerDB.CreateMissingTables(user_version: cardinal;
  Options: TSQLInitializeTableOptions);
var t,f,nt,nf: integer;
    TableNamesAtCreation, aFields: TRawUTF8DynArray;
    TableJustCreated: TSQLFieldTables;
    aSQL: RawUTF8;
begin
  if DB.TransactionActive then
    raise EBusinessLayerException.Create('CreateMissingTables in transaction');
  fDB.GetTableNames(TableNamesAtCreation);
  nt := length(TableNamesAtCreation);
  QuickSortRawUTF8(TableNamesAtCreation,nt,nil,@StrIComp);
  {$ifdef WITHLOG}
  fLogFamily.SynLog.Log(sllDB,'CreateMissingTables on %',[fDB],self);
  fLogFamily.SynLog.Log(sllDB,'GetTables',TypeInfo(TRawUTF8DynArray),TableNamesAtCreation,self);
  {$endif}
  FillcharFast(TableJustCreated,sizeof(TSQLFieldTables),0);
  try
    // create not static and not existing tables
    for t := 0 to high(Model.Tables) do
      if ((fStaticData=nil) or (fStaticData[t]=nil)) then
      // this table is not static -> check if already existing, create if necessary
      with Model.TableProps[t], Props do
      if not NoCreateMissingTable then
      if FastFindPUTF8CharSorted(pointer(TableNamesAtCreation),nt-1,pointer(SQLTableName),@StrIComp)<0 then begin
        if not DB.TransactionActive then
          DB.TransactionBegin; // make initialization faster by using transaction
        DB.Execute(Model.GetSQLCreate(t)); // don't catch exception in constructor
        include(TableJustCreated,t);       // mark to be initialized below
      end else
      if not(itoNoCreateMissingField in Options) then begin
        // this table is existing: check that all fields exist -> create if necessary
        DB.GetFieldNames(aFields,SQLTableName);
        nf := length(aFields);
        QuickSortRawUTF8(aFields,nf,nil,@StrIComp);
        for f := 0 to Fields.Count-1 do
          with Fields.List[f] do
          if SQLFieldType in COPIABLE_FIELDS then
          /// real database columns exist for Simple + Blob fields (not Many)
          if FastFindPUTF8CharSorted(pointer(aFields),nf-1,pointer(Name),@StrIComp)<0 then begin
            aSQL := Model.GetSQLAddField(t,f);
            if aSQL<>'' then begin // need a true field with data
              if not DB.TransactionActive then
                DB.TransactionBegin; // make initialization faster by using transaction
              DB.Execute(aSQL);
            end;
            Model.Tables[t].InitializeTable(self,Name,Options);
          end;
      end;
    if not DB.TransactionActive then
      exit;
    // database schema was modified -> update user version in SQLite3 file
    if user_version<>0 then
      DB.user_version := user_version;
    // initialize new tables AFTER creation of ALL tables
    if not IsZero(@TableJustCreated,sizeof(TSQLFieldTables)) then
      for t := 0 to high(Model.Tables) do
        if t in TableJustCreated then
          if not(Model.TableProps[t].Kind in IS_CUSTOM_VIRTUAL) or
             not TableHasRows(Model.Tables[t]) then // check is really void
            Model.Tables[t].InitializeTable(self,'',Options); // '' for table creation
    DB.Commit;
  except
    on E: Exception do begin
      DB.RollBack; // will close any active Transaction
      raise;     // caller must handle exception
    end;
  end;
end;

function TSQLRestServerDB.MainEngineDelete(TableModelIndex: integer; ID: TID): boolean;
begin
  if (TableModelIndex<0) or (ID<=0) then
    result := false else begin
    // notify BEFORE deletion
    InternalUpdateEvent(seDelete,TableModelIndex,ID,'',nil);
    result := ExecuteFmt('DELETE FROM % WHERE RowID=:(%):;',
      [fModel.TableProps[TableModelIndex].Props.SQLTableName,ID]);
  end;
end;

function TSQLRestServerDB.MainEngineDeleteWhere(TableModelIndex: Integer;
  const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean;
var i: integer;
    aSQLWhere: RawUTF8;
begin
  if (TableModelIndex<0) or (IDs=nil) then
    result := false else begin
    // notify BEFORE deletion
    for i := 0 to high(IDs) do
      InternalUpdateEvent(seDelete,TableModelIndex,IDs[i],'',nil);
    if IdemPChar(pointer(SQLWhere),'LIMIT ') or
       IdemPChar(pointer(SQLWhere),'ORDER BY ') then
      // LIMIT is not handled by SQLite3 when built from amalgamation
      // see http://www.sqlite.org/compile.html#enable_update_delete_limit
      aSQLWhere := Int64DynArrayToCSV(pointer(IDs),length(IDs),'RowID IN (',')') else
      aSQLWhere := SQLWhere;
    result := ExecuteFmt('DELETE FROM %%',
      [fModel.TableProps[TableModelIndex].Props.SQLTableName,SQLFromWhere(aSQLWhere)]);
  end;
end;

destructor TSQLRestServerDB.Destroy;
begin
  {$ifdef WITHLOG}
  with fLogClass.Enter('Destroy %', [fModel.SafeRoot], self) do
  {$endif}
  try
    if (fDB<>nil) and (fDB.InternalState=@InternalState) then
      fDB.InternalState := nil; // avoid memory modification on free block 
    inherited Destroy;
  finally
    try
      fStatementCache.ReleaseAllDBStatements;
    finally
      fOwnedDB.Free; // do nothing if DB<>fOwnedDB
    end;
  end;
end;

procedure TSQLRestServerDB.DefinitionTo(Definition: TSynConnectionDefinition);
begin
  if Definition=nil then
    exit;
  inherited; // set Kind
  if fDB<>nil then begin
    Definition.ServerName := StringToUTF8(fDB.FileName);
    Definition.PasswordPlain := fDB.Password;
  end;
end;

constructor TSQLRestServerDB.RegisteredClassCreateFrom(aModel: TSQLModel;
  aServerHandleAuthentication: boolean; aDefinition: TSynConnectionDefinition);
begin
  Create(aModel,UTF8ToString(aDefinition.ServerName),aServerHandleAuthentication,
    aDefinition.PasswordPlain);
end;

function TSQLRestServerDB.PrepareVacuum(const aSQL: RawUTF8): boolean;
begin
  result := not IdemPChar(Pointer(aSQL),'VACUUM');
  if result then
    exit;
  result :=  (fStaticVirtualTable=nil) or
    IsZero(fStaticVirtualTable,length(fStaticVirtualTable)*sizeof(pointer));
  if result then
    // VACUUM will fail if there are one or more active SQL statements
    fStatementCache.ReleaseAllDBStatements;
end;

function TSQLRestServerDB.InternalExecute(const aSQL: RawUTF8;
  ForceCacheStatement: boolean; ValueInt: PInt64; ValueUTF8: PRawUTF8;
  ValueInts: PInt64DynArray; LastInsertedID: PInt64; LastChangeCount: PInteger): boolean;
var ValueIntsCount, Res: Integer;
    msg: shortstring;
begin
  msg := '';
  if (self<>nil) and (DB<>nil) then
  try
    DB.Lock(aSQL);
    try
      result := true;
      if not PrepareVacuum(aSQL) then
        exit; // no-op if there are some static virtual tables around
      try
        GetAndPrepareStatement(aSQL,ForceCacheStatement);
        if ValueInts<>nil then begin
          ValueIntsCount := 0;
          repeat
            res := fStatement^.Step;
            if res=SQLITE_ROW then
              AddInt64(ValueInts^,ValueIntsCount,fStatement^.FieldInt(0));
          until res=SQLITE_DONE;
          SetLength(ValueInts^,ValueIntsCount);
          FormatShort('returned Int64 len=%',[ValueIntsCount],msg);
        end else
        if (ValueInt=nil) and (ValueUTF8=nil) then begin
          // default execution: loop through all rows
          repeat until fStatement^.Step<>SQLITE_ROW;
          if LastInsertedID<>nil then begin
            LastInsertedID^ := DB.LastInsertRowID;
            FormatShort(' lastInsertedID=%',[LastInsertedID^],msg);
          end;
          if LastChangeCount<>nil then begin
            LastChangeCount^ := DB.LastChangeCount;
            FormatShort(' lastChangeCount=%',[LastChangeCount^],msg);
          end;
        end else
          // get one row, and retrieve value
          if fStatement^.Step<>SQLITE_ROW then
            result := false else
            if ValueInt<>nil then begin
              ValueInt^ := fStatement^.FieldInt(0);
              FormatShort('returned=%',[ValueInt^],msg);
            end else begin
              ValueUTF8^ := fStatement^.FieldUTF8(0);
              FormatShort('returned="%"',[ValueUTF8^],msg);
            end;
        GetAndPrepareStatementRelease(nil,msg);
      except
        on E: Exception do begin
          GetAndPrepareStatementRelease(E);
          result := false;
        end;
      end;
    finally
      DB.UnLock;
    end;
  except
    on E: ESQLite3Exception do begin
      {$ifdef WITHLOG}
      InternalLog('% for % // %',[E,aSQL,fStatementGenericSQL],sllError);
      {$else}
      LogToTextFile('TSQLRestServerDB.InternalExecute '+RawUTF8(E.Message)+#13#10+aSQL);
      {$endif}
      result := false;
    end;
  end else
    result := false;
end;

function TSQLRestServerDB.StoredProcExecute(const aSQL: RawUTF8;
  StoredProc: TOnSQLStoredProc): boolean;
var R: TSQLRequest; // we don't use fStatementCache[] here
    Res: integer;
begin
  result := false;
  if (self<>nil) and (DB<>nil) and (aSQL<>'') and Assigned(StoredProc) then
  try
    {$ifdef WITHLOG}
    fLogFamily.SynLog.Enter('StoredProcExecute(%)', [aSQL], self);
    {$endif}
    DB.LockAndFlushCache; // even if aSQL is SELECT, StoredProc may update data
    try
      try
        R.Prepare(DB.DB,aSQL);
        if R.FieldCount>0 then
        repeat
          res := R.Step;
          if res=SQLITE_ROW then
            StoredProc(R); // apply the stored procedure to all rows
        until res=SQLITE_DONE;
        result := true;
      finally
        R.Close; // always release statement
      end;
    finally
      DB.UnLock;
    end;
  except
    on E: ESQLite3Exception do begin
      {$ifdef WITHLOG}
      fLogFamily.SynLog.Log(sllError,'% for %',[E,aSQL],self);
      {$else}
      LogToTextFile(ClassName+'.StoredProcExecute Error: '+RawUTF8(E.Message)+#13#10+aSQL);
      {$endif}
      result := false;
    end;
  end;
end;

function TSQLRestServerDB.EngineExecute(const aSQL: RawUTF8): boolean;
begin
  result := InternalExecute(aSQL,{forcecache=}false);
end;

procedure TSQLRestServerDB.InternalInfo(var info: TDocVariantData);
begin
  inherited InternalInfo(info);
  info.AddValue('db', FormatString('% %', [ExtractFileName(DB.FileName), KB(DB.FileSize)]));
end;

procedure TSQLRestServerDB.InternalStat(Ctxt: TSQLRestServerURIContext; W: TTextWriter);
var i: integer;
    ndx: TIntegerDynArray;
begin
  inherited InternalStat(Ctxt,W);
  if Ctxt.InputExists['withall'] or Ctxt.InputExists['withsqlite3'] then begin
    W.CancelLastChar('}');
    W.AddShort(',"sqlite3":[');
    DB.Lock;
    try
      fStatementCache.SortCacheByTotalTime(ndx);
      with fStatementCache do
      for i := 0 to Count-1 do
        with Cache[ndx[i]] do begin
          W.AddJSONEscape([StatementSQL,Timer]);
          W.Add(',');
        end;
    finally
      DB.UnLock;
    end;
    W.CancelLastComma;
    W.Add(']','}');
  end;
end;

procedure TSQLRestServerDB.ComputeDBStats(out result: variant);
var i: integer;
    ndx: TIntegerDynArray;
    doc: TDocVariantData absolute result;
begin
  if self=nil then
    exit;
  doc.Init(JSON_OPTIONS_FAST_EXTENDED,dvObject);
  DB.Lock;
  try
    fStatementCache.SortCacheByTotalTime(ndx);
    with fStatementCache do
    for i := 0 to Count-1 do
      with Cache[ndx[i]] do
        doc.AddValue(StatementSQL,Timer.ComputeDetails);
  finally
    DB.UnLock;
  end;
end;

function TSQLRestServerDB.ComputeDBStats: variant;
begin
  ComputeDBStats(result);
end;

function TSQLRestServerDB.MainEngineList(const SQL: RawUTF8; ForceAJAX: Boolean;
  ReturnedRowCount: PPtrInt): RawUTF8;
var MS: TRawByteStringStream;
    RowCount: integer;
begin
  result := '';
  RowCount := 0;
  if (self<>nil) and (DB<>nil) and (SQL<>'') then begin
    // need a SQL request for R.Execute() to prepare a statement
    result := DB.LockJSON(SQL,ReturnedRowCount); // lock and try from cache
    if result<>'' then
      exit;
    try // Execute request if was not got from cache
      try
        GetAndPrepareStatement(SQL,{forcecache=}false);
        MS := TRawByteStringStream.Create;
        try
          RowCount := fStatement^.Execute(0,'',MS,ForceAJAX or not NoAJAXJSON);
          result := MS.DataString;
        finally
          MS.Free;
        end;
        GetAndPrepareStatementRelease(nil, FormatToShort('returned % as %',
          [Plural('row',RowCount),KB(result)]));
      except
        on E: ESQLite3Exception do
          GetAndPrepareStatementRelease(E);
      end;
    finally
      DB.UnLockJSON(result,RowCount);
    end;
  end;
  if ReturnedRowCount<>nil then
    ReturnedRowCount^ := RowCount;
end;

function TSQLRestServerDB.MainEngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8;
var aSQL: RawUTF8;
begin
  result := '';
  if (ID<0) or (TableModelIndex<0) then
    exit;
  with Model.TableProps[TableModelIndex] do
    FormatUTF8('SELECT % FROM % WHERE RowID=:(%):;',
      [SQL.TableSimpleFields[true,false],Props.SQLTableName,ID],aSQL);
  result := EngineList(aSQL,true); // ForceAJAX=true -> '[{...}]'#10
  if result<>'' then
    if IsNotAjaxJSON(pointer(result)) then
      // '{"fieldCount":2,"values":["ID","FirstName"]}'#$A -> ID not found
      result := '' else
      // list '[{...}]'#10 -> object '{...}'
      result := copy(result,2,length(result)-3);
end;

function TSQLRestServerDB.MainEngineRetrieveBlob(TableModelIndex: integer; aID: TID;
  BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean;
var SQL: RawUTF8;
begin
  result := false;
  if (aID<0) or (TableModelIndex<0) or not BlobField^.IsBlob then
    exit;
  // retrieve the BLOB using SQL
  try
    SQL := FormatUTF8('SELECT % FROM % WHERE RowID=?',
      [BlobField^.Name,Model.TableProps[TableModelIndex].Props.SQLTableName],[aID]);
    DB.Lock(SQL); // UPDATE for a blob field -> no JSON cache flush, but UI refresh
    try
      GetAndPrepareStatement(SQL,true);
      try
        if (fStatement^.FieldCount=1) and (fStatement^.Step=SQLITE_ROW) then begin
          BlobData := fStatement^.FieldBlob(0);
          result := true;
        end;
        GetAndPrepareStatementRelease(nil,KB(BlobData));
      except
        on E: Exception do
          GetAndPrepareStatementRelease(E);
      end;
    finally
      DB.UnLock;
    end;
  except
    on ESQLite3Exception do
      result := false;
  end;
end;

function TSQLRestServerDB.RetrieveBlobFields(Value: TSQLRecord): boolean;
var Static: TSQLRest;
    SQL: RawUTF8;
    f: PtrInt;
    size: Int64;
    data: TSQLVar;
begin
  result := false;
  if Value=nil then
    exit;
  Static := GetStaticTable(PSQLRecordClass(Value)^);
  if Static<>nil then
    result := Static.RetrieveBlobFields(Value) else
    if (DB<>nil) and (Value.ID>0) and (PSQLRecordClass(Value)^<>nil) then
    with Value.RecordProps do
    if BlobFields<>nil then begin
      SQL := FormatUTF8('SELECT % FROM % WHERE ROWID=?',
        [SQLTableRetrieveBlobFields,SQLTableName],[Value.ID]);
      DB.Lock(SQL);
      try
        GetAndPrepareStatement(SQL,true);
        try
          if fStatement^.Step<>SQLITE_ROW then
            exit;
          size := 0;
          for f := 0 to high(BlobFields) do begin
            SQlite3ValueToSQLVar(fStatement^.FieldValue(f),data);
            BlobFields[f].SetFieldSQLVar(Value,data); // OK for all blobs
            inc(size,SQLVarLength(data));
          end;
          GetAndPrepareStatementRelease(nil,KB(size));
          result := true;
        except
          on E: Exception do
            GetAndPrepareStatementRelease(E);
        end;
      finally
        DB.UnLock;
      end;
    end;
end;

procedure TSQLRestServerDB.SetNoAJAXJSON(const Value: boolean);
begin
  inherited;
  if Value=NoAJAXJSON then
     exit;
  fDB.Cache.Reset; // we changed the JSON format -> cache must be updated
end;

function TSQLRestServerDB.MainEngineUpdate(TableModelIndex: integer; ID: TID;
  const SentData: RawUTF8): boolean;
var Props: TSQLRecordProperties;
    Decoder: TJSONObjectDecoder;
    SQL: RawUTF8;
begin
  if (TableModelIndex<0) or (ID<=0) then
    result := false else
  if SentData='' then // update with no simple field -> valid no-op
    result := true else begin
    // this SQL statement use :(inlined params): for all values
    Props := fModel.TableProps[TableModelIndex].Props;
    Decoder.Decode(SentData,nil,pInlined,ID,false);
    if Props.RecordVersionField<>nil then
      InternalRecordVersionHandle(
        soUpdate,TableModelIndex,decoder,Props.RecordVersionField);
    SQL := Decoder.EncodeAsSQL(true);
    result := ExecuteFmt('UPDATE % SET % WHERE RowID=:(%):',
      [Props.SQLTableName,SQL,ID]);
    InternalUpdateEvent(seUpdate,TableModelIndex,ID,SentData,nil);
  end;
end;

function TSQLRestServerDB.MainEngineUpdateBlob(TableModelIndex: integer; aID: TID;
  BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean;
var SQL: RawUTF8;
    AffectedField: TSQLFieldBits;
    Props: TSQLRecordProperties;
begin
  result := false;
  if (aID<0) or (TableModelIndex<0) or not BlobField^.IsBlob then
     exit;
  Props := Model.TableProps[TableModelIndex].Props;
  try
    FormatUTF8('UPDATE % SET %=? WHERE RowID=?',[Props.SQLTableName,BlobField^.Name],SQL);
    DB.Lock(SQL); // UPDATE for a blob field -> no JSON cache flush, but UI refresh
    try
      GetAndPrepareStatement(SQL,true);
      try
        if BlobData='' then
          fStatement^.BindNull(1) else
          fStatement^.BindBlob(1,BlobData);
        fStatement^.Bind(2,aID);
        repeat until fStatement^.Step<>SQLITE_ROW; // Execute
        GetAndPrepareStatementRelease(nil,FormatToShort('stored % in ID=%',
          [KB(BlobData),aID]),true);
        result := true;
      except
        on E: Exception do
          GetAndPrepareStatementRelease(E);
      end;
    finally
      DB.UnLock;
    end;
    Props.FieldBitsFromBlobField(BlobField,AffectedField);
    InternalUpdateEvent(seUpdateBlob,TableModelIndex,aID,'',@AffectedField);
  except
    on ESQLite3Exception do
      result := false;
  end;
end;

function TSQLRestServerDB.MainEngineUpdateFieldIncrement(TableModelIndex: integer;
  ID: TID; const FieldName: RawUTF8; Increment: Int64): boolean;
var Props: TSQLRecordProperties;
    Value: Int64;
begin
  result := false;
  if (TableModelIndex<0) or (FieldName='') then
    exit;
  Props := Model.TableProps[TableModelIndex].Props;
  if Props.Fields.IndexByName(FieldName)<0 then
    Exit;
  if InternalUpdateEventNeeded(TableModelIndex) or
     (Props.RecordVersionField<>nil) then
    result := OneFieldValue(Props.Table,FieldName,'ID=?',[],[ID],Value) and
              UpdateField(Props.Table,ID,FieldName,[Value+Increment]) else
    result := RecordCanBeUpdated(Props.Table,ID,seUpdate) and
      ExecuteFmt('UPDATE % SET %=%+:(%): WHERE ID=:(%):',
        [Props.SQLTableName,FieldName,FieldName,Increment,ID]);
end;

function TSQLRestServerDB.MainEngineUpdateField(TableModelIndex: integer;
  const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean;
var Props: TSQLRecordProperties;
    WhereID,RecordVersion: TID;
    i: integer;
    JSON,IDs: RawUTF8;
    ID: TIDDynArray;
begin
  result := false;
  if (TableModelIndex<0) or (SetFieldName='') then
    exit;
  Props := Model.TableProps[TableModelIndex].Props;
  if Props.Fields.IndexByName(SetFieldName)<0 then
    Exit;
  if IsRowID(pointer(WhereFieldName)) then begin
    WhereID := GetInt64(Pointer(WhereValue));
    if WhereID<=0 then
      exit;
  end else
    if Props.Fields.IndexByName(WhereFieldName)<0 then
      exit else
      WhereID := 0;
  if InternalUpdateEventNeeded(TableModelIndex) or
     (Props.RecordVersionField<>nil) then begin
    if WhereID>0 then begin
      SetLength(ID,1);
      ID[0] := WhereID;
    end else
      if not InternalExecute(FormatUTF8('select RowID from % where %=:(%):',
         [Props.SQLTableName,WhereFieldName,WhereValue]),true,nil,nil,@ID) then
        exit else
        if ID=nil then begin
          result := true; // nothing to update, but return success
          exit;
        end;
    for i := 0 to high(ID) do
      if not RecordCanBeUpdated(Props.Table,ID[i],seUpdate) then
        exit;
    if Length(ID)=1 then
      if Props.RecordVersionField=nil then
        result := ExecuteFmt('UPDATE % SET %=:(%): WHERE RowID=:(%):',
          [Props.SQLTableName,SetFieldName,SetValue,ID[0]]) else
        result := ExecuteFmt('UPDATE % SET %=:(%):,%=:(%): WHERE RowID=:(%):',
          [Props.SQLTableName,SetFieldName,SetValue,
           Props.RecordVersionField.Name,RecordVersionCompute,ID[0]]) else begin
      IDs := Int64DynArrayToCSV(pointer(ID),length(ID));
      if Props.RecordVersionField=nil then
        result := ExecuteFmt('UPDATE % SET %=% WHERE RowID IN (%)',
          [Props.SQLTableName,SetFieldName,SetValue,IDs]) else begin
        RecordVersion := RecordVersionCompute;
        result := ExecuteFmt('UPDATE % SET %=%,%=% WHERE RowID IN (%)',
          [Props.SQLTableName,SetFieldName,SetValue,
           Props.RecordVersionField.Name,RecordVersion,IDs]);
      end;
    end;
    if not result then
      exit;
    JSONEncodeNameSQLValue(SetFieldName,SetValue,JSON);
    for i := 0 to high(ID) do
      InternalUpdateEvent(seUpdate,TableModelIndex,ID[i],JSON,nil);
  end else
    if (WhereID>0) and not RecordCanBeUpdated(Props.Table,WhereID,seUpdate) then
      exit else // limitation: will only check for update when RowID is provided
      result := ExecuteFmt('UPDATE % SET %=:(%): WHERE %=:(%):',
        [Props.SQLTableName,SetFieldName,SetValue,WhereFieldName,WhereValue]);
end;

function TSQLRestServerDB.UpdateBlobFields(Value: TSQLRecord): boolean;
var Static: TSQLRest;
    SQL: RawUTF8;
    TableModelIndex,f: integer;
    data: TSQLVar;
    size: Int64;
    temp: RawByteString;
begin
  result := false;
  if Value=nil then
    exit;
  TableModelIndex := Model.GetTableIndexExisting(PSQLRecordClass(Value)^);
  Static := GetStaticTableIndex(TableModelIndex);
  if Static<>nil then
    result := Static.UpdateBlobFields(Value) else
    if (DB<>nil) and (Value.ID>0) and (PSQLRecordClass(Value)^<>nil) then
    with Model.TableProps[TableModelIndex].Props do
    if BlobFields<>nil then begin
      FormatUTF8('UPDATE % SET % WHERE ROWID=?',[SQLTableName,SQLTableUpdateBlobFields],SQL);
      DB.Lock(SQL); // UPDATE for all blob fields -> no cache flush, but UI refresh
      try
        GetAndPrepareStatement(SQL,true);
        try
          size := 0;
          for f := 1 to length(BlobFields) do begin
            BlobFields[f-1].GetFieldSQLVar(Value,data,temp); // OK for all blobs
            if data.VType=ftBlob then begin
              fStatement^.Bind(f,data.VBlob,data.VBlobLen);
              inc(size,data.VBlobLen);
            end else
              fStatement^.BindNull(f); // e.g. Value was ''
          end;
          fStatement^.Bind(length(BlobFields)+1,Value.ID);
          repeat until fStatement^.Step<>SQLITE_ROW; // Execute
          GetAndPrepareStatementRelease(nil,FormatToShort('stored % in ID=%',
            [KB(size),Value.ID]),true);
          result := true;
        except
          on E: Exception do
            GetAndPrepareStatementRelease(E);
        end;
      finally
        DB.UnLock;
      end;
      InternalUpdateEvent(seUpdateBlob,TableModelIndex,Value.ID,'',@FieldBits[sftBlob]);
    end else
      result := true; // as TSQLRest.UpdateblobFields()
end;

procedure TSQLRestServerDB.Commit(SessionID: cardinal; RaiseException: boolean);
begin
  inherited Commit(SessionID,RaiseException);
  // reset fTransactionActive + write all TSQLVirtualTableJSON
  try
    DB.Commit; // will call DB.Lock
  except
    on Exception do
      if RaiseException then
        raise;  // default RaiseException=false will just ignore the exception
  end;
end;

procedure TSQLRestServerDB.RollBack(SessionID: cardinal);
begin
  inherited RollBack(SessionID); // reset TSQLRestServerDB.fTransactionActive flag
  try
    DB.RollBack; // will call DB.Lock
  except
    on ESQLite3Exception do
      ; // just catch exception
  end;
end;

function TSQLRestServerDB.TransactionBegin(aTable: TSQLRecordClass; SessionID: cardinal): boolean;
begin
  result := not DB.TransactionActive and inherited TransactionBegin(aTable,SessionID);
  if not result then
    exit; // fTransactionActive flag was already set
  try
    DB.TransactionBegin; // will call DB.Lock
  except
    on ESQLite3Exception do
      result := false;
  end;
end;

{$ifndef KYLIX3}

function TSQLRestServerDB.Backup(Dest: TStream): boolean;
{$ifndef WITHUNSAFEBACKUP} // deprecated - use DB.BackupBackground() instead
begin
  result := false;
end;
{$else}
var Source: TFileStream;
    Closed: boolean;
    user_version: cardinal;
begin
  result := false;
  if (Self=nil) or (DB=nil) then
    exit;
  user_version := DB.user_version;
  DB.LockAndFlushCache;
  try
    try
      fStatementCache.ReleaseAllDBStatements;
      // perform a VACCUM to recreate the database content
      EngineExecute('VACUUM');
      Closed := false;
      try
        Closed := DB.DBClose=SQLITE_OK;
        // compress the database content file
        Source := FileStreamSequentialRead(DB.FileName);
        try
          Dest.CopyFrom(Source,0);  // Count=0 for whole stream copy
          result := true;
        finally
          Source.Free;
        end;
      finally
        if Closed then begin
          // reopen the database if was previously closed
          DB.DBOpen;
          // register functions and modules
          InitializeEngine;
          // register virtual tables
          CreateMissingTables(user_version,fCreateMissingTablesOptions);
        end;
      end;
    finally
      DB.UnLock;
    end;
  except
    on E: Exception do
      result := false;
  end;
end;
{$endif}

function TSQLRestServerDB.BackupGZ(const DestFileName: TFileName;
  CompressionLevel: integer): boolean;
{$ifndef WITHUNSAFEBACKUP} // deprecated - use DB.BackupBackground() instead
begin
  result := false;
end;
{$else}
var D,Z: TStream;
begin
  try
    D := TFileStream.Create(DestFileName,fmCreate);
    try
      Z := TSynZipCompressor.Create(D,CompressionLevel,szcfGZ);
      try
        {$WARN SYMBOL_DEPRECATED OFF} // BackupGZ() itself is marked deprecated
        result := Backup(Z);
        {$WARN SYMBOL_DEPRECATED ON}
      finally
        Z.Free;
      end;
    finally
      D.Free;
    end;
  except
    result := false;
  end;
end;
{$endif}

{$endif KYLIX3}

function TSQLRestServerDB.RestoreGZ(const BackupFileName: TFileName): boolean;
{$ifndef WITHUNSAFEBACKUP} // deprecated - use DB.BackupBackground() instead
begin
  result := false;
end;
{$else}
begin
  try
    with TSynMemoryStreamMapped.Create(BackupFileName) do
    try
      result := Restore(GZRead(Memory,Size));
    finally
      Free;
    end;
  except
    on Exception do
      result := false;
  end;
end;
{$endif}

function TSQLRestServerDB.Restore(const ContentToRestore: RawByteString): boolean;
{$ifndef WITHUNSAFEBACKUP} // deprecated - use DB.BackupBackground() instead
begin
  result := false;
end;
{$else}
var BackupFileName: TFileName;
    user_version: cardinal;
begin
  result := false;
  if (Self=nil) or (DB=nil) or
     not IdemPChar(pointer(ContentToRestore),'SQLITE FORMAT 3') then
    exit; // invalid restore content
  user_version := DB.user_version;
  DB.LockAndFlushCache;
  try
    try
      fStatementCache.ReleaseAllDBStatements;
      if DB.DBClose<>SQLITE_OK then
        exit; // impossible to close DB.FileName (some statement may be opened)
      BackupFileName := ChangeFileExt(DB.FileName,'.bak');
      DeleteFile(BackupFileName);
      try
        {$ifdef MSWINDOWS}
        if MoveFile(pointer(DB.FileName),pointer(BackupFileName)) then
        {$else}
        if RenameFile(DB.FileName,BackupFileName) then
        {$endif}
          if FileFromString(ContentToRestore,DB.FileName,true) and
             (StringFromFile(DB.FileName)=ContentToRestore) then
            result := (DB.DBOpen=SQLITE_OK);
      finally
        if result then
          DeleteFile(BackupFileName) else begin
          // on error, restore previous db file
          DeleteFile(DB.FileName);
          {$ifdef MSWINDOWS}
          MoveFile(pointer(BackupFileName),pointer(DB.FileName));
          {$else}
          RenameFile(BackupFileName,DB.FileName);
          {$endif}
          DB.DBOpen; // always reopen the database
        end;
        // register functions and modules
        InitializeEngine;
        // register virtual tables
        CreateMissingTables(user_version,fCreateMissingTablesOptions);
      end;
    finally
      DB.UnLock;
    end;
  except
    on E: Exception do
      result := false;
  end;
end;
{$endif}

procedure TSQLRestServerDB.AdministrationExecute(const DatabaseName,SQL: RawUTF8;
  var result: TServiceCustomAnswer);
var new,cmd,fn: RawUTF8;
    bfn: TFileName;
    compress: boolean;
    i: integer;
begin
  inherited AdministrationExecute(DatabaseName,SQL,result);
  if (SQL<>'') and (SQL[1]='#') then begin
    case IdemPCharArray(@SQL[2],['VERSION','HELP','DB','BACKUP']) of
    0: new := FormatUTF8('"sqlite3":?}',[],[sqlite3.Version],true);
    1: begin
      result.Content[length(result.Content)] := '|';
      result.Content := result.Content+'#db [*/filename]|#backup [filename]"';
    end;
    2: begin
      split(SQL,' ',cmd,fn);
      if fn='' then begin
        result.Content := ObjectToJSON(DB,[woFullExpand]);
        if fStaticData<>nil then begin
          for i := 0 to high(fStaticData) do
            if fStaticData[i]<>nil then
              new := new+ObjectToJSON(fStaticData[i],[woFullExpand])+',';
          if new<>'' then begin
            new[length(new)] := ']';
            new := '"StaticTables":['+new+'}';
          end;
        end;
      end else
        AdministrationExecuteGetFiles(ExtractFilePath(DB.FileName),
          '*.db;*.db3;*.dbs',fn,result); // *.dbs includes *.dbsynlz
    end;
    3: begin
      split(SQL,' ',cmd,fn);
      if fn='' then
        FormatUTF8('% %',[NowToString(false),ChangeFileExt(DB.FileNameWithoutPath,'.dbsynlz')],fn);
      if (fn<>' ') and (PosEx('..',fn)=0) then begin
        bfn := UTF8ToString(fn);
        if ExtractFilePath(bfn)='' then // put in local data folder if not set
          bfn := ExtractFilePath(DB.FileName)+bfn;
        compress := GetFileNameExtIndex(bfn,'dbsynlz')=0;
        if DB.BackupBackground(bfn,4*1024,1,nil,compress) then // 4*1024*4096=16MB step
          result.Content := JsonEncode(['started',bfn,'compress',compress]) else
          result.Content := '"Backup failed to start"';
      end;
    end;
    else exit;
    end;
    if new<>'' then begin
      if result.Content='' then
        result.Content := '{' else
        result.Content[length(result.Content)] := ',';
      result.Content := result.Content+new;
    end;
  end;
end;

procedure TSQLRestServerDB.FlushInternalDBCache;
begin
  inherited;
  if DB=nil then
    exit;
  DB.Lock;
  try
    DB.CacheFlush;
  finally
    DB.UnLock;
  end;
end;

function TSQLRestServerDB.InternalBatchStart(
  Method: TSQLURIMethod; BatchOptions: TSQLRestBatchOptions): boolean;
begin
  result := false; // means BATCH mode not supported
  if method=mPOST then begin // POST=ADD=INSERT -> MainEngineAdd() to fBatchValues[]
    if (fBatchMethod<>mNone) or (fBatchValuesCount<>0) or (fBatchIDCount<>0) then
      raise EORMBatchException.CreateUTF8('%.InternalBatchStop should have been called',[self]);
    fBatchMethod := method;
    fBatchOptions := BatchOptions;
    fBatchTableIndex := -1;
    fBatchIDMax := 0; // MainEngineAdd() will search for max(id)
    result := true; // means BATCH mode is supported
  end;
end;

procedure TSQLRestServerDB.InternalBatchStop;
const MAX_PARAMS = 500; // pragmatic value (theoritical limit is 999)
var ndx,f,r,prop,fieldCount,valuesCount,
    rowCount,valuesFirstRow: integer;
    P: PUTF8Char;
    DecodeSaved,UpdateEventNeeded: boolean;
    Fields, Values: TRawUTF8DynArray;
    ValuesNull: TByteDynArray;
    Types: TSQLDBFieldTypeDynArray;
    SQL: RawUTF8;
    Props: TSQLRecordProperties;
    Decode: TJSONObjectDecoder;
    tmp: TSynTempBuffer;
begin
  if (fBatchValuesCount=0) or (fBatchTableIndex<0) then
    exit; // nothing to add
  if fBatchMethod<>mPOST then
    raise EORMBatchException.CreateUTF8('%.InternalBatchStop: BatchMethod=%',
      [self,ToText(fBatchMethod)^]);
  try
    if fBatchValuesCount<>fBatchIDCount then
      raise EORMBatchException.CreateUTF8('%.InternalBatchStop(*Count?)',[self]);
    UpdateEventNeeded := InternalUpdateEventNeeded(fBatchTableIndex);
    Props := fModel.Tables[fBatchTableIndex].RecordProps;
    if fBatchValuesCount=1 then begin // handle single record insert
      Decode.Decode(fBatchValues[0],nil,pInlined,fBatchID[0]);
      if Props.RecordVersionField<>nil then
        InternalRecordVersionHandle(
          soInsert,fBatchTableIndex,Decode,Props.RecordVersionField);
      SQL := 'INSERT INTO '+Props.SQLTableName+Decode.EncodeAsSQL(False)+';';
      if not InternalExecute(SQL,true) then // just like ESQLite3Exception below
        raise EORMBatchException.CreateUTF8('%.InternalBatchStop failed on %', [self, SQL]);
      if UpdateEventNeeded then
        InternalUpdateEvent(seAdd,fBatchTableIndex,fBatchID[0],fBatchValues[0],nil);
      exit;
    end;
    DecodeSaved := true;
    valuesCount := 0;
    rowCount := 0;
    valuesFirstRow := 0;
    SetLength(ValuesNull,(MAX_PARAMS shr 3)+1);
    SetLength(Values,32);
    Fields := nil; // makes compiler happy
    fieldCount := 0;
    ndx := 0;
    repeat
      repeat
        // decode a row
        if DecodeSaved then
        try
          if UpdateEventNeeded then begin
            tmp.Init(fBatchValues[ndx]);
            P := tmp.buf;
          end else
            P := pointer(fBatchValues[ndx]);
          if P=nil then
            raise EORMBatchException.CreateUTF8(
              '%.InternalBatchStop: fBatchValues[%]=""',[self,ndx]);
          while P^ in [#1..' ','{','['] do inc(P);
          Decode.Decode(P,nil,pNonQuoted,fBatchID[ndx]);
          if Props.RecordVersionField<>nil then
            InternalRecordVersionHandle(
              soInsert,fBatchTableIndex,Decode,Props.RecordVersionField);
          inc(ndx);
          DecodeSaved := false;
        finally
          if UpdateEventNeeded then
            tmp.Done;
        end;
        if Fields=nil then begin
          Decode.AssignFieldNamesTo(Fields);
          fieldCount := Decode.FieldCount;
          SQL := Decode.EncodeAsSQLPrepared(Props.SQLTableName,soInsert,'',fBatchOptions);
          SetLength(Types,fieldCount);
          for f := 0 to fieldCount-1 do begin
            prop := Props.Fields.IndexByNameOrExcept(Decode.FieldNames[f]);
            if prop<0 then // RowID
              Types[f] := ftInt64 else
              Types[f] := Props.Fields.List[prop].SQLDBFieldType;
          end;
        end else
          if not Decode.SameFieldNames(Fields) then
            break else // this item would break the SQL statement
          if valuesCount+fieldCount>MAX_PARAMS then
            break; // this item would bound too many params
        // if we reached here, we can add this row to Values[]
        if valuesCount+fieldCount>length(Values) then
          SetLength(Values,MAX_PARAMS);
        for f := 0 to fieldCount-1 do
          if Decode.FieldTypeApproximation[f]=ftaNull then
            SetBitPtr(pointer(ValuesNull),valuesCount+f) else
            Values[valuesCount+f] := Decode.FieldValues[f];
        inc(ValuesCount,fieldCount);
        inc(rowCount);
        DecodeSaved := true;
      until ndx=fBatchValuesCount;
      // INSERT Values[] into the DB
      DB.LockAndFlushCache;
      try
        try
          FormatUTF8('% multi %',[rowCount,SQL],fStatementSQL);
          if rowCount>1 then
            SQL := SQL+','+CSVOfValue('('+CSVOfValue('?',fieldCount)+')',rowCount-1);
          fStatementGenericSQL := SQL; // full log on error
          PrepareStatement((rowCount<5) or (valuesCount+fieldCount>MAX_PARAMS));
          prop := 0;
          for f := 0 to valuesCount-1 do begin
            if GetBitPtr(pointer(ValuesNull),f) then
              fStatement^.BindNull(f+1) else
              case Types[prop] of
              ftInt64:
                fStatement^.Bind(f+1,GetInt64(pointer(Values[f])));
              ftDouble, ftCurrency:
                fStatement^.Bind(f+1,GetExtended(pointer(Values[f])));
              ftDate, ftUTF8:
                fStatement^.Bind(f+1,Values[f]);
              ftBlob:
                fStatement^.BindBlob(f+1,Values[f]);
              end;
            inc(prop);
            if prop=fieldCount then
              prop := 0;
          end;
          repeat until fStatement^.Step<>SQLITE_ROW; // ESQLite3Exception on error
          if UpdateEventNeeded then
            for r := valuesFirstRow to valuesFirstRow+rowCount-1 do
              InternalUpdateEvent(seAdd,fBatchTableIndex,fBatchID[r],fBatchValues[r],nil);
          inc(valuesFirstRow,rowCount);
          GetAndPrepareStatementRelease;
        except
          on E: Exception do begin
            GetAndPrepareStatementRelease(E);
            raise;
          end;
        end;
      finally
        DB.UnLock;
      end;
      FillcharFast(ValuesNull[0],(ValuesCount shr 3)+1,0);
      ValuesCount := 0;
      rowCount := 0;
      Fields := nil; // force new SQL statement and Values[]
    until DecodeSaved and (ndx=fBatchValuesCount);
    if valuesFirstRow<>fBatchValuesCount then
      raise EORMBatchException.CreateUTF8('%.InternalBatchStop(valuesFirstRow)',[self]);
  finally
    fBatchMethod := mNone;
    fBatchValuesCount := 0;
    fBatchValues := nil;
    fBatchIDCount := 0;
    fBatchID := nil;
  end;
end;


{ TSQLRestClientDB }

constructor TSQLRestClientDB.Create(aClientModel, aServerModel: TSQLModel; aDB: TSQLDataBase;
   aServerClass: TSQLRestServerDBClass; aHandleUserAuthentication: boolean);
begin
  aDB.UseCache := true;      // we better use caching in this JSON oriented use
  inherited Create(aClientModel);
  if aServerModel=nil then
    aServerModel := TSQLModel.Create(aClientModel); // clone from client
  // next line will create aModel tables if necessary
  fOwnedServer := aServerClass.Create(aServerModel,aDB,aHandleUserAuthentication);
  fServer := fOwnedServer;
  fServer.NoAJAXJSON := true; // use smaller JSON size in this local use (never AJAX)
end;

constructor TSQLRestClientDB.Create(aClientModel, aServerModel: TSQLModel;
  const aDBFileName: TFileName; aServerClass: TSQLRestServerDBClass;
  aHandleUserAuthentication: boolean; const aPassword: RawUTF8;
  aDefaultCacheSize: integer);
begin
  fOwnedDB := TSQLDataBase.Create(aDBFileName,aPassword,0,aDefaultCacheSize);
  Create(aClientModel,aServerModel,fOwnedDB,aServerClass,aHandleUserAuthentication);
end;

constructor TSQLRestClientDB.Create(aRunningServer: TSQLRestServerDB);
var ClientModel: TSQLModel;
begin
  if aRunningServer=nil then
    raise EORMException.Create('TSQLRestClientDB.Create(nil)');
  ClientModel := TSQLModel.Create(aRunningServer.Model);
  ClientModel.Owner := Self; // auto-free ClientModel in TSQLRest.Destroy
  inherited Create(ClientModel);
  fServer := aRunningServer; // leave fOwnedServer=nil
end;

destructor TSQLRestClientDB.Destroy;
var M: TSQLModel;
begin
  try
    inherited Destroy; // UnLock records + SessionClose
  finally
    if fOwnedServer<>nil then begin
      if fServer=nil then
        M := nil else
        M := fServer.Model;
      if (M<>nil) and (M.Owner<>nil) then
        M := nil; // free associated model only if it's owned by nobody
      try
        FreeAndNil(fOwnedServer);
        fServer := nil;
      finally
        M.Free;
        fOwnedDB.Free;
      end;
    end;
  end;
end;

function TSQLRestClientDB.getDB: TSQLDataBase;
begin
  result := fServer.DB;
end;

function TSQLRestClientDB.List(const Tables: array of TSQLRecordClass;
  const SQLSelect, SQLWhere: RawUTF8): TSQLTableJSON;
var aSQL: RawUTF8;
    n: integer;
begin
  result := nil;
  n := length(Tables);
  if (self<>nil) and (n>0) then
  try // will use JSON cache if available:
    aSQL := Model.SQLFromSelectWhere(Tables,SQLSelect,SQLWhere);
    if n=1 then
      // InternalListJSON will handle both static and DB tables
      result := fServer.ExecuteList(Tables,aSQL) else
      // we access localy the DB -> TSQLTableDB handle Tables parameter
      result := TSQLTableDB.Create(fServer.DB,Tables,aSQL,not fServer.NoAJAXJSON);
    if fServer.DB.InternalState<>nil then
      result.InternalState := fServer.DB.InternalState^;
  except
    on ESQLite3Exception do
      result := nil;
  end;
end;

procedure TSQLRestClientDB.InternalURI(var call: TSQLRestURIParams);
begin
  if fInternalHeader='' then
    fInternalHeader := 'RemoteIP: 127.0.0.1'#13#10'ConnectionID: '+PointerToHex(self);
  AddToCSV(fInternalHeader,call.InHead,#13#10);
  call.RestAccessRights := @FULL_ACCESS_RIGHTS;
  fServer.URI(call);
  if (call.OutInternalState=0) and (fServer.DB.InternalState<>nil) then
    call.OutInternalState := fServer.DB.InternalState^; // manual update if necessary
end;

function TSQLRestClientDB.InternalCheckOpen: boolean;
begin
  result := true;
end;

procedure TSQLRestClientDB.InternalClose;
begin
end;


{ TSQLVirtualTableModuleSQLite3 }

procedure Notify(const Format: RawUTF8; const Args: array of const);
begin
  {$ifdef WITHLOG}
  SynSQLite3Log.DebuggerNotify(sllWarning,Format,Args);
  {$endif}
end;

function TSQLVirtualTableModuleSQLite3.FileName(const aTableName: RawUTF8): TFileName;
begin
  if FilePath<>'' then
    // if a file path is specified (e.g. by SynDBExplorer) -> always use this
    result := inherited FileName(aTableName) else
  if SameText(DB.FileName,SQLITE_MEMORY_DATABASE_NAME) then
    // in-memory databases virtual tables should remain in memory
    result := '' else
    // change file path to current DB folder
    result := ExtractFilePath(DB.FileName)+ExtractFileName(inherited FileName(aTableName));
end;

function vt_Create(DB: TSQLite3DB; pAux: Pointer;
  argc: Integer; const argv: PPUTF8CharArray;
  var ppVTab: PSQLite3VTab; var pzErr: PUTF8Char): Integer; cdecl;
var Module: TSQLVirtualTableModuleSQLite3 absolute pAux;
    Table: TSQLVirtualTable;
    Structure: RawUTF8;
    ModuleName: RawUTF8;
begin
  if Module<>nil then
    ModuleName := Module.ModuleName;
  if (Module=nil) or (Module.DB.DB<>DB) or
     (StrIComp(pointer(ModuleName),argv[0])<>0) then begin
    Notify('vt_Create(%<>%)',[argv[0],ModuleName]);
    result := SQLITE_ERROR;
    exit;
  end;
  ppVTab := sqlite3.malloc(sizeof(TSQLite3VTab));
  if ppVTab=nil then begin
    result := SQLITE_NOMEM;
    exit;
  end;
  FillcharFast(ppVTab^,sizeof(ppVTab^),0);
  try
    Table := Module.TableClass.Create(Module,RawUTF8(argv[2]),argc-3,@argv[3]);
  except
    on E: Exception do begin
      ExceptionToSqlite3Err(E,pzErr);
      sqlite3.free_(ppVTab);
      result := SQLITE_ERROR;
      exit;
    end;
  end;
  Structure := Table.Structure;
  result := sqlite3.declare_vtab(DB,pointer(Structure));
  if result<>SQLITE_OK then begin
    Notify('vt_Create(%) declare_vtab(%)',[ModuleName,Structure]);
    Table.Free;
    sqlite3.free_(ppVTab);
    result := SQLITE_ERROR;
  end else
    ppVTab^.pInstance := Table;
end;

function vt_Disconnect(pVTab: PSQLite3VTab): Integer; cdecl;
begin
  TSQLVirtualTable(pvTab^.pInstance).Free;
  sqlite3.free_(pVTab);
  result := SQLITE_OK;
end;

function vt_Destroy(pVTab: PSQLite3VTab): Integer; cdecl;
begin
  if TSQLVirtualTable(pvTab^.pInstance).Drop then
    result := SQLITE_OK else begin
    Notify('vt_Destroy',[]);
    result := SQLITE_ERROR;
  end;
  vt_Disconnect(pVTab); // release memory
end;

function vt_BestIndex(var pVTab: TSQLite3VTab; var pInfo: TSQLite3IndexInfo): Integer;
  cdecl;
const COST: array[TSQLVirtualTablePreparedCost] of double = (1E10,1E8,10,1);
      // costFullScan, costScanWhere, costSecondaryIndex, costPrimaryIndex
var Prepared: PSQLVirtualTablePrepared;
    Table: TSQLVirtualTable;
    i, n: Integer;
begin
  result := SQLITE_ERROR;
  Table := TSQLVirtualTable(pvTab.pInstance);
  if (cardinal(pInfo.nOrderBy)>MAX_SQLFIELDS) or
     (cardinal(pInfo.nConstraint)>MAX_SQLFIELDS) then begin
    Notify('nOrderBy=% nConstraint=%',[pInfo.nOrderBy,pInfo.nConstraint]);
    exit; // avoid buffer overflow
  end;
  Prepared := sqlite3.malloc(sizeof(TSQLVirtualTablePrepared));
  try
    // encode the incoming parameters into Prepared^ record
    Prepared^.WhereCount := pInfo.nConstraint;
    Prepared^.EstimatedCost := costFullScan;
    for i := 0 to pInfo.nConstraint-1 do
      with Prepared^.Where[i], pInfo.aConstraint^[i] do begin
        OmitCheck := False;
        Value.VType := ftUnknown;
        if usable then begin
          Column := iColumn;
          case op of
            SQLITE_INDEX_CONSTRAINT_EQ:    Operation := soEqualTo;
            SQLITE_INDEX_CONSTRAINT_GT:    Operation := soGreaterThan;
            SQLITE_INDEX_CONSTRAINT_LE:    Operation := soLessThanOrEqualTo;
            SQLITE_INDEX_CONSTRAINT_LT:    Operation := soLessThan;
            SQLITE_INDEX_CONSTRAINT_GE:    Operation := soGreaterThanOrEqualTo;
            SQLITE_INDEX_CONSTRAINT_MATCH: Operation := soBeginWith;
            else Column := VIRTUAL_TABLE_IGNORE_COLUMN; // unhandled operator
          end;
        end else
          Column := VIRTUAL_TABLE_IGNORE_COLUMN;
      end;
    Prepared^.OmitOrderBy := false;
    if pInfo.nOrderBy>0 then begin
      assert(sizeof(TSQLVirtualTablePreparedOrderBy)=sizeof(TSQLite3IndexOrderBy));
      Prepared^.OrderByCount := pInfo.nOrderBy;
      MoveFast(pInfo.aOrderBy^[0],Prepared^.OrderBy[0],pInfo.nOrderBy*sizeof(Prepared^.OrderBy[0]));
    end else
      Prepared^.OrderByCount := 0;
    // perform the index query
    if not Table.Prepare(Prepared^) then
      exit;
    // update pInfo and store Prepared into pInfo.idxStr for vt_Filter()
    n := 0;
    for i := 0 to pInfo.nConstraint-1 do
    if Prepared^.Where[i].Value.VType<>ftUnknown then begin
      if i<>n then // expression needed for Search() method to be moved at [n]
        MoveFast(Prepared^.Where[i],Prepared^.Where[n],sizeof(Prepared^.Where[i]));
      inc(n);
      pInfo.aConstraintUsage[i].argvIndex := n;
      pInfo.aConstraintUsage[i].omit := Prepared^.Where[i].OmitCheck;
    end;
    Prepared^.WhereCount := n; // will match argc in vt_Filter()
    if Prepared^.OmitOrderBy then
      pInfo.orderByConsumed := 1 else
      pInfo.orderByConsumed := 0;
    pInfo.estimatedCost := COST[Prepared^.EstimatedCost];
    if sqlite3.VersionNumber>=3008002000 then // starting with SQLite 3.8.2
      case Prepared^.EstimatedCost of
      costFullScan:
        pInfo.estimatedRows := Prepared^.EstimatedRows;
      costScanWhere: // estimate a WHERE clause is a slight performance gain
        pInfo.estimatedRows := Prepared^.EstimatedRows shr 1;
      costSecondaryIndex:
        pInfo.estimatedRows := 10;
      costPrimaryIndex:
        pInfo.estimatedRows := 1;
      else raise EORMException.Create('vt_BestIndex: unexpected EstimatedCost');
      end;
    pInfo.idxStr := pointer(Prepared);
    pInfo.needToFreeIdxStr := 1; // will do sqlite3.free(idxStr) when needed
    result := SQLITE_OK;
    {$ifdef SQLVIRTUALLOGS}
    if Table.Static is TSQLRestStorageExternal then
      TSQLRestStorageExternal(Table.Static).ComputeSQL(prepared^);
    SQLite3Log.Add.Log(sllDebug,'vt_BestIndex(%) plan=% -> cost=% rows=%',
      [sqlite3.VersionNumber,ord(Prepared^.EstimatedCost),pInfo.estimatedCost,pInfo.estimatedRows]);
    {$endif SQLVIRTUALLOGS}
  finally
    if result<>SQLITE_OK then
      sqlite3.free_(Prepared); // avoid memory leak on error
  end;
end;

function vt_Filter(var pVtabCursor: TSQLite3VTabCursor; idxNum: Integer; const idxStr: PAnsiChar;
   argc: Integer; var argv: TSQLite3ValueArray): Integer;
  cdecl;
var Prepared: PSQLVirtualTablePrepared absolute idxStr; // idxNum is not used
    i: integer;
begin
  result := SQLITE_ERROR;
  if Prepared^.WhereCount<>argc then begin
    Notify('vt_Filter WhereCount=% argc=%',[Prepared^.WhereCount,argc]);
    exit; // invalid prepared array (should not happen)
  end;
  for i := 0 to argc-1 do
    SQlite3ValueToSQLVar(argv[i],Prepared^.Where[i].Value);
  if TSQLVirtualTableCursor(pVtabCursor.pInstance).Search(Prepared^) then
    result := SQLITE_OK else
    Notify('vt_Filter Search()',[]);
end;

function vt_Open(var pVTab: TSQLite3VTab; var ppCursor: PSQLite3VTabCursor): Integer;
  cdecl;
var Table: TSQLVirtualTable;
begin
  ppCursor := sqlite3.malloc(sizeof(TSQLite3VTabCursor));
  if ppCursor=nil then begin
    result := SQLITE_NOMEM;
    exit;
  end;
  Table := TSQLVirtualTable(pvTab.pInstance);
  if (Table=nil) or (Table.Module=nil) or (Table.Module.CursorClass=nil) then begin
    Notify('vt_Open',[]);
    sqlite3.free_(ppCursor);
    result := SQLITE_ERROR;
    exit;
  end;
  ppCursor.pInstance := Table.Module.CursorClass.Create(Table);
  result := SQLITE_OK;
end;

function vt_Close(pVtabCursor: PSQLite3VTabCursor): Integer;
  cdecl;
begin
  TSQLVirtualTableCursor(pVtabCursor^.pInstance).Free;
  sqlite3.free_(pVtabCursor);
  result := SQLITE_OK;
end;

function vt_next(var pVtabCursor: TSQLite3VTabCursor): Integer;
  cdecl;
begin
  if TSQLVirtualTableCursor(pVtabCursor.pInstance).Next then
    result := SQLITE_OK else
    result := SQLITE_ERROR;
end;

function vt_Eof(var pVtabCursor: TSQLite3VTabCursor): Integer;
  cdecl;
begin
  if TSQLVirtualTableCursor(pVtabCursor.pInstance).HasData then
    result := 0 else
    result := 1; // reached actual EOF
end;

function vt_Column(var pVtabCursor: TSQLite3VTabCursor; sContext: TSQLite3FunctionContext;
  N: Integer): Integer; cdecl;
var Res: TSQLVar;
begin
  Res.VType := ftUnknown;
  if (N>=0) and TSQLVirtualTableCursor(pVtabCursor.pInstance).Column(N,Res) and
     SQLVarToSQlite3Context(Res,sContext) then
    result := SQLITE_OK else begin
    Notify('vt_Column(%) Res=%',[N,ord(Res.VType)]);
    result := SQLITE_ERROR;
  end;
end;

function vt_Rowid(var pVtabCursor: TSQLite3VTabCursor; var pRowid: Int64): Integer;
  cdecl;
var Res: TSQLVar;
begin
  result := SQLITE_ERROR;
  with TSQLVirtualTableCursor(pVtabCursor.pInstance) do
  if Column(-1,Res) then begin
    case Res.VType of
    ftInt64:    pRowID := Res.VInt64;
    ftDouble:   pRowID := trunc(Res.VDouble);
    ftCurrency: pRowID := trunc(Res.VCurrency);
    ftUTF8:     pRowID := GetInt64(Res.VText);
    else begin
      Notify('vt_Rowid Res=%',[ord(Res.VType)]);
      exit;
    end;
    end;
    result := SQLITE_OK;
  end else
    Notify('vt_Rowid Column',[]);
end;

function vt_Update(var pVTab: TSQLite3VTab;
  nArg: Integer; var ppArg: TSQLite3ValueArray;
  var pRowid: Int64): Integer; cdecl;
var Values: TSQLVarDynArray;
    Table: TSQLVirtualTable;
    RowID0, RowID1: Int64;
    i: integer;
    OK: boolean;
begin // call Delete/Insert/Update methods according to supplied parameters
  Table := TSQLVirtualTable(pvTab.pInstance);
  result := SQLITE_ERROR;
  if (nArg<=0) or (nArg>1024) then
    exit;
  case sqlite3.value_type(ppArg[0]) of
    SQLITE_INTEGER: RowID0 := sqlite3.value_int64(ppArg[0]);
    SQLITE_NULL:    RowID0 := 0;
    else exit; // invalid call
  end;
  if nArg=1 then
    OK := Table.Delete(RowID0) else begin
    case sqlite3.value_type(ppArg[1]) of
      SQLITE_INTEGER: RowID1 := sqlite3.value_int64(ppArg[1]);
      SQLITE_NULL:    RowID1 := 0;
      else exit; // invalid call
    end;
    SetLength(Values,nArg-2);
    for i := 0 to nArg-3 do
      SQlite3ValueToSQLVar(ppArg[i+2],Values[i]);
    if RowID0=0 then
      OK := Table.Insert(RowID1,Values,pRowid) else
      OK := Table.Update(RowID0,RowID1,Values);
  end;
  if OK then
    result := SQLITE_OK else
    Notify('vt_Update(%)',[pRowID]);
end;

function InternalTrans(pVTab: TSQLite3VTab; aState: TSQLVirtualTableTransaction;
  aSavePoint: integer): integer;
begin
  if TSQLVirtualTable(pvTab.pInstance).Transaction(aState,aSavePoint) then
    result := SQLITE_OK else begin
    Notify('Transaction(%,%)',[ToText(aState)^,aSavePoint]);
    result := SQLITE_ERROR;
  end;
end;

function vt_Begin(var pVTab: TSQLite3VTab): Integer; cdecl;
begin
  result := InternalTrans(pVTab,vttBegin,0);
end;

function vt_Commit(var pVTab: TSQLite3VTab): Integer; cdecl;
begin
  result := InternalTrans(pVTab,vttCommit,0);
end;

function vt_RollBack(var pVTab: TSQLite3VTab): Integer; cdecl;
begin
  result := InternalTrans(pVTab,vttRollBack,0);
end;

function vt_Sync(var pVTab: TSQLite3VTab): Integer; cdecl;
begin
  result := InternalTrans(pVTab,vttSync,0);
end;

function vt_SavePoint(var pVTab: TSQLite3VTab; iSavepoint: integer): Integer; cdecl;
begin
  result := InternalTrans(pVTab,vttSavePoint,iSavePoint);
end;

function vt_Release(var pVTab: TSQLite3VTab; iSavepoint: integer): Integer; cdecl;
begin
  result := InternalTrans(pVTab,vttRelease,iSavePoint);
end;

function vt_RollBackTo(var pVTab: TSQLite3VTab; iSavepoint: integer): Integer; cdecl;
begin
  result := InternalTrans(pVTab,vttRollBackTo,iSavePoint);
end;

function vt_Rename(var pVTab: TSQLite3VTab; const zNew: PAnsiChar): Integer; cdecl;
begin
  if TSQLVirtualTable(pvTab.pInstance).Rename(RawUTF8(zNew)) then
    result := SQLITE_OK else begin
    Notify('vt_Rename(%)',[zNew]);
    result := SQLITE_ERROR;
  end;
end;

procedure sqlite3InternalFreeModule(p: pointer); cdecl;
begin
  if (p<>nil) and (TSQLVirtualTableModuleSQLite3(p).fDB<>nil) then
    TSQLVirtualTableModuleSQLite3(p).Free;
end;

procedure TSQLVirtualTableModuleSQLite3.Attach(aDB: TSQLDataBase);
begin
  if aDB=nil then
    raise EBusinessLayerException.CreateFmt('aDB=nil at %s.SetDB()',[ClassName]);
  if fDB<>nil then
    raise EBusinessLayerException.CreateFmt('fDB<>nil at %s.SetDB()',[ClassName]);
  FillCharFast(fModule,sizeof(fModule),0);
  fModule.iVersion := 1;
  fModule.xCreate := vt_Create;
  fModule.xConnect := vt_Create;
  fModule.xBestIndex := vt_BestIndex;
  fModule.xDisconnect := vt_Disconnect;
  fModule.xDestroy := vt_Destroy;
  fModule.xOpen := vt_Open;
  fModule.xClose := vt_Close;
  fModule.xFilter := vt_Filter;
  fModule.xNext := vt_Next;
  fModule.xEof := vt_Eof;
  fModule.xColumn := vt_Column;
  fModule.xRowid := vt_Rowid;
  if vtWrite in Features then begin
    fModule.xUpdate := vt_Update;
    if vtTransaction in Features then begin
      fModule.xBegin := vt_Begin;
      fModule.xSync := vt_Sync;
      fModule.xCommit := vt_Commit;
      fModule.xRollback := vt_RollBack;
    end;
    if vtSavePoint in Features then begin
      fModule.iVersion := 2;
      fModule.xSavePoint := vt_SavePoint;
      fModule.xRelease := vt_Release;
      fModule.xRollBackTo := vt_RollBackTo;
    end;
    fModule.xRename := vt_Rename;
  end;
  sqlite3_check(aDB.DB,sqlite3.create_module_v2(aDB.DB,pointer(fModuleName),fModule,
    self,sqlite3InternalFreeModule)); // raise ESQLite3Exception on error
  fDB := aDB; // mark successfull create_module() for sqlite3InternalFreeModule
end;


{ TSQLVirtualTableModuleServerDB }

constructor TSQLVirtualTableModuleServerDB.Create(
  aClass: TSQLVirtualTableClass; aServer: TSQLRestServer);
begin
  if not aServer.InheritsFrom(TSQLRestServerDB) then
    raise EBusinessLayerException.CreateFmt('%.Create expects a DB Server',[ClassName]);
  inherited;
  Attach(TSQLRestServerDB(aServer).DB);
  // any exception in Attach() will let release the instance by the RTL
end;


{ TSQLRestStorageShardDB }

constructor TSQLRestStorageShardDB.Create(aClass: TSQLRecordClass;
  aServer: TSQLRestServer; aShardRange: TID; aOptions: TSQLRestStorageShardOptions;
  const aShardRootFileName: TFileName; aMaxShardCount: integer;
  aSynchronous: TSQLSynchronousMode; aCacheSizePrevious,aCacheSizeLast: Integer);
begin
  fShardRootFileName := aShardRootFileName;
  fSynchronous := aSynchronous;
  fCacheSizePrevious := aCacheSizePrevious;
  fCacheSizeLast := aCacheSizeLast;
  inherited Create(aClass,aServer,aShardRange,aOptions,aMaxShardCount);
end;

function TSQLRestStorageShardDB.DBFileName(ShardIndex: Integer): TFileName;
begin
  result := Format('%s%.4d.dbs',[fShardRootFileName,fShardOffset+ShardIndex]);
end;

function TSQLRestStorageShardDB.InitNewShard: TSQLRest;
var db: TSQLRestServerDB;
    cachesize: integer;
    sql: TSQLDataBase;
    model: TSQLModel;
begin
  inc(fShardLast);
  model := TSQLModel.Create([fStoredClass],FormatUTF8('shard%',[fShardLast]));
  if fInitShardsIsLast then // last/new .dbs = 2MB cache, previous 1MB only
    cachesize := fCacheSizeLast else
    cachesize := fCacheSizePrevious;
  sql := TSQLDatabase.Create(DBFileName(fShardLast),'',0,cachesize);
  sql.LockingMode := lmExclusive;
  sql.Synchronous := fSynchronous;
  db := TSQLRestServerDB.Create(model,sql,false,true);
  model.Owner := db;
  db.CreateMissingTables;
  result := db;
  SetLength(fShards,fShardLast+1);
  fShards[fShardLast] := result;
end;

procedure TSQLRestStorageShardDB.InitShards;
var f,i,num,first: integer;
    db: TFindFilesDynArray;
    mask: TFileName;
begin
  if fShardRootFileName='' then
    fShardRootFileName := ExeVersion.ProgramFilePath+UTF8ToString(fStoredClass.SQLTableName);
  mask := DBFileName(0);
  i := Pos('0000',mask);
  if i>0 then begin
    system.Delete(mask,i,3);
    mask[i] := '*';
  end else
    mask := fShardRootFileName+'*.dbs';
  db := FindFiles(ExtractFilePath(mask),ExtractFileName(mask),'',{sorted=}true); 
  if db=nil then
    exit; // no existing data
  fShardOffset := -1;
  first := length(db)-integer(fMaxShardCount);
  if first<0 then
    first := 0;
  for f := first to high(db) do begin
    i := Pos('.dbs',db[f].Name);
    if (i<=4) or not TryStrToInt(Copy(db[f].Name,i-4,4),num) then begin
      InternalLog('InitShards(%)?',[db[f].Name],sllWarning);
      continue;
    end;
    if fShardOffset<0 then
      fShardOffset := num;
    dec(num,fShardOffset);
    if not SameText(DBFileName(num),db[f].Name) then
      raise EORMException.CreateUTF8('%.InitShards(%)',[self,db[f].Name]);
    if f = high(db) then
      fInitShardsIsLast := true;
    fShardLast := num-1; // 'folder\root0005.dbs' -> fShardLast := 4
    InitNewShard;        // now fShardLast=5, fShards[5] contains root005.dbs
  end;
  if fShardOffset<0 then
    fShardOffset := 0;
  if Integer(fShardLast)<0 then begin
    InternalLog('InitShards?',sllWarning);
    exit;
  end;
  fInitShardsIsLast := true; // any newly appended .dbs would use 2MB of cache
  fShardLastID := fShards[fShardLast].TableMaxID(fStoredClass);
  if fShardLastID<0 then
    fShardLastID := 0; // no data yet
end;


function RegisterVirtualTableModule(aModule: TSQLVirtualTableClass; aDatabase: TSQLDataBase): TSQLVirtualTableModule;
begin
  result := TSQLVirtualTableModuleSQLite3.Create(aModule,nil);
  try
    TSQLVirtualTableModuleSQLite3(result).Attach(aDatabase);
  except
    on Exception do begin
      result.Free; // should be released by hand here
      raise; // e.g. EBusinessLayerException or ESQLite3Exception
    end;
  end;
end;

initialization
  {$ifdef WITHLOG}
  // all our SynSQlite3 related functions shall log to main TSQLLog
  SynSQLite3Log := TSQLLog;
  {$endif}
  TSQLRestServerDB.RegisterClassNameForDefinition;
end.



