2561 lines
95 KiB
ObjectPascal
2561 lines
95 KiB
ObjectPascal
/// 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.
|
|
|
|
|
|
|