mirror of
https://github.com/HeidiSQL/HeidiSQL.git
synced 2026-03-13 09:24:25 +08:00
3012 lines
117 KiB
ObjectPascal
3012 lines
117 KiB
ObjectPascal
/// SQLite3 embedded Database engine used as the kernel of mORMot
|
|
// - this unit is a part of the freeware Synopse mORMot framework,
|
|
// licensed under a MPL/GPL/LGPL tri-license; version 1.17
|
|
unit SQLite3;
|
|
|
|
{
|
|
This file is part of Synopse mORMot framework.
|
|
|
|
Synopse mORMot framework. Copyright (C) 2012 Arnaud Bouchez
|
|
Synopse Informatique - http://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 SQLite3/mORMot database framework.
|
|
|
|
The Initial Developer of the Original Code is Arnaud Bouchez.
|
|
|
|
Portions created by the Initial Developer are Copyright (C) 2012
|
|
the Initial Developer. All Rights Reserved.
|
|
|
|
Contributor(s):
|
|
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 *****
|
|
|
|
|
|
Initial version: 2008 March, by Arnaud Bouchez
|
|
|
|
Version 1.3 - January 22, 2010
|
|
- allow backup directly into a custom file (not only a custom directory)
|
|
- on-the-fly Restore of the database from a compressed backup file
|
|
- some small fixes and multi-compiler enhancements
|
|
- compiler conditional renamed ENHANCEDRTL instead of ENHANCEDTRTL
|
|
|
|
Version 1.4 - February 08, 2010
|
|
- whole Synopse SQLite3 database framework released under the GNU Lesser
|
|
General Public License version 3, instead of generic "Public Domain"
|
|
|
|
Version 1.5 - March 10, 2010
|
|
- updated engine to version 3.6.23
|
|
- included FTS3 embedded sqlite3fts3.obj version
|
|
|
|
Version 1.6
|
|
- SQLite3 database layer updated to version 3.6.23.1
|
|
|
|
Version 1.7
|
|
- alter table with newly added fields to a TSQLRecord
|
|
|
|
Version 1.8
|
|
- includes Unitary Testing class and functions
|
|
- database update engine to version 3.7.0 (main new feature is WAL)
|
|
- SetWALMode() method for enabling Write-Ahead Logging for the database
|
|
- the RTREE extension is now compiled by default into the engine
|
|
- new tests added (mostly relative to the new functions or classes)
|
|
|
|
Version 1.9
|
|
- database engine updated to 3.7.1
|
|
- fix issue in TSQLRestServerDB.CreateMissingTables when an exception occured
|
|
- handle now RowID as a valid alias to the ID field (needed for TSQLRecordFTS3)
|
|
- handle TSQLRecordFTS3 record, for using FTS3 virtual tables,
|
|
i.e. implementing full-text search (including dedicated regression tests)
|
|
|
|
Version 1.9.1
|
|
- update engine to version 3.7.2: an obscure but very old bug makes
|
|
SQLite authors recommend to use 3.7.2 for all new development.
|
|
Upgrading from all prior SQLite versions is also recommended.
|
|
|
|
Version 1.9.2
|
|
- fixed a potential GPF in TSQLRestClientDB.Destroy
|
|
|
|
Version 1.10
|
|
- code modifications to compile with Delphi 6 compiler (Delphi 5 failed due
|
|
to some obscure compiler bugs in SynCrypto.pas)
|
|
- update SQLite3 engine to version 3.7.3
|
|
|
|
Version 1.11
|
|
- update SQLite3 engine to version 3.7.4
|
|
- introduces new TSQLite3DB, TSQLite3Statement, TSQLite3Blob, TSQLite3Value
|
|
and TSQLite3FunctionContext types to clarify SQLite3 internal handle usage
|
|
- new sqlite3_busy_timeout and sqlite3_busy_handler low-level functions
|
|
- new TSQLDataBase.BusyTimeout property, to set the database timeout
|
|
value in milliseconds
|
|
- now handles User Defined Functions, via sqlite3_create_function_v2 and
|
|
corresponding sqlite3_result_* functions: as sample, the MOD() function
|
|
is defined in any database opened via TSQLDataBase (it was needed
|
|
to have compatibility with Oracle/MySQL/MSSQL/PostGreSQL engines)
|
|
- protect the TSQLDatabase methods called when self is nil, which could
|
|
occur if the database is not yet initialized (could occur if only a
|
|
TSQLRestServerStatic exists, like in TTestSQLite3Engine._TSQLRestClientDB)
|
|
- new SOUNDEX() function available in SQL statements (calling SoundExUTF8)
|
|
and associated SOUNDEXFR/SOUNDEXES for french or spanish Soundex variants
|
|
- fixed an issue found out by WladiD about all collation functions:
|
|
If a field with your custom collate ISO8601 is empty '' (not NULL),
|
|
then SQLite calls the registered collate function with length 0 for s1len
|
|
or s2len, but the pointers s1 or s2 map to the string of the previous call
|
|
- added sqlite3_result_error() call to make wrong parameter count error
|
|
explicit during SQL statement internal functions calls
|
|
- handles new FTS4 extension module - see
|
|
http://sqlite.org/fts3.html#section_1_1 - which is available since 3.7.4
|
|
- new RANK() function available in SQL statements for ranking FTS3/FTS4
|
|
with best performance (used by the new TSQLRest.FTSMatch() overloaded
|
|
method) - see http://www.sqlite.org/fts3.html#appendix_a
|
|
- fixed dual memory release in case of FTS3 use, in TSQLDataBase.Destroy
|
|
- source code modified to be 7 bit Ansi (so will work with all encodings)
|
|
|
|
Version 1.12
|
|
- update SQLite3 engine to version 3.7.5
|
|
- fixed sqlite3_result_text() implementation
|
|
- added sqlite3_aggregate_context(), sqlite3_value_numeric_type() and
|
|
sqlite3InternalFree() functions
|
|
- new CONCAT() function available in SQL statements to process fast
|
|
string concatenation
|
|
- now handle automaticaly prepared SQL statements: the parameters must
|
|
be surrounded with :(...): in order to use an internal pool of prepared
|
|
TSQLRequest; example of possible inlined values are :(1234):
|
|
:(12.34): :(12E-34): :("text"): or :('text'): (with double quoting
|
|
inside the text, just like any SQL statement)
|
|
- new sqlite3_stmt_readonly() function and TSQLRequest.IsReadOnly property
|
|
|
|
Version 1.13
|
|
- update SQLite3 engine to version 3.7.6.3
|
|
- added sqlite3InternalFreeObject(), sqlite3_malloc/realloc/free(),
|
|
sqlite3_memory_used/highwater(), sqlite3_set_authorizer() and
|
|
sqlite3_update/commit/rollback_hook() functions
|
|
- introducing TSQLVirtualTableModuleSQLite3 / TSQLVirtualTableModuleServerDB
|
|
classes, and associated low-level sqlite3 functions and memory structures,
|
|
in order to implement Virtual Table modules in pure Delphi code via
|
|
common TSQLVirtualTable classes as defined in SQLite3Commons
|
|
- new IntegerDynArrayContains(), RawUTF8DynArrayContainsCase/NoCase() and
|
|
Byte/Word/Cardinal/Int64/CurrencyDynArrayContains(BlobField,I64)
|
|
SQL functions, able to fast search data within T*DynArray and
|
|
TRawUTF8DynArray published properties BLOB (Currency mapped as PInt64)
|
|
- new TSQLDataBaseSQLFunction and TSQLDataBase.RegisterSQLFunction method,
|
|
to implement custom SQL functions with any kind of BLOB data
|
|
- regression test now exclude unfixed order for select (may vary,
|
|
just like happened for 3.7.6 when e.g. indexes started to be used)
|
|
- added regression tests for sftBlobDynArray and sftObject kind of records
|
|
- TSQLRestServerDB now uses faster TDynArrayHashed for its internal prepared
|
|
statement cache
|
|
- fixed issue in TSQLRestClientDB.URI: wrong InternalState returned
|
|
- fastest internal cdecl qsort() function (optimized for PAnsiChar)
|
|
|
|
Version 1.14
|
|
- update SQLite3 engine to version 3.7.7.1
|
|
- fixed issue in produced JSON stream using '=' instead of ':'
|
|
- TSQLDatabase.user_version now defined as a property, with a getter and
|
|
a setter methods (not read/only any more)
|
|
|
|
Version 1.15
|
|
- updated SQLite3 engine to version 3.7.8
|
|
- unit now tested with Delphi XE2 (32 Bit)
|
|
- transactions now following a safe concurent access (both thread-safe and
|
|
client/connection-safe) - but authentication should be enabled
|
|
- the SQLite3 wrapper is now located in a separate SynSQLite3 unit: this
|
|
will allow to use it as a separate database engine, e.g. using SynDB
|
|
classes without the overhead/features of our mORMot framework
|
|
- statement cache is now shared with SynDBSQLite3, via the new
|
|
TSQLStatementCached object as defined in SynSQLite3.pas
|
|
- now TSQLRestServerDB will unregister any TSQLVirtualTableModuleServerDB
|
|
to avoid random GPF in TSQLVirtualTable.Destroy
|
|
- TSQLRestClientDB and TSQLRestServerDB constructors now accept an optional
|
|
Password parameter, associated to the supplied file name, in order to
|
|
use database encryption
|
|
|
|
Version 1.16
|
|
- updated SQLite3 engine to version 3.7.12.1
|
|
- unit now includes FTS3/FTS4 by default (i.e. INCLUDE_FTS3 conditional is
|
|
set in both SQLite3.pas and SynSQLite3.pas units)
|
|
- fixed TSQLRestServerDB.UpdateField(ByID=true) implementation
|
|
- fixed VACUUM failure if there are one or more active SQL statements
|
|
- new overloaded TSQLRestServerDB.UpdateField method
|
|
- TSQLRestServerDB.EngineList() method now handles an optional integer
|
|
pointer, to return the count of row data
|
|
- updated TSQLRestServerTest published methods to use the new parameter
|
|
layout using "var aParams: TSQLRestServerCallBackParams"
|
|
|
|
Version 1.17
|
|
- updated SQLite3 engine to version 3.7.14
|
|
- added overriden TSQLRestServerDB.FlushInternalDBCache method
|
|
- added TSQLRestServerDB.BackupGZ method for live database backup into a
|
|
compressed .gz archive file
|
|
|
|
}
|
|
|
|
interface
|
|
|
|
{$I Synopse.inc} // define HASINLINE USETYPEINFO CPU32 CPU64 WITHLOG
|
|
|
|
uses
|
|
Windows,
|
|
SysUtils,
|
|
Classes,
|
|
{$ifndef LVCL}
|
|
Contnrs,
|
|
{$endif}
|
|
SynZip,
|
|
SynCommons,
|
|
SynSQLite3,
|
|
SQLite3Commons;
|
|
|
|
{$define INCLUDE_FTS3}
|
|
{ define this if you want to include the FTS3/FTS4 feature into the library
|
|
- FTS3 is an SQLite module implementing full-text search
|
|
- will include also FTS4 extension module since 3.7.4
|
|
- see http://www.sqlite.org/fts3.html for documentation
|
|
- is defined by default, but can be unset to save about 50 KB of code size
|
|
- should be defined for both SynSQLite3 and SQLite3 units }
|
|
|
|
{.$define USEFASTCALL}
|
|
{ use the fastcall calling convention to access the SQLite3 library
|
|
- BCC32 -pr fastcall (=Delphi resgister) is buggy, don't know why
|
|
(because of issues with BCC32 itself, or some obfuscated calls in source?)
|
|
- should be defined for both SynSQLite3 and SQLite3 units }
|
|
|
|
|
|
{ ****************** 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 TClass;
|
|
const aSQL: RawUTF8; Expand: boolean);
|
|
end;
|
|
|
|
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
|
|
/// internal copy of the SQLite3 database engine
|
|
fDB: TSQLDataBase;
|
|
/// initialized by Create(aModel,aDBFileName)
|
|
fOwnedDB: TSQLDataBase;
|
|
/// prepared statements with parameters for faster SQLite3 execution
|
|
// - works for SQL code with :(%): internal parameters
|
|
fStatementCache: TSQLStatementCached;
|
|
/// static statement used if fStatementCache is not to be used
|
|
// - for any SQL code with no :(%): internal parameters
|
|
// - only one global is OK: is protected via a nested DB.LockJSON/DB.Unlock
|
|
fStaticStatement: TSQLRequest;
|
|
/// used to unregister Module.Server to avoid GPF in TSQLVirtualTable.Destroy
|
|
fRegisteredVirtualTableModule: array of TSQLVirtualTableModuleServerDB;
|
|
protected
|
|
/// retrieve a TSQLRequest instance, corresponding to any previous
|
|
// prepared statement using :(%): internal parameters
|
|
// - will return @fStaticStatement if no :(%): internal parameters appear:
|
|
// in this case, the TSQLRequest.Close method must be called
|
|
// - will return 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"):'
|
|
function GetAndPrepareStatement(const SQL: RawUTF8): PSQLRequest;
|
|
/// reset the cache if necessary
|
|
procedure SetNoAJAXJSON(const Value: boolean); override;
|
|
/// overriden methods for direct sqlite3 database engine call:
|
|
function EngineList(const SQL: RawUTF8; ForceAJAX: Boolean=false; ReturnedRowCount: PPtrInt=nil): RawUTF8; override;
|
|
function EngineRetrieve(TableModelIndex, ID: integer): RawUTF8; override;
|
|
function EngineAdd(Table: TSQLRecordClass; const SentData: RawUTF8): integer; override;
|
|
function EngineUpdate(Table: TSQLRecordClass; ID: integer; const SentData: RawUTF8): boolean; override;
|
|
function EngineDelete(Table: TSQLRecordClass; ID: integer): boolean; override;
|
|
function EngineDeleteWhere(Table: TSQLRecordClass; const SQLWhere: RawUTF8;
|
|
const IDs: TIntegerDynArray): boolean; override;
|
|
function EngineRetrieveBlob(Table: TSQLRecordClass; aID: integer;
|
|
BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean; override;
|
|
function EngineUpdateBlob(Table: TSQLRecordClass; aID: integer;
|
|
BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean; override;
|
|
function EngineUpdateField(Table: TSQLRecordClass;
|
|
const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; override;
|
|
/// execute one SQL statement
|
|
// - intercept any DB exception and return false on error, true on success
|
|
function EngineExecute(const aSQL: RawUTF8; ValueInt: PInt64=nil; ValueUTF8: PRawUTF8=nil): boolean; overload;
|
|
/// EngineExecute directly a SQL statement with supplied parameters
|
|
// - expect the same format as FormatUTF8() function, that is only % with
|
|
// strings (already quoted) and integers (not ? with parameters)
|
|
// - return true on success
|
|
function EngineExecuteFmt(SQLFormat: PUTF8Char; const Args: array of const): boolean;
|
|
/// execute one SQL statement, which must return an integer (Int64) value
|
|
// - intercept any DB exception and return false on error, true on success
|
|
function EngineExecute(const aSQL: RawUTF8; out Value: Int64): boolean; overload;
|
|
/// execute one SQL statement, which must return a UTF-8 encoded value
|
|
// - intercept any DB exception and return false on error, true on success
|
|
function EngineExecute(const aSQL: RawUTF8; out Value: RawUTF8): boolean; overload;
|
|
/// 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 EngineExecute(const aSQL: RawUTF8; StoredProc: TOnSQLStoredProc): boolean; overload;
|
|
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 otherwize }
|
|
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); 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;
|
|
/// overriden methods for direct sqlite3 database engine call
|
|
function EngineExecuteAll(const aSQL: RawUTF8): boolean; override;
|
|
{{ backup of the opened Database into an external stream (e.g. a file,
|
|
compressed or not)
|
|
- this method doesn't use the experimental SQLite Online Backup API
|
|
(which restart the backup process on any write: so no good performance
|
|
could be achieved on a working database: this method uses a faster
|
|
lock + copy approach)
|
|
- database is closed, VACCUUMed, copied, then reopened }
|
|
function Backup(Dest: TStream): boolean;
|
|
{{ backup of the opened Database into a .gz compressed file
|
|
- 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;
|
|
{{ restore a database content on the fly
|
|
- database is closed, source DB file is replaced by the supplied content,
|
|
then reopened }
|
|
function Restore(const ContentToRestore: RawByteString): boolean;
|
|
/// 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.
|
|
// TSQLRestServerStaticExternal classes defined in SQLite3DB) could flush
|
|
// the database content without proper notification
|
|
// - this overriden implementation will call TSQLDataBase.CacheFlush method
|
|
procedure FlushInternalDBCache; override;
|
|
public
|
|
/// initialize a REST server with a database
|
|
// - any needed TSQLVirtualTable class should have been already registered
|
|
// via the RegisterVirtualTableModule() method
|
|
constructor Create(aModel: TSQLModel; aDB: TSQLDataBase;
|
|
aHandleUserAuthentication: boolean=false); 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=''); reintroduce; overload;
|
|
/// close database and free used memory
|
|
destructor Destroy; 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);
|
|
|
|
/// associated database
|
|
property DB: TSQLDataBase read fDB;
|
|
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;
|
|
fOwnedDB: TSQLDataBase;
|
|
function getDB: TSQLDataBase;
|
|
protected
|
|
/// method calling the RESTful server fServer
|
|
function InternalURI(const url, method: RawUTF8; Resp: PRawUTF8=nil;
|
|
Head: PRawUTF8=nil; SendData: PRawUTF8=nil): Int64Rec; 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=''); reintroduce; overload;
|
|
/// release the server
|
|
destructor Destroy; override;
|
|
|
|
/// retrieve a list of members as a TSQLTable (implements REST GET Collection)
|
|
// - this overriden 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;
|
|
|
|
/// this class defined two published methods of type TSQLRestServerCallBack in
|
|
// order to test the Server-Side ModelRoot/TableName/ID/MethodName RESTful model
|
|
TSQLRestServerTest = class(TSQLRestServerDB)
|
|
published
|
|
{/ test ModelRoot/People/ID/DataAsHex
|
|
- this method is called by TSQLRestServer.URI when a
|
|
ModelRoot/People/ID/DataAsHex GET request is provided
|
|
- Parameters values are not used here: this service only need aRecord.ID
|
|
- SentData is set with incoming data from a PUT method
|
|
- if called from ModelRoot/People/ID/DataAsHex with GET or PUT methods,
|
|
TSQLRestServer.URI will create a TSQLRecord instance and set its ID
|
|
(but won't retrieve its other field values automaticaly)
|
|
- if called from ModelRoot/People/DataAsHex with GET or PUT methods,
|
|
TSQLRestServer.URI will leave aRecord.ID=0 before launching it
|
|
- if called from ModelRoot/DataAsHex with GET or PUT methods,
|
|
TSQLRestServer.URI will leave aRecord=nil before launching it
|
|
- implementation must return the HTTP error code (e.g. 200 as success)
|
|
- Table is overloaded as TSQLRecordPeople here, and still match the
|
|
TSQLRestServerCallBack prototype: but you have to check the class
|
|
at runtime: it can be called by another similar but invalid URL, like
|
|
ModelRoot/OtherTableName/ID/DataAsHex }
|
|
function DataAsHex(var aParams: TSQLRestServerCallBackParams): Integer;
|
|
{/ method used to test the Server-Side ModelRoot/Sum or
|
|
ModelRoot/People/Sum Requests
|
|
- implementation of this method returns the sum of two floating-points,
|
|
named A and B, as in the public TSQLRecordPeople.Sum() method,
|
|
which implements the Client-Side of this service
|
|
- Table nor ID are never used here }
|
|
function Sum(var aParams: TSQLRestServerCallBackParams): Integer;
|
|
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;
|
|
/// initialize the DB connection via sqlite3_create_module_v2(fModule)
|
|
procedure SetDB(aDB: TSQLDataBase);
|
|
public
|
|
/// retrieve the file name to be used for a specific Virtual Table
|
|
// - overriden method returning a file located in the DB file folder, and
|
|
// '' if the main DB was created as ':memory:' (so 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 ':memory:'
|
|
function FileName(const aTableName: RawUTF8): TFileName; override;
|
|
/// the associated SQLite3 database connection
|
|
// - setting the DB property will initialize the virtual table module
|
|
// for this DB connection, calling the sqlite3_create_module_v2() function
|
|
property DB: TSQLDataBase read fDB write SetDB;
|
|
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
|
|
constructor Create(aClass: TSQLVirtualTableClass;
|
|
aServer: TSQLRestServer); override;
|
|
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)
|
|
function RegisterVirtualTableModule(aModule: TSQLVirtualTableClass; aDatabase: TSQLDataBase): TSQLVirtualTableModule;
|
|
|
|
/// set a TVarData into a SQlite3 result context
|
|
// - only handle varNull, varInt64, varDouble, varString (mapping a constant
|
|
// PUTF8Char), and varAny (BLOB with size = VLongs[0]) types in TVarData
|
|
// - therefore it's not a true Variant typecast, only a way of data sharing e.g.
|
|
// for the TSQLVirtualTableCursor.Column method
|
|
// - will call the corresponding sqlite3_result_*() function and return true,
|
|
// or will return false if the TVarData type is not handled
|
|
function VarDataToContext(Context: TSQLite3FunctionContext; const Res: TVarData): boolean;
|
|
|
|
/// set a SQLite3 value into a TVarData
|
|
// - only handle varNull, varInt64, varDouble, varString (mapping a constant
|
|
// PUTF8Char), and varAny (BLOB with size = VLongs[0]) types in TVarData
|
|
// - therefore it's not a true Variant typecast, only a way of data sharing e.g.
|
|
// for the TSQLVirtualTable.Insert or Update methods
|
|
// - will call the corresponding sqlite3_value_*() function to retrieve the
|
|
// data with the less overhead (e.g. memory allocation or copy) as possible
|
|
procedure VarDataFromValue(Value: TSQLite3Value; var Res: TVarData);
|
|
|
|
|
|
{ ************ Unit-Testing classes and functions }
|
|
|
|
type
|
|
/// a parent test case which will test most functions, classes and types defined
|
|
// and implemented in the SQLite3 unit, i.e. the SQLite3 engine itself
|
|
// - it should not be called directly, but through TTestFileBased,
|
|
// TTestMemoryBased and TTestMemoryBased children
|
|
TTestSQLite3Engine = class(TSynTestCase)
|
|
protected
|
|
{ these values are used internaly by the published methods below }
|
|
TempFileName: TFileName;
|
|
IsMemory: boolean;
|
|
Demo: TSQLDataBase;
|
|
Req: RawUTF8;
|
|
JS: RawUTF8;
|
|
public
|
|
destructor Destroy; override;
|
|
published
|
|
/// test direct access to the SQLite3 engine
|
|
// - i.e. via TSQLDataBase and TSQLRequest classes
|
|
procedure DatabaseDirectAccess;
|
|
/// test direct access to the Virtual Table features of SQLite3
|
|
procedure VirtualTableDirectAccess;
|
|
/// test the TSQLTableJSON table
|
|
// - the JSON content generated must match the original data
|
|
// - a VACCUM is performed, for testing some low-level SQLite3 engine
|
|
// implementation
|
|
// - the SortField feature is also tested
|
|
procedure _TSQLTableJSON;
|
|
/// test the TSQLRestClientDB, i.e. a local Client/Server driven usage
|
|
// of the framework
|
|
// - validates TSQLModel, TSQLRestServer and TSQLRestServerStatic by checking
|
|
// the coherency of the data between client and server instances, after
|
|
// update from both sides
|
|
// - use all RESTful commands (GET/UDPATE/POST/DELETE...)
|
|
// - test the 'many to many' features (i.e. TSQLRecordMany) and dynamic
|
|
// arrays published properties handling
|
|
// - also test FTS implementation if INCLUDE_FTS3 conditional is defined
|
|
// - test dynamic tables
|
|
procedure _TSQLRestClientDB;
|
|
end;
|
|
|
|
/// this test case will test most functions, classes and types defined and
|
|
// implemented in the SQLite3 unit, i.e. the SQLite3 engine itself,
|
|
// with a file-based approach
|
|
TTestFileBased = class(TTestSQLite3Engine);
|
|
|
|
/// this test case will test most functions, classes and types defined and
|
|
// implemented in the SQLite3 unit, i.e. the SQLite3 engine itself,
|
|
// with a memory-based approach
|
|
// - this class will also test the TSQLRestServerStatic class, and its
|
|
// 100% Delphi simple database engine
|
|
TTestMemoryBased = class(TTestSQLite3Engine);
|
|
|
|
/// this test case will test most functions, classes and types defined and
|
|
// implemented in the SQLite3 unit, i.e. the SQLite3 engine itself,
|
|
// with a file-based approach
|
|
// - purpose of this class is to test Write-Ahead Logging for the database
|
|
TTestFileBasedWAL = class(TTestFileBased);
|
|
|
|
|
|
implementation
|
|
|
|
|
|
{ TSQLTableDB }
|
|
|
|
constructor TSQLTableDB.Create(aDB: TSQLDataBase; const Tables: array of TClass;
|
|
const aSQL: RawUTF8; Expand: boolean);
|
|
var JSONCached: RawUTF8;
|
|
R: TSQLRequest;
|
|
n: PtrInt;
|
|
begin
|
|
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 Create(Tables,aSQL,JSONCached);
|
|
Assert(n=RowCount);
|
|
finally
|
|
aDB.UnLockJSON(JSONCached,n);
|
|
end
|
|
else begin
|
|
inherited Create(Tables,aSQL,JSONCached);
|
|
Assert(n=RowCount);
|
|
end;
|
|
end;
|
|
|
|
|
|
{ TSQLRestServerDB }
|
|
|
|
function TSQLRestServerDB.GetAndPrepareStatement(const SQL: RawUTF8): PSQLRequest;
|
|
var i, maxParam: integer;
|
|
Types: TSQLFieldTypeArray;
|
|
Values: TRawUTF8DynArray;
|
|
GenericSQL: RawUTF8;
|
|
begin
|
|
GenericSQL := ExtractInlineParameters(SQL,Types,Values,maxParam);
|
|
if maxParam=0 then begin
|
|
// SQL code with no valid :(...): internal parameters
|
|
result := @fStaticStatement;
|
|
result^.Prepare(DB.DB,SQL);
|
|
DB.Log.Log(sllSQL,'% is no prepared statement',SQL,self);
|
|
exit;
|
|
end;
|
|
DB.Log.Log(sllSQL,'% prepared as % with % param',[SQL,GenericSQL,maxParam],self);
|
|
result := fStatementCache.Prepare(GenericSQL);
|
|
// bind parameters
|
|
assert(sqlite3_bind_parameter_count(result^.Request)=maxParam);
|
|
for i := 0 to maxParam-1 do
|
|
case Types[i] of
|
|
sftDateTime, // date/time are stored as ISO-8601 TEXT in SQLite3
|
|
sftUTF8Text: result^.Bind(i+1,Values[i]);
|
|
sftBlob: result^.Bind(i+1,pointer(Values[i]),length(Values[i]));
|
|
sftInteger: result^.Bind(i+1,GetInt64(pointer(Values[i])));
|
|
sftFloat: result^.Bind(i+1,GetExtended(pointer(Values[i])));
|
|
end;
|
|
end;
|
|
|
|
function TSQLRestServerDB.EngineAdd(Table: TSQLRecordClass; const SentData: RawUTF8): integer;
|
|
var SQL: RawUTF8;
|
|
begin
|
|
if (self=nil) or (Table=nil) then begin
|
|
result := 0; // avoid GPF
|
|
exit;
|
|
end;
|
|
SQL := 'INSERT INTO '+Table.RecordProps.SQLTableName;
|
|
if trim(SentData)='' then
|
|
SQL := SQL+' DEFAULT VALUES;' else
|
|
SQL := SQL+GetJSONObjectAsSQL(SentData,false,true)+';';
|
|
if EngineExecute(SQL) then begin
|
|
result := DB.LastInsertRowID;
|
|
if Assigned(OnUpdateEvent) then
|
|
OnUpdateEvent(self,seAdd,Table,result);
|
|
end else
|
|
result := 0;
|
|
end;
|
|
|
|
procedure InternalRTreeIn(Context: TSQLite3FunctionContext;
|
|
argc: integer; var argv: TSQLite3ValueArray); {$ifndef USEFASTCALL}cdecl;{$endif}
|
|
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; aDB: TSQLDataBase;
|
|
aHandleUserAuthentication: boolean);
|
|
begin
|
|
fStatementCache.Init(aDB.DB);
|
|
aDB.UseCache := true; // we better use caching in this JSON oriented use
|
|
fDB := aDB;
|
|
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;
|
|
|
|
procedure TSQLRestServerDB.InitializeEngine;
|
|
var i: integer;
|
|
aModule: TSQLVirtualTableClass;
|
|
begin
|
|
for i := 0 to high(Model.TableProps) do
|
|
case Model.TableProps[i].Kind of
|
|
rRTree: // register all *_in() SQL functions
|
|
sqlite3_create_function_v2(DB.DB,
|
|
pointer(TSQLRecordRTree(Model.Tables[i]).RTreeSQLFunctionName),
|
|
2,SQLITE_ANY,Model.Tables[i],InternalRTreeIn,nil,nil,nil);
|
|
rCustomForcedID, rCustomAutoID: begin
|
|
aModule := Model.VirtualTableModule(Model.Tables[i]);
|
|
if aModule<>nil then begin
|
|
// perform registration via sqlite3_create_module_v2() API
|
|
if fRegisteredVirtualTableModule=nil then
|
|
SetLength(fRegisteredVirtualTableModule,length(Model.TableProps));
|
|
fRegisteredVirtualTableModule[i] := TSQLVirtualTableModuleServerDB.Create(aModule,self);
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
constructor TSQLRestServerDB.Create(aModel: TSQLModel; const aDBFileName: TFileName;
|
|
aHandleUserAuthentication: boolean; const aPassword: RawUTF8);
|
|
begin
|
|
fOwnedDB := TSQLDataBase.Create(aDBFileName,aPassword); // will be freed in Destroy
|
|
Create(aModel,fOwnedDB,aHandleUserAuthentication);
|
|
end;
|
|
|
|
procedure TSQLRestServerDB.CreateMissingTables(user_version: cardinal);
|
|
var t,f,nt,nf: integer;
|
|
TableNamesAtCreation, aFields: TRawUTF8DynArray;
|
|
TableJustCreated: TSQLFieldTables;
|
|
aSQL: RawUTF8;
|
|
begin
|
|
if DB.TransactionActive then
|
|
raise EBusinessLayerException.Create('CreateMissingTables: Transaction');
|
|
fDB.GetTableNames(TableNamesAtCreation);
|
|
nt := length(TableNamesAtCreation);
|
|
QuickSortRawUTF8(TableNamesAtCreation,nt,nil,StrIComp);
|
|
fillchar(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] do
|
|
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 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 high(Fields) do
|
|
if FieldType[f] in COPIABLE_FIELDS then
|
|
/// real database columns exist for Simple + Blob fields (not Many)
|
|
if FastFindPUTF8CharSorted(pointer(aFields),nf-1,pointer(FieldsName[f]),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,FieldsName[f]);
|
|
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
|
|
Model.Tables[t].InitializeTable(self,''); // '' 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.EngineDelete(Table: TSQLRecordClass; ID: integer): boolean;
|
|
begin
|
|
if (Table=nil) or (ID<=0) then begin
|
|
result := false; // avoid GPF
|
|
exit;
|
|
end;
|
|
if Assigned(OnUpdateEvent) then
|
|
OnUpdateEvent(self,seDelete,Table,ID); // notify BEFORE deletion
|
|
result := EngineExecuteFmt(
|
|
'DELETE FROM % WHERE RowID=:(%):;',[Table.RecordProps.SQLTableName,ID]);
|
|
end;
|
|
|
|
function TSQLRestServerDB.EngineDeleteWhere(Table: TSQLRecordClass;
|
|
const SQLWhere: RawUTF8; const IDs: TIntegerDynArray): boolean;
|
|
var i: integer;
|
|
begin
|
|
if (Table=nil) or (SQLWhere='') or (IDs=nil) then begin
|
|
result := false;
|
|
exit;
|
|
end;
|
|
if Assigned(OnUpdateEvent) then
|
|
for i := 0 to high(IDs) do
|
|
OnUpdateEvent(self,seDelete,Table,IDs[i]); // notify BEFORE deletion
|
|
result := EngineExecuteFmt(
|
|
'DELETE FROM % WHERE %',[Table.RecordProps.SQLTableName,SQLWhere]);
|
|
end;
|
|
|
|
destructor TSQLRestServerDB.Destroy;
|
|
var i: integer;
|
|
begin
|
|
try
|
|
for i := 0 to high(fRegisteredVirtualTableModule) do
|
|
if fRegisteredVirtualTableModule[i]<>nil then
|
|
// to avoid GPF in TSQLVirtualTable.Destroy e.g.
|
|
fRegisteredVirtualTableModule[i].fServer := nil;
|
|
inherited Destroy;
|
|
finally
|
|
try
|
|
fStatementCache.ReleaseAllDBStatements;
|
|
finally
|
|
fOwnedDB.Free;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function TSQLRestServerDB.EngineExecute(const aSQL: RawUTF8;
|
|
ValueInt: PInt64=nil; ValueUTF8: PRawUTF8=nil): boolean;
|
|
var Req: PSQLRequest;
|
|
begin
|
|
if (self<>nil) and (DB<>nil) then
|
|
try
|
|
DB.Lock(aSQL);
|
|
try
|
|
result := true;
|
|
if IdemPChar(Pointer(aSQL),'VACUUM') then
|
|
// VACUUM will fail if there are one or more active SQL statements
|
|
fStatementCache.ReleaseAllDBStatements;
|
|
Req := GetAndPrepareStatement(aSQL);
|
|
if Req<>nil then
|
|
with Req^ do
|
|
try
|
|
if (ValueInt=nil) and (ValueUTF8=nil) then
|
|
// default execution: loop through all rows
|
|
repeat until Step<>SQLITE_ROW else begin
|
|
// get one row, and retrieve value
|
|
if Step=SQLITE_ROW then
|
|
if ValueInt<>nil then
|
|
ValueInt^ := FieldInt(0) else
|
|
ValueUTF8^ := FieldUTF8(0) else
|
|
result := false;
|
|
end;
|
|
finally
|
|
if Req=@fStaticStatement then
|
|
Close;
|
|
end;
|
|
finally
|
|
DB.UnLock;
|
|
end;
|
|
except
|
|
on E: ESQLite3Exception do begin
|
|
{$ifdef WITHLOG}
|
|
DB.Log.Log(sllError,'% for %',[E,aSQL],self);
|
|
{$else}
|
|
LogToTextFile('TSQLRestServerDB.EngineExecute: '+RawUTF8(E.Message)+#13#10+aSQL);
|
|
{$endif}
|
|
result := false;
|
|
end;
|
|
end else
|
|
result := false;
|
|
end;
|
|
|
|
function TSQLRestServerDB.EngineExecuteFmt(SQLFormat: PUTF8Char;
|
|
const Args: array of const): boolean;
|
|
begin
|
|
result := EngineExecute(FormatUTF8(SQLFormat,Args));
|
|
end;
|
|
|
|
function TSQLRestServerDB.EngineExecute(const aSQL: RawUTF8; out Value: Int64): boolean;
|
|
begin
|
|
result:= EngineExecute(aSQL,@Value,nil);
|
|
end;
|
|
|
|
function TSQLRestServerDB.EngineExecute(const aSQL: RawUTF8; out Value: RawUTF8): boolean;
|
|
begin
|
|
result:= EngineExecute(aSQL,nil,@Value);
|
|
end;
|
|
|
|
function TSQLRestServerDB.EngineExecute(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
|
|
DB.Lock(''); // even if aSQL is SELECT, StoredProc may update data -> cache flush
|
|
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}
|
|
DB.Log.Log(sllError,'% for %',[E,aSQL],self);
|
|
{$else}
|
|
LogToTextFile('TSQLRestServerDB.EngineExecute Error: '+RawUTF8(E.Message)+#13#10+aSQL);
|
|
{$endif}
|
|
result := false;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function TSQLRestServerDB.EngineExecuteAll(const aSQL: RawUTF8): boolean;
|
|
begin
|
|
try
|
|
if IdemPChar(Pointer(aSQL),'VACUUM') then
|
|
// VACUUM will fail if there are one or more active SQL statements
|
|
fStatementCache.ReleaseAllDBStatements;
|
|
DB.ExecuteAll(aSQL); // Execute all statements (don't use fStatementCache[])
|
|
result := true;
|
|
except
|
|
on E: ESQLite3Exception do begin
|
|
{$ifdef WITHLOG}
|
|
DB.Log.Log(sllError,'% for %',[E,aSQL],self);
|
|
{$else}
|
|
LogToTextFile('TSQLRestServerDB.EngineExecuteAll Error: '+RawUTF8(E.Message)+#13#10+aSQL);
|
|
{$endif}
|
|
result := false;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function TSQLRestServerDB.EngineList(const SQL: RawUTF8; ForceAJAX: Boolean=false;
|
|
ReturnedRowCount: PPtrInt=nil): RawUTF8;
|
|
var Req: PSQLRequest;
|
|
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 // Execute request if was not got from cache
|
|
try
|
|
try
|
|
Req := GetAndPrepareStatement(SQL);
|
|
if Req<>nil then begin
|
|
MS := TRawByteStringStream.Create;
|
|
try
|
|
try
|
|
RowCount := Req^.Execute(0,'',MS,ForceAJAX or (not NoAJAXJSON));
|
|
if ReturnedRowCount<>nil then
|
|
ReturnedRowCount^ := RowCount;
|
|
result := MS.DataString;
|
|
finally
|
|
if Req=@fStaticStatement then
|
|
Req^.Close;
|
|
end;
|
|
finally
|
|
MS.Free;
|
|
end;
|
|
end;
|
|
except
|
|
on ESQLite3Exception do
|
|
result := '';
|
|
end;
|
|
finally
|
|
DB.UnLockJSON(result,RowCount);
|
|
end;
|
|
end;
|
|
if ReturnedRowCount<>nil then
|
|
ReturnedRowCount^ := RowCount;
|
|
end;
|
|
|
|
function TSQLRestServerDB.EngineRetrieve(TableModelIndex, ID: integer): RawUTF8;
|
|
var SQL: RawUTF8;
|
|
begin
|
|
if (self=nil) or (cardinal(TableModelIndex)>=cardinal(length(Model.TableProps))) then
|
|
exit;
|
|
with Model.TableProps[TableModelIndex] do
|
|
SQL := FormatUTF8('SELECT % FROM % WHERE RowID=:(%):;',
|
|
[SQLTableSimpleFields[true,false],SQLTableName,ID]);
|
|
result := EngineList(SQL,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.EngineRetrieveBlob(Table: TSQLRecordClass;
|
|
aID: integer; BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean;
|
|
var SQL: RawUTF8;
|
|
Req: PSQLRequest;
|
|
begin
|
|
result := false;
|
|
if (self=nil) or (DB=nil) or (aID<=0) or (Table=nil) or not BlobField^.IsBlob then
|
|
exit;
|
|
try
|
|
SQL := FormatUTF8('SELECT % FROM % WHERE RowID=?;',
|
|
[BlobField^.Name,Table.RecordProps.SQLTableName]);
|
|
DB.Lock(SQL); // UPDATE for a blob field -> no JSON cache flush, but UI refresh
|
|
try
|
|
Req := fStatementCache.Prepare(SQL);
|
|
with Req^ do
|
|
try
|
|
Bind(1,aID);
|
|
if (FieldCount=1) and (Step=SQLITE_ROW) then begin
|
|
BlobData := FieldBlob(0);
|
|
result := true;
|
|
end;
|
|
finally
|
|
if Req=@fStaticStatement then
|
|
Close;
|
|
end;
|
|
finally
|
|
DB.UnLock;
|
|
end;
|
|
except
|
|
on ESQLite3Exception do
|
|
result := false;
|
|
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.EngineUpdate(Table: TSQLRecordClass; ID: integer;
|
|
const SentData: RawUTF8): boolean;
|
|
begin
|
|
if (self=nil) or (Table=nil) or (ID<=0) then
|
|
result := false else begin
|
|
// this SQL statement use :(inlined params): for all values
|
|
result := EngineExecuteFmt('UPDATE % SET % WHERE RowID=:(%):;',
|
|
[Table.RecordProps.SQLTableName,GetJSONObjectAsSQL(SentData,true,true),ID]);
|
|
if Assigned(OnUpdateEvent) then
|
|
OnUpdateEvent(self,seUpdate,Table,ID);
|
|
end;
|
|
end;
|
|
|
|
function TSQLRestServerDB.EngineUpdateBlob(Table: TSQLRecordClass;
|
|
aID: integer; BlobField: PPropInfo;
|
|
const BlobData: TSQLRawBlob): boolean;
|
|
var SQL: RawUTF8;
|
|
begin
|
|
result := false;
|
|
if (self=nil) or (DB=nil) or (aID<=0) or (Table=nil) or not BlobField^.IsBlob then
|
|
exit;
|
|
try
|
|
SQL := FormatUTF8('UPDATE % SET %=? WHERE RowID=?;',
|
|
[Table.RecordProps.SQLTableName,BlobField^.Name]);
|
|
DB.Lock(SQL); // UPDATE for a blob field -> no JSON cache flush, but UI refresh
|
|
try
|
|
with fStatementCache.Prepare(SQL)^ do begin
|
|
Bind(1,pointer(BlobData),length(BlobData));
|
|
Bind(2,aID);
|
|
repeat
|
|
until Step<>SQLITE_ROW; // Execute all steps of the first statement
|
|
result := true;
|
|
end;
|
|
finally
|
|
DB.UnLock;
|
|
end;
|
|
except
|
|
on ESQLite3Exception do
|
|
result := false;
|
|
end;
|
|
end;
|
|
|
|
function TSQLRestServerDB.EngineUpdateField(Table: TSQLRecordClass;
|
|
const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean;
|
|
var Static: TSQLRestServerStatic;
|
|
WhereID: integer;
|
|
begin
|
|
result := false;
|
|
if (self=nil) or (Table=nil) then
|
|
exit;
|
|
Static := GetStaticDataServerOrVirtualTable(Table);
|
|
if Static<>nil then
|
|
result := Static.EngineUpdateField(Table,SetFieldName,SetValue,WhereFieldName,WhereValue) else
|
|
with Table.RecordProps do
|
|
if FieldIndexFromRawUTF8(SetFieldName)>=0 then begin
|
|
WhereID := 0;
|
|
if IsRowID(pointer(WhereFieldName)) then begin
|
|
WhereID := GetInteger(Pointer(WhereValue));
|
|
if (WhereID<=0) or not RecordCanBeUpdated(Table,WhereID,seUpdate) then
|
|
exit; // limitation: will only check for update from RowID
|
|
end else
|
|
if FieldIndexFromRawUTF8(WhereFieldName)<0 then
|
|
exit;
|
|
result := EngineExecuteFmt('UPDATE % SET %=:(%): WHERE %=:(%):',
|
|
[SQLTableName,SetFieldName,SetValue,WhereFieldName,WhereValue]);
|
|
if (WhereID>0) and Assigned(OnUpdateEvent) then
|
|
OnUpdateEvent(self,seUpdate,Table,WhereID);
|
|
end;
|
|
end;
|
|
|
|
procedure TSQLRestServerDB.Commit(SessionID: cardinal=1);
|
|
begin
|
|
inherited Commit(SessionID); // reset fTransactionActive + write all TSQLVirtualTableJSON
|
|
try
|
|
DB.Commit;
|
|
except
|
|
on ESQLite3Exception do
|
|
; // just catch exception
|
|
end;
|
|
end;
|
|
|
|
procedure TSQLRestServerDB.RollBack(SessionID: cardinal=1);
|
|
begin
|
|
inherited; // reset TSQLRestServerDB.fTransactionActive flag
|
|
try
|
|
DB.RollBack; // reset TSQLDataBase.RollBack
|
|
except
|
|
on ESQLite3Exception do
|
|
; // just catch exception
|
|
end;
|
|
end;
|
|
|
|
function TSQLRestServerDB.TransactionBegin(aTable: TSQLRecordClass; SessionID: cardinal=1): boolean;
|
|
begin
|
|
result := inherited TransactionBegin(aTable,SessionID);
|
|
if result then
|
|
// fTransactionActive flag was not already set
|
|
try
|
|
DB.TransactionBegin;
|
|
except
|
|
on ESQLite3Exception do
|
|
result := false;
|
|
end;
|
|
end;
|
|
|
|
function TSQLRestServerDB.Backup(Dest: TStream): boolean;
|
|
var Source: TFileStream;
|
|
Closed: boolean;
|
|
begin
|
|
result := false;
|
|
if (Self=nil) or (DB=nil) then
|
|
exit;
|
|
DB.Lock(''); // flush internal cache
|
|
try
|
|
try
|
|
// perform a VACCUM to recreate the database content
|
|
EngineExecuteAll('VACUUM');
|
|
Closed := false;
|
|
try
|
|
Closed := sqlite3_close(DB.DB)=SQLITE_OK;
|
|
// compress the database content file
|
|
Source := TFileStream.Create(DB.FileName,fmOpenRead or fmShareDenyNone);
|
|
try
|
|
Dest.CopyFrom(Source,0); // Count=0 for whole stream copy
|
|
result := true;
|
|
finally
|
|
Source.Free;
|
|
end;
|
|
finally
|
|
if Closed then begin
|
|
DB.DBOpen; // reopen the database if was previously closed
|
|
InitializeEngine; // register functions and modules
|
|
end;
|
|
end;
|
|
finally
|
|
DB.UnLock;
|
|
end;
|
|
except
|
|
on E: Exception do
|
|
result := false;
|
|
end;
|
|
end;
|
|
|
|
function TSQLRestServerDB.BackupGZ(const DestFileName: TFileName; CompressionLevel: integer): boolean;
|
|
var D,Z: TStream;
|
|
begin
|
|
try
|
|
D := TFileStream.Create(DestFileName,fmCreate);
|
|
try
|
|
Z := TSynZipCompressor.Create(D,CompressionLevel,szcfGZ);
|
|
try
|
|
result := Backup(Z);
|
|
finally
|
|
Z.Free;
|
|
end;
|
|
finally
|
|
D.Free;
|
|
end;
|
|
except
|
|
result := false;
|
|
end;
|
|
end;
|
|
|
|
function TSQLRestServerDB.Restore(const ContentToRestore: RawByteString): boolean;
|
|
var BackupFileName: TFileName;
|
|
begin
|
|
result := false;
|
|
if (Self=nil) or (DB=nil) or
|
|
not IdemPChar(pointer(ContentToRestore),'SQLITE FORMAT 3') then
|
|
exit; // invalid restore content
|
|
DB.Lock(''); // flush internal cache
|
|
try
|
|
try
|
|
fStatementCache.ReleaseAllDBStatements;
|
|
if sqlite3_close(DB.DB)<>SQLITE_OK then
|
|
exit; // impossible to close DB.FileName (some statement may be opened)
|
|
BackupFileName := ChangeFileExt(DB.FileName,'.bak');
|
|
DeleteFile(BackupFileName);
|
|
try
|
|
if MoveFile(pointer(DB.FileName),pointer(BackupFileName)) then
|
|
result := FileFromString(ContentToRestore,DB.FileName,true) and
|
|
(StringFromFile(DB.FileName)=ContentToRestore) and
|
|
(DB.DBOpen=SQLITE_OK);
|
|
finally
|
|
if result then
|
|
DeleteFile(BackupFileName) else begin
|
|
// on error, restore previous db file
|
|
DeleteFile(DB.FileName);
|
|
MoveFile(pointer(BackupFileName),pointer(DB.FileName));
|
|
DB.DBOpen; // always reopen the database
|
|
end;
|
|
InitializeEngine; // register functions and modules
|
|
end;
|
|
finally
|
|
DB.UnLock;
|
|
end;
|
|
except
|
|
on E: Exception do
|
|
result := false;
|
|
end;
|
|
end;
|
|
|
|
procedure TSQLRestServerDB.FlushInternalDBCache;
|
|
begin
|
|
inherited;
|
|
if DB<>nil then
|
|
DB.CacheFlush;
|
|
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
|
|
fServer := aServerClass.Create(aServerModel,aDB,aHandleUserAuthentication);
|
|
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);
|
|
begin
|
|
fOwnedDB := TSQLDatabase.Create(aDBFileName,aPassword);
|
|
Create(aClientModel, aServerModel,fOwnedDB,aServerClass,aHandleUserAuthentication);
|
|
end;
|
|
|
|
destructor TSQLRestClientDB.Destroy;
|
|
var M: TSQLModel;
|
|
begin
|
|
try
|
|
SessionClose;
|
|
finally
|
|
M := fServer.Model;
|
|
if M.Owner<>nil then
|
|
M := nil; // free associated model if it's owned by nobody
|
|
try
|
|
FreeAndNil(fServer);
|
|
finally
|
|
M.Free;
|
|
fOwnedDB.Free;
|
|
inherited;
|
|
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 := SQLFromSelectWhere(Tables,SQLSelect,SQLWhere);
|
|
if n=1 then
|
|
// InternalListJSON will handle both static and DB tables
|
|
result := fServer.InternalListJSON(TSQLRecordClass(Tables[0]),aSQL) else
|
|
// we access localy the DB -> TSQLTableDB handle Tables parameter
|
|
result := TSQLTableDB.Create(fServer.DB,
|
|
RecordClassesToClasses(Tables),aSQL,not fServer.NoAJAXJSON);
|
|
if fServer.DB.InternalState<>nil then
|
|
result.InternalState := fServer.DB.InternalState^;
|
|
except
|
|
on ESQLite3Exception do
|
|
result := nil;
|
|
end;
|
|
end;
|
|
|
|
function TSQLRestClientDB.InternalURI(const url, method: RawUTF8;
|
|
Resp, Head, SendData: PRawUTF8): Int64Rec;
|
|
var R,H,S: RawUTF8; // temp '' string to be used when no PString is provided
|
|
begin
|
|
if Resp=nil then Resp := @R;
|
|
if Head=nil then Head := @H;
|
|
if SendData=nil then SendData := @S;
|
|
result := fServer.URI(url,method,SendData^,Resp^,Head^,@FULL_ACCESS_RIGHTS);
|
|
if (result.Hi=0) and (fServer.DB.InternalState<>nil) then
|
|
result.Hi := fServer.DB.InternalState^; // manual update if necessary
|
|
end;
|
|
|
|
{ TSQLVirtualTableModuleSQLite3 }
|
|
|
|
function VarDataToContext(Context: TSQLite3FunctionContext; const Res: TVarData): boolean;
|
|
begin
|
|
case Res.VType of
|
|
varNull: sqlite3_result_null(Context);
|
|
varInt64: sqlite3_result_int64(Context,Res.VInt64);
|
|
varDouble: sqlite3_result_double(Context,Res.VDouble);
|
|
varString: sqlite3_result_text(Context,Res.VString);
|
|
varAny: sqlite3_result_blob(Context,Res.VAny,Res.VLongs[0]);
|
|
else begin
|
|
result := false; // not handled type
|
|
exit;
|
|
end;
|
|
end;
|
|
result := true;
|
|
end;
|
|
|
|
procedure VarDataFromValue(Value: TSQLite3Value; var Res: TVarData);
|
|
begin
|
|
case sqlite3_value_type(Value) of
|
|
SQLITE_NULL:
|
|
Res.VType := varNull;
|
|
SQLITE_INTEGER: begin
|
|
Res.VType := varInt64;
|
|
Res.VInt64 := sqlite3_value_int64(Value);
|
|
end;
|
|
SQLITE_FLOAT: begin
|
|
Res.VType := varDouble;
|
|
Res.VDouble := sqlite3_value_double(Value);
|
|
end;
|
|
SQLITE_TEXT: begin
|
|
Res.VType := varString;
|
|
Res.VString := sqlite3_value_text(Value);
|
|
end;
|
|
SQLITE_BLOB: begin
|
|
Res.VType := varAny;
|
|
Res.VLongs[0] := sqlite3_value_bytes(Value);
|
|
Res.VAny := sqlite3_value_blob(Value);
|
|
end;
|
|
else
|
|
Res.VType := varUnknown;
|
|
end;
|
|
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,':memory:') 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;
|
|
|
|
procedure ExceptionToSqlite3Err(E: Exception; var pzErr: PAnsiChar);
|
|
var U: RawUTF8;
|
|
begin
|
|
U := StringToUTF8(E.Message);
|
|
pzErr := sqlite3_malloc(length(U));
|
|
move(pointer(U)^,pzErr^,length(U));
|
|
end;
|
|
|
|
function vt_Create(DB: TSQLite3DB; pAux: Pointer;
|
|
argc: Integer; const argv: PPUTF8CharArray;
|
|
var ppVTab: PSQLite3VTab; var pzErr: PAnsiChar): Integer; {$ifndef USEFASTCALL}cdecl;{$endif}
|
|
var Module: TSQLVirtualTableModuleSQLite3 absolute pAux;
|
|
Table: TSQLVirtualTable;
|
|
begin
|
|
if (Module=nil) or (Module.DB.DB<>DB) or
|
|
(StrIComp(pointer(Module.ModuleName),argv[0])<>0) then begin
|
|
result := SQLITE_ERROR;
|
|
exit;
|
|
end;
|
|
ppVTab := sqlite3_malloc(sizeof(TSQLite3VTab));
|
|
if ppVTab=nil then begin
|
|
result := SQLITE_NOMEM;
|
|
exit;
|
|
end;
|
|
ppVTab^.zErrMsg := nil; // to avoid GPF
|
|
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;
|
|
result := sqlite3_declare_vtab(DB,pointer(Table.Structure));
|
|
if result<>SQLITE_OK then begin
|
|
Table.Free;
|
|
sqlite3_free(ppVTab);
|
|
result := SQLITE_ERROR;
|
|
end else
|
|
ppVTab^.pInstance := Table;
|
|
end;
|
|
|
|
function vt_Disconnect(pVTab: PSQLite3VTab): Integer; {$ifndef USEFASTCALL}cdecl;{$endif}
|
|
begin
|
|
TSQLVirtualTable(pvTab^.pInstance).Free;
|
|
sqlite3_free(pVTab);
|
|
result := SQLITE_OK;
|
|
end;
|
|
|
|
function vt_Destroy(pVTab: PSQLite3VTab): Integer; {$ifndef USEFASTCALL}cdecl;{$endif}
|
|
begin
|
|
if TSQLVirtualTable(pvTab^.pInstance).Drop then
|
|
result := SQLITE_OK else
|
|
result := SQLITE_ERROR;
|
|
vt_Disconnect(pVTab); // release memory
|
|
end;
|
|
|
|
function vt_BestIndex(var pVTab: TSQLite3VTab; var pInfo: TSQLite3IndexInfo): Integer;
|
|
{$ifndef USEFASTCALL}cdecl;{$endif}
|
|
var Prepared: PSQLVirtualTablePrepared;
|
|
i, n: Integer;
|
|
begin
|
|
result := SQLITE_ERROR;
|
|
if (cardinal(pInfo.nOrderBy)>MAX_SQLFIELDS) or
|
|
(cardinal(pInfo.nConstraint)>MAX_SQLFIELDS) then
|
|
exit; // avoid buffer overflow
|
|
Prepared := sqlite3_malloc(sizeof(TSQLVirtualTablePrepared));
|
|
try
|
|
// encode the incoming parameters into Prepared^ record
|
|
fillchar(Prepared^,sizeof(Prepared^),0);
|
|
Prepared^.WhereCount := pInfo.nConstraint;
|
|
for i := 0 to pInfo.nConstraint-1 do
|
|
with Prepared^.Where[i], pInfo.aConstraint^[i] do
|
|
if usable then begin
|
|
Column := iColumn;
|
|
case op of
|
|
SQLITE_INDEX_CONSTRAINT_EQ: Operator := soEqualTo;
|
|
SQLITE_INDEX_CONSTRAINT_GT: Operator := soGreaterThan;
|
|
SQLITE_INDEX_CONSTRAINT_LE: Operator := soLessThanOrEqualTo;
|
|
SQLITE_INDEX_CONSTRAINT_LT: Operator := soLessThan;
|
|
SQLITE_INDEX_CONSTRAINT_GE: Operator := soGreaterThanOrEqualTo;
|
|
SQLITE_INDEX_CONSTRAINT_MATCH: Operator := soBeginWith;
|
|
else exit; // invalid parameter
|
|
end;
|
|
end else
|
|
Column := VIRTUAL_TABLE_IGNORE_COLUMN;
|
|
assert(sizeof(TSQLVirtualTablePreparedOrderBy)=sizeof(TSQLite3IndexOrderBy));
|
|
if pInfo.nOrderBy>0 then begin
|
|
Prepared^.OrderByCount := pInfo.nOrderBy;
|
|
Move(pInfo.aOrderBy^[0],Prepared^.OrderBy[0],pInfo.nOrderBy*sizeof(Prepared^.OrderBy[0]));
|
|
end;
|
|
// perform the index query
|
|
if not TSQLVirtualTable(pvTab.pInstance).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<>varEmpty then begin
|
|
if i<>n then // expression needed for Search() method to be moved at [n]
|
|
move(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()
|
|
pInfo.orderByConsumed := integer(Prepared^.OmitOrderBy);
|
|
pInfo.estimatedCost := Prepared^.EstimatedCost;
|
|
pInfo.idxStr := pointer(Prepared);
|
|
pInfo.needToFreeIdxStr := 1; // will do sqlite3_free(idxStr) when needed
|
|
result := SQLITE_OK;
|
|
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;
|
|
{$ifndef USEFASTCALL}cdecl;{$endif}
|
|
var Prepared: PSQLVirtualTablePrepared absolute idxStr; // idxNum is not used
|
|
i: integer;
|
|
begin
|
|
result := SQLITE_ERROR;
|
|
if Prepared^.WhereCount<>argc then
|
|
exit; // invalid prepared array
|
|
for i := 0 to argc-1 do
|
|
VarDataFromValue(argv[i],Prepared^.Where[i].Value);
|
|
if TSQLVirtualTableCursor(pVtabCursor.pInstance).Search(Prepared^) then
|
|
result := SQLITE_OK;
|
|
end;
|
|
|
|
function vt_Open(var pVTab: TSQLite3VTab; var ppCursor: PSQLite3VTabCursor): Integer;
|
|
{$ifndef USEFASTCALL}cdecl;{$endif}
|
|
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
|
|
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;
|
|
{$ifndef USEFASTCALL}cdecl;{$endif}
|
|
begin
|
|
TSQLVirtualTableCursor(pVtabCursor^.pInstance).Free;
|
|
sqlite3_free(pVtabCursor);
|
|
result := SQLITE_OK;
|
|
end;
|
|
|
|
function vt_next(var pVtabCursor: TSQLite3VTabCursor): Integer;
|
|
{$ifndef USEFASTCALL}cdecl;{$endif}
|
|
begin
|
|
if TSQLVirtualTableCursor(pVtabCursor.pInstance).Next then
|
|
result := SQLITE_OK else
|
|
result := SQLITE_ERROR;
|
|
end;
|
|
|
|
function vt_Eof(var pVtabCursor: TSQLite3VTabCursor): Integer;
|
|
{$ifndef USEFASTCALL}cdecl;{$endif}
|
|
begin
|
|
result := integer(not TSQLVirtualTableCursor(pVtabCursor.pInstance).HasData);
|
|
end;
|
|
|
|
function vt_Column(var pVtabCursor: TSQLite3VTabCursor; sContext: TSQLite3FunctionContext;
|
|
N: Integer): Integer; {$ifndef USEFASTCALL}cdecl;{$endif}
|
|
var Res: TVarData;
|
|
begin
|
|
if (N>=0) and TSQLVirtualTableCursor(pVtabCursor.pInstance).Column(N,Res) and
|
|
VarDataToContext(sContext,Res) then
|
|
result := SQLITE_OK else
|
|
result := SQLITE_ERROR;
|
|
end;
|
|
|
|
function vt_Rowid(var pVtabCursor: TSQLite3VTabCursor; var pRowid: Int64): Integer;
|
|
{$ifndef USEFASTCALL}cdecl;{$endif}
|
|
var Res: TVarData;
|
|
begin
|
|
result := SQLITE_ERROR;
|
|
if TSQLVirtualTableCursor(pVtabCursor.pInstance).Column(-1,Res) then begin
|
|
case Res.VType of
|
|
varInt64: pRowID := Res.VInt64;
|
|
varCurrency, varDouble: pRowID := trunc(Res.VDouble);
|
|
varString: pRowID := GetInt64(Res.VPointer);
|
|
else exit;
|
|
end;
|
|
result := SQLITE_OK;
|
|
end;
|
|
end;
|
|
|
|
function vt_Update(var pVTab: TSQLite3VTab;
|
|
nArg: Integer; var ppArg: TSQLite3ValueArray;
|
|
var pRowid: Int64): Integer; {$ifndef USEFASTCALL}cdecl;{$endif}
|
|
var Values: TVarDataDynArray;
|
|
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 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
|
|
VarDataFromValue(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;
|
|
end;
|
|
|
|
function vt_Begin(var pVTab: TSQLite3VTab): Integer; {$ifndef USEFASTCALL}cdecl;{$endif}
|
|
begin
|
|
if TSQLVirtualTable(pvTab.pInstance).Transaction(vttBegin,0) then
|
|
result := SQLITE_OK else
|
|
result := SQLITE_ERROR;
|
|
end;
|
|
|
|
function vt_Commit(var pVTab: TSQLite3VTab): Integer; {$ifndef USEFASTCALL}cdecl;{$endif}
|
|
begin
|
|
if TSQLVirtualTable(pvTab.pInstance).Transaction(vttCommit,0) then
|
|
result := SQLITE_OK else
|
|
result := SQLITE_ERROR;
|
|
end;
|
|
|
|
function vt_RollBack(var pVTab: TSQLite3VTab): Integer; {$ifndef USEFASTCALL}cdecl;{$endif}
|
|
begin
|
|
if TSQLVirtualTable(pvTab.pInstance).Transaction(vttRollBack,0) then
|
|
result := SQLITE_OK else
|
|
result := SQLITE_ERROR;
|
|
end;
|
|
|
|
function vt_Sync(var pVTab: TSQLite3VTab): Integer; {$ifndef USEFASTCALL}cdecl;{$endif}
|
|
begin
|
|
if TSQLVirtualTable(pvTab.pInstance).Transaction(vttSync,0) then
|
|
result := SQLITE_OK else
|
|
result := SQLITE_ERROR;
|
|
end;
|
|
|
|
function vt_SavePoint(var pVTab: TSQLite3VTab; iSavepoint: integer): Integer; {$ifndef USEFASTCALL}cdecl;{$endif}
|
|
begin
|
|
if TSQLVirtualTable(pvTab.pInstance).Transaction(vttSavePoint,iSavePoint) then
|
|
result := SQLITE_OK else
|
|
result := SQLITE_ERROR;
|
|
end;
|
|
|
|
function vt_Release(var pVTab: TSQLite3VTab; iSavepoint: integer): Integer; {$ifndef USEFASTCALL}cdecl;{$endif}
|
|
begin
|
|
if TSQLVirtualTable(pvTab.pInstance).Transaction(vttRelease,iSavePoint) then
|
|
result := SQLITE_OK else
|
|
result := SQLITE_ERROR;
|
|
end;
|
|
|
|
function vt_RollBackTo(var pVTab: TSQLite3VTab; iSavepoint: integer): Integer; {$ifndef USEFASTCALL}cdecl;{$endif}
|
|
begin
|
|
if TSQLVirtualTable(pvTab.pInstance).Transaction(vttRollBackTo,iSavePoint) then
|
|
result := SQLITE_OK else
|
|
result := SQLITE_ERROR;
|
|
end;
|
|
|
|
function vt_Rename(var pVTab: TSQLite3VTab; const zNew: PAnsiChar): Integer;
|
|
{$ifndef USEFASTCALL}cdecl;{$endif}
|
|
begin
|
|
if TSQLVirtualTable(pvTab.pInstance).Rename(RawUTF8(zNew)) then
|
|
result := SQLITE_OK else
|
|
result := SQLITE_ERROR;
|
|
end;
|
|
|
|
procedure TSQLVirtualTableModuleSQLite3.SetDB(aDB: TSQLDataBase);
|
|
begin
|
|
fDB := aDB;
|
|
fillchar(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(fDB.DB,sqlite3_create_module_v2(fDB.DB,pointer(fModuleName),fModule,
|
|
self,sqlite3InternalFreeObject));
|
|
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 Create(aClass,aServer);
|
|
DB := TSQLRestServerDB(aServer).DB; // SetDB setter will do the work
|
|
end;
|
|
|
|
|
|
{ ************ Unit-Testing classes and functions }
|
|
|
|
{ TTestSQLite3Engine }
|
|
|
|
destructor TTestSQLite3Engine.Destroy;
|
|
begin
|
|
Demo.Free;
|
|
inherited;
|
|
end;
|
|
|
|
procedure InternalSQLFunctionCharIndex(Context: TSQLite3FunctionContext;
|
|
argc: integer; var argv: TSQLite3ValueArray); cdecl;
|
|
var StartPos: integer;
|
|
begin
|
|
case argc of
|
|
2: StartPos := 1;
|
|
3: begin
|
|
StartPos := sqlite3_value_int64(argv[2]);
|
|
if StartPos<=0 then
|
|
StartPos := 1;
|
|
end;
|
|
else begin
|
|
ErrorWrongNumberOfArgs(Context);
|
|
exit;
|
|
end;
|
|
end;
|
|
if (sqlite3_value_type(argv[0])=SQLITE_NULL) or
|
|
(sqlite3_value_type(argv[1])=SQLITE_NULL) then
|
|
sqlite3_result_int64(Context,0) else
|
|
sqlite3_result_int64(Context,SynCommons.PosEx(
|
|
sqlite3_value_text(argv[0]),sqlite3_value_text(argv[1]),StartPos));
|
|
end;
|
|
|
|
{$ifdef UNICODE}
|
|
{$WARNINGS OFF} // don't care about implicit string cast in tests
|
|
{$endif}
|
|
|
|
const // BLOBs are stored as array of byte to avoid any charset conflict
|
|
BlobDali: array[0..3] of byte = (97,233,224,231);
|
|
BlobMonet: array[0..13] of byte = (224,233,231,ord('d'),ord('s'),ord('j'),
|
|
ord('d'),ord('s'),ord('B'),ord('L'),ord('O'),ord('B'),ord('2'),ord('3'));
|
|
|
|
procedure TTestSQLite3Engine.DatabaseDirectAccess;
|
|
procedure InsertData(n: integer);
|
|
var i: integer;
|
|
s: string;
|
|
ins: RawUTF8;
|
|
R: TSQLRequest;
|
|
begin
|
|
// this code is a lot faster than sqlite3 itself, even if it use Utf8 encoding:
|
|
// -> we test the engine speed, not the test routines speed :)
|
|
ins := 'INSERT INTO People (FirstName,LastName,Data,YearOfBirth,YearOfDeath) VALUES (''';
|
|
for i := 1 to n do begin
|
|
str(i,s);
|
|
// we put some accents in order to test UTF-8 encoding
|
|
R.Prepare(Demo.DB,ins+'Salvador'+RawUTF8(s)+''', ''Dali'', ?, 1904, 1989);');
|
|
R.Bind(1,@BlobDali,4); // Bind Blob
|
|
R.Execute;
|
|
Demo.Execute(ins+StringToUtf8('Samuel Finley Breese'+s+''', ''Morse'', ''a'+chr(233)+chr(224)+chr(231)+''', 1791, 1872);'));
|
|
Demo.Execute(ins+StringToUtf8('Sergei'+s+''', ''Rachmaninoff'', '''+chr(233)+'z'+chr(231)+'b'', 1873, 1943);'));
|
|
Demo.Execute(ins+StringToUtf8('Alexandre'+s+''', ''Dumas'', '''+chr(233)+chr(231)+'b'', 1802, 1870);'));
|
|
Demo.Execute(ins+StringToUtf8('Franz'+s+''', ''Schubert'', '''+chr(233)+chr(224)+chr(231)+'a'', 1797, 1828);'));
|
|
Demo.Execute(ins+StringToUtf8('Leonardo'+s+''', ''da Vin'+chr(231)+'i'', ''@'+chr(231)+'b'', 1452, 1519);'));
|
|
Demo.Execute(ins+StringToUtf8('Aldous Leonard'+s+''', ''Huxley'', '''+chr(233)+chr(224)+''', 1894, 1963);'));
|
|
R.Prepare(Demo.DB,ins+StringToUtf8('Claud'+chr(232)+s+#10#7''', ''M'+chr(244)+'net'', ?, 1840, 1926);'));
|
|
R.Bind(1,@BlobMonet,sizeof(BlobMonet)); // Bind Blob
|
|
R.Execute;
|
|
Demo.Execute(ins+StringToUtf8('Albert'+s+''', ''Einstein'', '''+chr(233)+chr(231)+'p'', 1879, 1955);'));
|
|
Demo.Execute(ins+StringToUtf8('Johannes'+s+''', ''Gutenberg'', '''+chr(234)+'mls'', 1400, 1468);'));
|
|
Demo.Execute(ins+StringToUtf8('Jane'+s+''', ''Aust'+chr(232)+'n'', '''+chr(231)+chr(224)+chr(231)+'m'', 1775, 1817);'));
|
|
end;
|
|
end;
|
|
const
|
|
ReqAnsi: WinAnsiString =
|
|
'SELECT * FROM People WHERE LastName=''M'+chr(244)+'net'' ORDER BY FirstName;';
|
|
SoundexValues: array[0..5] of RawUTF8 =
|
|
('bonjour','bonchour','Bnjr','mohammad','mohhhammeeet','bonjourtr'+chr(232)+'slongmotquid'+chr(233)+'passe');
|
|
var Names: TRawUTF8DynArray;
|
|
i1,i2: integer;
|
|
Res: Int64;
|
|
s: RawUTF8;
|
|
begin
|
|
if PosEx(RawUTF8('SQlite3 engine'),Owner.CustomVersions,1)=0 then
|
|
Owner.CustomVersions := Owner.CustomVersions+#13#10'SQlite3 engine used: '+
|
|
sqlite3_libversion;
|
|
IsMemory := InheritsFrom(TTestMemoryBased);
|
|
if IsMemory then
|
|
TempFileName := ':memory:' else begin
|
|
TempFileName := 'test.db3';
|
|
DeleteFile(TempFileName); // use a temporary file
|
|
CreateSQLEncryptTable('password1');
|
|
end;
|
|
Demo := TSQLDataBase.Create(TempFileName);
|
|
Demo.Synchronous := smOff;
|
|
for i1 := 0 to 100 do
|
|
for i2 := 1 to 100 do begin
|
|
s := FormatUTF8('SELECT MOD(%,%);',[i1,i2]);
|
|
Demo.Execute(s,res);
|
|
Check(res=i1 mod i2,s);
|
|
end;
|
|
for i1 := 0 to high(SoundexValues) do begin
|
|
s := FormatUTF8('SELECT SoundEx("%");',[SoundexValues[i1]]);
|
|
Demo.Execute(s,res);
|
|
Check(res=SoundExUTF8(pointer(SoundexValues[i1])),s);
|
|
end;
|
|
for i1 := 0 to high(SoundexValues) do begin
|
|
s := FormatUTF8('SELECT SoundExFr("%");',[SoundexValues[i1]]);
|
|
Demo.Execute(s,res);
|
|
Check(res=SoundExUTF8(pointer(SoundexValues[i1]),nil,sndxFrench),s);
|
|
end;
|
|
for i1 := 0 to high(SoundexValues) do begin
|
|
s := FormatUTF8('SELECT SoundExEs("%");',[SoundexValues[i1]]);
|
|
Demo.Execute(s,res);
|
|
Check(res=SoundExUTF8(pointer(SoundexValues[i1]),nil,sndxSpanish),s);
|
|
end;
|
|
Demo.RegisterSQLFunction(InternalSQLFunctionCharIndex,2,'CharIndex');
|
|
Demo.RegisterSQLFunction(InternalSQLFunctionCharIndex,3,'CharIndex');
|
|
for i1 := 0 to high(SoundexValues) do begin
|
|
s := FormatUTF8('SELECT CharIndex("o","%");',[SoundexValues[i1]]);
|
|
Demo.Execute(s,res);
|
|
Check(res=PosEx('o',SoundexValues[i1]),s);
|
|
s := FormatUTF8('SELECT CharIndex("o","%",5);',[SoundexValues[i1]]);
|
|
Demo.Execute(s,res);
|
|
Check(res=PosEx('o',SoundexValues[i1],5),s);
|
|
end;
|
|
Demo.UseCache := true; // use the cache for the JSON requests
|
|
Demo.WALMode := InheritsFrom(TTestFileBasedWAL); // test Write-Ahead Logging
|
|
Check(Demo.WALMode=InheritsFrom(TTestFileBasedWAL));
|
|
Demo.Execute(
|
|
' CREATE TABLE IF NOT EXISTS People (' +
|
|
' ID INTEGER PRIMARY KEY,'+
|
|
' FirstName TEXT COLLATE SYSTEMNOCASE,' +
|
|
' LastName TEXT,' +
|
|
' Data BLOB,'+
|
|
' YearOfBirth INTEGER,' +
|
|
' YearOfDeath INTEGER); ');
|
|
// Inserting data 1x without transaction ');
|
|
InsertData(1);
|
|
{ Insert some sample data - now with transaction. Multiple records are
|
|
inserted and not yet commited until the transaction is finally ended.
|
|
This single transaction is very fast compared to multiple individual
|
|
transactions. It is even faster than other database engines. }
|
|
Demo.TransactionBegin;
|
|
InsertData(1000);
|
|
Demo.Commit;
|
|
Req := WinAnsiToUtf8(ReqAnsi);
|
|
Check(Utf8ToWinAnsi(Req)=ReqAnsi,'WinAnsiToUtf8/Utf8ToWinAnsi');
|
|
JS := Demo.ExecuteJSON(Req); // get result in JSON format
|
|
FileFromString(JS,'Test1.json');
|
|
Check(Hash32(JS)=$2D6A1467,'Expected ExecuteJSON result not retrieved');
|
|
if not IsMemory then begin // check file encryption password change
|
|
FreeAndNil(Demo); // if any exception occurs in Create(), Demo.Free is OK
|
|
ChangeSQLEncryptTablePassWord(TempFileName,'password1','');
|
|
ChangeSQLEncryptTablePassWord(TempFileName,'','NewPass');
|
|
CreateSQLEncryptTable('NewPass');
|
|
Demo := TSQLDataBase.Create(TempFileName); // reuse the temporary file
|
|
Demo.Synchronous := smOff;
|
|
Demo.UseCache := true; // use the cache for the JSON requests
|
|
Demo.WALMode := InheritsFrom(TTestFileBasedWAL); // test Write-Ahead Logging
|
|
Check(Demo.WALMode=InheritsFrom(TTestFileBasedWAL));
|
|
Check(Hash32(Demo.ExecuteJSON(Req))=$2D6A1467,'ExecuteJSON crypted');
|
|
end;
|
|
Demo.GetTableNames(Names);
|
|
Check(length(Names)=1);
|
|
Check(Names[0]='People');
|
|
Demo.Execute('SELECT Concat(FirstName," and ") FROM People WHERE LastName="Einstein"',s);
|
|
Check(Hash32(s)=$68A74D8E,'Albert1 and Albert1 and Albert2 and Albert3 and ...');
|
|
end;
|
|
|
|
procedure TTestSQLite3Engine.VirtualTableDirectAccess;
|
|
const
|
|
LOG1: RawUTF8 = 'D:\Dev\lib\SQLite3\exe\TestSQL3.exe 1.2.3.4 (2011-04-07)'#13#10+
|
|
'Host=MyPC User=MySelf CPU=2*0-15-1027 OS=2.3=5.1.2600 Wow64=0 Freq=3579545'#13#10+
|
|
'TSynLog 1.13 LVCL 2011-04-07 12:04:09'#13#10#13#10+
|
|
'20110407 12040904 debug {"TObjectList(00AF8D00)":["TObjectList(00AF8D20)",'+
|
|
'"TObjectList(00AF8D60)","TFileVersion(00ADC0B0)","TSynMapFile(00ACC990)"]}';
|
|
var Res: Int64;
|
|
s,s2,s3: RawUTF8;
|
|
n: PtrInt;
|
|
begin
|
|
// register the Log virtual table module to this connection
|
|
RegisterVirtualTableModule(TSQLVirtualTableLog,Demo);
|
|
// test Log virtual table module
|
|
FileFromString(LOG1,'temptest.log');
|
|
Demo.Execute('CREATE VIRTUAL TABLE test USING log(temptest.log);');
|
|
Demo.Execute('select count(*) from test',Res);
|
|
Check(Res=1);
|
|
n := 0;
|
|
s := Demo.ExecuteJSON('select * from test',False,@n);
|
|
Check(s<>'');
|
|
Check(n=Res);
|
|
s2 := Demo.ExecuteJSON('select * from test where rowid=2',False,@n);
|
|
Check(s2='{"fieldCount":3,"values":["DateTime","Level","Content"]}'#$A);
|
|
Check(n=0);
|
|
s2 := Demo.ExecuteJSON('select * from test where rowid=1',False,@n);
|
|
Check(s2<>'');
|
|
Check(s=s2);
|
|
Check(n=1);
|
|
n := 0;
|
|
s3 := Demo.ExecuteJSON('select * from test where level=2',False,@n);
|
|
Check(n=1);
|
|
Check(s3='{"fieldCount":3,"values":["DateTime","Level","Content",40640.5028819444,'+
|
|
'2,"20110407 12040904 debug {\"TObjectList(00AF8D00)\":[\"TObjectList(00AF8D20)\",'+
|
|
'\"TObjectList(00AF8D60)\",\"TFileVersion(00ADC0B0)\",\"TSynMapFile(00ACC990)\"]}"]}'#$A);
|
|
s3 := Demo.ExecuteJSON('select * from test where level=3',False,@n);
|
|
Check(s3='{"fieldCount":3,"values":["DateTime","Level","Content"]}'#$A);
|
|
Check(n=0);
|
|
end;
|
|
|
|
type
|
|
TFV = packed record
|
|
Major, Minor, Release, Build: integer;
|
|
Main, Detailed: string;
|
|
end;
|
|
TFVs = array of TFV;
|
|
{$ifndef LVCL}
|
|
TCollTest = class(TCollectionItem)
|
|
private
|
|
FLength: Integer;
|
|
FColor: Integer;
|
|
FName: RawUTF8;
|
|
published
|
|
property Color: Integer read FColor write FColor;
|
|
property Length: Integer read FLength write FLength;
|
|
property Name: RawUTF8 read FName write FName;
|
|
end;
|
|
TCollTests = class(TCollection)
|
|
private
|
|
function GetCollItem(Index: Integer): TCollTest;
|
|
public
|
|
function Add: TCollTest;
|
|
property Item[Index: Integer]: TCollTest read GetCollItem; default;
|
|
end;
|
|
|
|
TCollTst = class(TPersistent)
|
|
private
|
|
fColl: TCollTests;
|
|
fTCollTest: TCollTest;
|
|
fStr: TStringList;
|
|
public
|
|
constructor Create;
|
|
destructor Destroy; override;
|
|
published
|
|
property One: TCollTest read fTCollTest write fTCollTest;
|
|
property Coll: TCollTests read fColl write fColl;
|
|
property Str: TStringList read fStr write fStr;
|
|
end;
|
|
{$endif}
|
|
{$ifdef INCLUDE_FTS3}
|
|
TSQLFTSTest = class(TSQLRecordFTS3)
|
|
private
|
|
fSubject: RawUTF8;
|
|
fBody: RawUTF8;
|
|
published
|
|
property Subject: RawUTF8 read fSubject write fSubject;
|
|
property Body: RawUTF8 read fBody write fBody;
|
|
end;
|
|
{$endif}
|
|
TSQLASource = class;
|
|
TSQLADest = class;
|
|
TSQLADests = class(TSQLRecordMany)
|
|
private
|
|
fTime: TDateTime;
|
|
fDest: TSQLADest;
|
|
fSource: TSQLASource;
|
|
published
|
|
property Source: TSQLASource read fSource;
|
|
property Dest: TSQLADest read fDest;
|
|
property AssociationTime: TDateTime read fTime write fTime;
|
|
end;
|
|
TSQLASource = class(TSQLRecordSigned)
|
|
private
|
|
fDestList: TSQLADests;
|
|
published
|
|
property SignatureTime;
|
|
property Signature;
|
|
property DestList: TSQLADests read fDestList;
|
|
end;
|
|
TSQLADest = class(TSQLRecordSigned)
|
|
published
|
|
property SignatureTime;
|
|
property Signature;
|
|
end;
|
|
TSQLRecordPeopleArray = class(TSQLRecordPeople)
|
|
private
|
|
fInts: TIntegerDynArray;
|
|
fCurrency: TCurrencyDynArray;
|
|
{$ifdef PUBLISHRECORD}
|
|
fRec: TFTSMatchInfo;
|
|
{$endif PUBLISHRECORD}
|
|
fFileVersion: TFVs;
|
|
fUTF8: RawUTF8;
|
|
published
|
|
{$ifdef PUBLISHRECORD}
|
|
property Rec: TFTSMatchInfo read fRec write fRec;
|
|
{$endif PUBLISHRECORD}
|
|
property UTF8: RawUTF8 read fUTF8 write fUTF8;
|
|
property Ints: TIntegerDynArray index 1 read fInts write fInts;
|
|
property Currency: TCurrencyDynArray index 2 read fCurrency write fCurrency;
|
|
property FileVersion: TFVs index 3 read fFileVersion write fFileVersion;
|
|
end;
|
|
{$ifndef LVCL}
|
|
TSQLRecordPeopleObject = class(TSQLRecordPeople)
|
|
private
|
|
fPersistent: TCollTst;
|
|
fUTF8: TRawUTF8List;
|
|
public
|
|
/// will create an internal TCollTst instance for Persistent property
|
|
constructor Create; override;
|
|
/// will release the internal TCollTst instance for Persistent property
|
|
destructor Destroy; override;
|
|
published
|
|
property UTF8: TRawUTF8List read fUTF8;
|
|
property Persistent: TCollTst read fPersistent;
|
|
end;
|
|
{$endif}
|
|
TSQLRecordDali1 = class(TSQLRecordVirtualTableAutoID)
|
|
private
|
|
fYearOfBirth: integer;
|
|
fFirstName: RawUTF8;
|
|
fYearOfDeath: word;
|
|
published
|
|
property FirstName: RawUTF8 read fFirstName write fFirstName;
|
|
property YearOfBirth: integer read fYearOfBirth write fYearOfBirth;
|
|
property YearOfDeath: word read fYearOfDeath write fYearOfDeath;
|
|
end;
|
|
TSQLRecordDali2 = class(TSQLRecordDali1);
|
|
|
|
|
|
procedure TTestSQLite3Engine._TSQLRestClientDB;
|
|
var V,V2: TSQLRecordPeople;
|
|
VA: TSQLRecordPeopleArray;
|
|
{$ifndef LVCL}
|
|
VO: TSQLRecordPeopleObject;
|
|
{$endif}
|
|
FV: TFV;
|
|
ModelC: TSQLModel;
|
|
Client: TSQLRestClientDB;
|
|
ClientDist: TSQLRestClientURI;
|
|
Server: TSQLRestServer;
|
|
aStatic: TSQLRestServerStaticInMemory;
|
|
Curr: Currency;
|
|
DaVinci, s, h: RawUTF8;
|
|
Refreshed: boolean;
|
|
J: TSQLTableJSON;
|
|
i, n, nupd, ndx: integer;
|
|
IntArray, Results: TIntegerDynArray;
|
|
Data: TSQLRawBlob;
|
|
DataS: THeapMemoryStream;
|
|
a,b: double;
|
|
procedure checks(Leonard: boolean; Client: TSQLRestClient; const msg: string);
|
|
var ID: integer;
|
|
begin
|
|
ID := V.ID; // ClearProperties do ID := 0;
|
|
V.ClearProperties; // reset values
|
|
Check(Client.Retrieve(ID,V),msg); // internaly call URL()
|
|
if Leonard then
|
|
Check(V.FirstName='Leonard') else
|
|
Check(V.FirstName='Leonardo1',msg);
|
|
Check(V.LastName=DaVinci,msg);
|
|
Check(V.YearOfBirth=1452,msg);
|
|
Check(V.YearOfDeath=1519,msg);
|
|
end;
|
|
procedure TestMany(aClient: TSQLRestClient);
|
|
var MS: TSQLASource;
|
|
MD, MD2: TSQLADest;
|
|
i: integer;
|
|
sID, dID: array[1..100] of Integer;
|
|
res: TIntegerDynArray;
|
|
begin
|
|
MS := TSQLASource.Create;
|
|
MD := TSQLADest.Create;
|
|
try
|
|
MD.fSignatureTime := Iso8601Now;
|
|
MS.fSignatureTime := MD.fSignatureTime;
|
|
Check(MS.DestList<>nil);
|
|
Check(MS.DestList.InheritsFrom(TSQLRecordMany));
|
|
Check(aClient.TransactionBegin(TSQLASource)); // faster process
|
|
for i := 1 to high(dID) do begin
|
|
MD.fSignature := FormatUTF8('% %',[aClient.ClassName,i]);
|
|
dID[i] := aClient.Add(MD,true);
|
|
Check(dID[i]>0);
|
|
end;
|
|
for i := 1 to high(sID) do begin
|
|
MS.fSignature := FormatUTF8('% %',[aClient.ClassName,i]);
|
|
sID[i] := aClient.Add(MS,True);
|
|
Check(sID[i]>0);
|
|
MS.DestList.AssociationTime := i;
|
|
Check(MS.DestList.ManyAdd(aClient,sID[i],dID[i])); // associate both lists
|
|
Check(not MS.DestList.ManyAdd(aClient,sID[i],dID[i],true)); // no dup
|
|
end;
|
|
aClient.Commit;
|
|
for i := 1 to high(dID) do begin
|
|
Check(MS.DestList.SourceGet(aClient,dID[i],res));
|
|
if not CheckFailed(length(res)=1) then
|
|
Check(res[0]=sID[i]);
|
|
Check(MS.DestList.ManySelect(aClient,sID[i],dID[i]));
|
|
Check(MS.DestList.AssociationTime=i);
|
|
end;
|
|
for i := 1 to high(sID) do begin
|
|
Check(MS.DestList.DestGet(aClient,sID[i],res));
|
|
if CheckFailed(length(res)=1) then
|
|
continue; // avoid GPF
|
|
Check(res[0]=dID[i]);
|
|
Check(MS.DestList.FillMany(aClient,sID[i])=1);
|
|
Check(MS.DestList.FillOne);
|
|
Check(Integer(MS.DestList.Source)=sID[i]);
|
|
Check(Integer(MS.DestList.Dest)=dID[i]);
|
|
Check(MS.DestList.AssociationTime=i);
|
|
Check(not MS.DestList.FillOne);
|
|
Check(MS.DestList.DestGetJoined(aClient,'',sID[i],res));
|
|
if not CheckFailed(length(res)=1) then
|
|
Check(res[0]=dID[i]);
|
|
Check(MS.DestList.DestGetJoined(aClient,'ADest.SignatureTime=:(0):',sID[i],res));
|
|
Check(length(res)=0);
|
|
Check(MS.DestList.DestGetJoined(aClient,
|
|
FormatUTF8('ADest.SignatureTime=?',[],[MD.SignatureTime]),sID[i],res));
|
|
// 'ADest.SignatureTime=:('+Int64ToUTF8(MD.SignatureTime)+'):',sID[i],res));
|
|
if CheckFailed(length(res)=1) then
|
|
continue; // avoid GPF
|
|
Check(res[0]=dID[i]);
|
|
MD2 := MS.DestList.DestGetJoined(aClient,
|
|
FormatUTF8('ADest.SignatureTime=?',[],[MD.SignatureTime]),sID[i]) as TSQLADest;
|
|
// 'ADest.SignatureTime=:('+Int64ToUTF8(MD.SignatureTime)+'):',sID[i]) as TSQLADest;
|
|
if CheckFailed(MD2<>nil) then
|
|
continue;
|
|
try
|
|
Check(MD2.FillOne);
|
|
Check(MD2.ID=dID[i]);
|
|
Check(MD2.Signature=FormatUTF8('% %',[aClient.ClassName,i]));
|
|
finally
|
|
MD2.Free;
|
|
end;
|
|
end;
|
|
Check(MS.FillPrepareMany(aClient,
|
|
'DestList.Dest.SignatureTime<>% and id>=? and DestList.AssociationTime<>0 '+
|
|
'and SignatureTime=DestList.Dest.SignatureTime '+
|
|
'and DestList.Dest.Signature<>"DestList.AssociationTime"',[0],[sID[1]]));
|
|
Check(MS.FillTable.RowCount=length(sID));
|
|
for i := 1 to high(sID) do begin
|
|
MS.SignatureTime := 0;
|
|
MS.DestList.Dest.SignatureTime := 0;
|
|
if CheckFailed(MS.FillOne) then
|
|
break;
|
|
Check(MS.fID=sID[i]);
|
|
Check(MS.SignatureTime=MD.fSignatureTime);
|
|
Check(MS.DestList.AssociationTime=i);
|
|
Check(MS.DestList.Dest.fID=dID[i]);
|
|
Check(MS.DestList.Dest.SignatureTime=MD.fSignatureTime);
|
|
Check(MS.DestList.Dest.Signature=FormatUTF8('% %',[aClient.ClassName,i]));
|
|
end;
|
|
MS.FillClose;
|
|
Check(aClient.TransactionBegin(TSQLADests)); // faster process
|
|
for i := 1 to high(sID) shr 2 do
|
|
Check(MS.DestList.ManyDelete(aClient,sID[i*4],dID[i*4]));
|
|
aClient.Commit;
|
|
for i := 1 to high(sID) do
|
|
if i and 3<>0 then begin
|
|
Check(MS.DestList.ManySelect(aClient,sID[i],dID[i]));
|
|
Check(MS.DestList.AssociationTime=i);
|
|
end else
|
|
Check(not MS.DestList.ManySelect(aClient,sID[i],dID[i]));
|
|
finally
|
|
MD.Free;
|
|
MS.Free;
|
|
end;
|
|
end;
|
|
procedure TestDynArray(aClient: TSQLRestClient);
|
|
var i, j, k, l: integer;
|
|
IDs: TIntegerDynArray;
|
|
begin
|
|
VA.ClearProperties;
|
|
for i := 1 to n do begin
|
|
aClient.Retrieve(i,VA);
|
|
Check(VA.ID=i);
|
|
Check(VA.LastName='Dali');
|
|
Check(length(VA.Ints)=i shr 5);
|
|
Check(length(VA.Currency)=i shr 5);
|
|
Check(length(VA.FileVersion)=i shr 5);
|
|
if i and 31=0 then begin
|
|
Check(VA.UTF8='');
|
|
for j := 0 to high(VA.Ints) do
|
|
Check(VA.Ints[j]=(j+1) shl 5);
|
|
for j := 0 to high(VA.Currency) do
|
|
Check(PInt64(@VA.Currency[j])^=(j+1)*3200);
|
|
for j := 0 to high(VA.FileVersion) do
|
|
with VA.FileVersion[j] do begin
|
|
k := (j+1) shl 5;
|
|
Check(Major=k);
|
|
Check(Minor=k+2000);
|
|
Check(Release=k+3000);
|
|
Check(Build=k+4000);
|
|
Check(Main=IntToStr(k));
|
|
Check(Detailed=IntToStr(k+1000));
|
|
end;
|
|
end else begin
|
|
Check(GetInteger(pointer(VA.UTF8))=i);
|
|
for j := 0 to high(VA.FileVersion) do
|
|
with VA.FileVersion[j] do begin
|
|
k := (j+1) shl 5;
|
|
Check(Major=k);
|
|
Check(Minor=k+2000);
|
|
Check(Release=k+3000);
|
|
Check(Build=k+4000);
|
|
end;
|
|
end;
|
|
{$ifdef PUBLISHRECORD}
|
|
Check(VA.fRec.nPhrase=i);
|
|
Check(VA.fRec.nCol=i*2);
|
|
Check(VA.fRec.hits[2].docs_with_hits=i*3);
|
|
{$endif PUBLISHRECORD}
|
|
end;
|
|
for i := 1 to n shr 5 do begin
|
|
k := i shl 5;
|
|
aClient.OneFieldValues(TSQLRecordPeopleArray,'ID',
|
|
FormatUTF8('IntegerDynArrayContains(Ints,?)',[],[k]),IDs);
|
|
l := n+1-32*i;
|
|
Check(length(IDs)=l);
|
|
for j := 0 to high(IDs) do
|
|
Check(IDs[j]=k+j);
|
|
aClient.OneFieldValues(TSQLRecordPeopleArray,'ID',
|
|
FormatUTF8('CardinalDynArrayContains(Ints,?)',[],[k]),IDs);
|
|
Check(length(IDs)=l);
|
|
for j := 0 to high(IDs) do
|
|
Check(IDs[j]=k+j);
|
|
aClient.OneFieldValues(TSQLRecordPeopleArray,'ID',
|
|
FormatUTF8('MyIntegerDynArrayContains(Ints,:("%"):)',
|
|
[BinToBase64WithMagic(@k,sizeof(k))]),IDs);
|
|
Check(length(IDs)=l);
|
|
for j := 0 to high(IDs) do
|
|
Check(IDs[j]=k+j);
|
|
end;
|
|
end;
|
|
{$ifndef LVCL}
|
|
procedure TestObject(aClient: TSQLRestClient);
|
|
var i, j, k: integer;
|
|
begin
|
|
for i := 1 to n do begin
|
|
VO.ClearProperties;
|
|
aClient.Retrieve(i,VO);
|
|
Check(VO.ID=i);
|
|
Check(VO.LastName='Morse');
|
|
Check(VO.UTF8.Count=i shr 5);
|
|
for j := 0 to VO.UTF8.Count-1 do
|
|
Check(GetInteger(pointer(VO.UTF8[j]))=(j+1) shl 5);
|
|
Check(VO.Persistent.One.Length=i);
|
|
Check(VO.Persistent.One.Color=i+100);
|
|
Check(GetInteger(pointer(VO.Persistent.One.Name))=i);
|
|
Check(VO.Persistent.Coll.Count=i shr 5);
|
|
for j := 0 to VO.Persistent.Coll.Count-1 do
|
|
with VO.Persistent.Coll[j] do begin
|
|
k := (j+1) shl 5;
|
|
Check(Color=k+1000);
|
|
Check(Length=k*2);
|
|
Check(GetInteger(pointer(Name))=k*3);
|
|
end;
|
|
end;
|
|
end;
|
|
{$endif}
|
|
{$ifdef INCLUDE_FTS3}
|
|
procedure TestFTS3(aClient: TSQLRestClient);
|
|
var FTS: TSQLFTSTest;
|
|
StartID, i: integer;
|
|
IntResult: TIntegerDynArray;
|
|
c: Char;
|
|
const COUNT=400;
|
|
begin
|
|
Check(Length(IntArray)>COUNT*2);
|
|
FTS := TSQLFTSTest.Create;
|
|
try
|
|
if aClient=Client then
|
|
StartID := 0 else
|
|
StartID := COUNT;
|
|
Check(aClient.TransactionBegin(TSQLFTSTest)); // MUCH faster with this
|
|
for i := StartID to StartID+COUNT-1 do begin
|
|
FTS.DocID := IntArray[i];
|
|
FTS.Subject := aClient.OneFieldValue(TSQLRecordPeople,'FirstName',FTS.DocID);
|
|
Check(IdemPChar(pointer(FTS.Subject),'SALVADOR'));
|
|
FTS.Body := FTS.Subject+' bodY'+IntToStr(FTS.DocID);
|
|
aClient.Add(FTS,true);
|
|
end;
|
|
aClient.Commit; // Commit must be BEFORE OptimizeFTS3, memory leak otherwize
|
|
Check(FTS.OptimizeFTS3Index(Client.fServer));
|
|
for i := StartID to StartID+COUNT-1 do begin
|
|
Check(IdemPChar(pointer(aClient.OneFieldValue(TSQLFTSTest,'Subject',IntArray[i])),'SALVADOR'));
|
|
FTS.DocID := 0;
|
|
FTS.Subject := '';
|
|
FTS.Body := '';
|
|
Check(aClient.Retrieve(IntArray[i],FTS));
|
|
Check(FTS.DocID=IntArray[i]);
|
|
Check(IdemPChar(pointer(FTS.Subject),'SALVADOR'));
|
|
Check(PosEx(Int32ToUtf8(FTS.DocID),FTS.Body,1)>0);
|
|
end;
|
|
Check(aClient.FTSMatch(TSQLFTSTest,'Subject MATCH "salVador1"',IntResult));
|
|
for i := 0 to high(IntResult) do
|
|
Check(SameTextU(aClient.OneFieldValue(
|
|
TSQLRecordPeople,'FirstName',IntResult[i]),'SALVADOR1'));
|
|
Check(aClient.FTSMatch(TSQLFTSTest,'Subject MATCH "salVador1*"',IntResult));
|
|
for i := 0 to high(IntResult) do
|
|
Check(IdemPChar(pointer(aClient.OneFieldValue(
|
|
TSQLRecordPeople,'FirstName',IntResult[i])),'SALVADOR1'));
|
|
Check(not aClient.FTSMatch(TSQLFTSTest,'body*',IntResult,[1]),'invalid count');
|
|
for c := '1' to '9' do begin
|
|
Check(aClient.FTSMatch(TSQLFTSTest,'Body MATCH "body'+c+'*"',IntResult));
|
|
Check(length(IntResult)>0);
|
|
for i := 0 to high(IntResult) do
|
|
Check(IntToStr(IntResult[i])[1]=c);
|
|
Check(aClient.FTSMatch(TSQLFTSTest,'body'+c+'*',IntResult,[1,0.5]),'rank');
|
|
Check(length(IntResult)>0);
|
|
for i := 0 to high(IntResult) do
|
|
Check(IntToStr(IntResult[i])[1]=c);
|
|
end;
|
|
finally
|
|
FTS.Free;
|
|
end;
|
|
end;
|
|
{$endif}
|
|
procedure TestVirtual(aClient: TSQLRestClient; DirectSQL: boolean; const Msg: string;
|
|
aClass: TSQLRecordClass);
|
|
var n, i, ndx: integer;
|
|
VD, VD2: TSQLRecordDali1;
|
|
Static: TSQLRestServerStatic;
|
|
begin
|
|
Client.fServer.StaticVirtualTableDirect := DirectSQL;
|
|
Check(Client.fServer.EngineExecuteFmt('DROP TABLE %',[aClass.SQLTableName]));
|
|
Client.fServer.CreateMissingTables(0);
|
|
VD := aClass.Create as TSQLRecordDali1;
|
|
try
|
|
if aClient.TransactionBegin(aClass) then
|
|
try
|
|
// add some items to the file
|
|
V2.FillPrepare(aClient,'LastName=:("Dali"):');
|
|
n := 0;
|
|
while V2.FillOne do begin
|
|
VD.FirstName := V2.FirstName;
|
|
VD.YearOfBirth := V2.YearOfBirth;
|
|
VD.YearOfDeath := V2.YearOfDeath;
|
|
inc(n);
|
|
Check(aClient.Add(VD,true)=n,Msg);
|
|
end;
|
|
// update some items in the file
|
|
Check(aClient.TableRowCount(aClass)=1001,'Check SQL Count(*)');
|
|
for i := 1 to n do begin
|
|
VD.ClearProperties;
|
|
Check(VD.ID=0);
|
|
Check(VD.FirstName='');
|
|
Check(VD.YearOfBirth=0);
|
|
Check(VD.YearOfDeath=0);
|
|
Check(aClient.Retrieve(i,VD),Msg);
|
|
Check(VD.ID=i);
|
|
Check(IdemPChar(pointer(VD.FirstName),'SALVADOR'));
|
|
Check(VD.YearOfBirth=1904);
|
|
Check(VD.YearOfDeath=1989);
|
|
VD.YearOfBirth := VD.YearOfBirth+i;
|
|
VD.YearOfDeath := VD.YearOfDeath+i;
|
|
Check(aClient.Update(VD),Msg);
|
|
end;
|
|
// check SQL requests
|
|
for i := 1 to n do begin
|
|
VD.ClearProperties;
|
|
Check(VD.ID=0);
|
|
Check(VD.FirstName='');
|
|
Check(VD.YearOfBirth=0);
|
|
Check(VD.YearOfDeath=0);
|
|
Check(aClient.Retrieve(i,VD),Msg);
|
|
Check(IdemPChar(pointer(VD.FirstName),'SALVADOR'));
|
|
Check(VD.YearOfBirth=1904+i);
|
|
Check(VD.YearOfDeath=1989+i);
|
|
end;
|
|
Check(aClient.TableRowCount(aClass)=1001);
|
|
aClient.Commit; // write to file
|
|
// try to read directly from file content
|
|
Static := Client.Server.StaticVirtualTable[aClass];
|
|
if CheckFailed(Static<>nil) then
|
|
exit;
|
|
if Static.FileName<>'' then begin // no file content if ':memory' DB
|
|
(Static as TSQLRestServerStaticInMemoryExternal).
|
|
UpdateFile; // force update (COMMIT not always calls xCommit)
|
|
Static := TSQLRestServerStaticInMemoryExternal.Create(aClass,nil,Static.FileName,
|
|
aClass=TSQLRecordDali2);
|
|
try
|
|
Check(TSQLRestServerStaticInMemory(Static).Count=n);
|
|
for i := 1 to n do begin
|
|
ndx := TSQLRestServerStaticInMemory(Static).IDToIndex(i);
|
|
if CheckFailed(ndx>=0) then
|
|
continue;
|
|
VD2 := TSQLRestServerStaticInMemory(Static).Items[ndx] as TSQLRecordDali1;
|
|
if CheckFailed(VD2<>nil) then
|
|
continue;
|
|
Check(VD2.ID=i);
|
|
Check(IdemPChar(pointer(VD2.FirstName),'SALVADOR'));
|
|
Check(VD2.YearOfBirth=1904+i);
|
|
Check(VD2.YearOfDeath=1989+i);
|
|
end;
|
|
finally
|
|
Static.Free;
|
|
end;
|
|
end;
|
|
except
|
|
aClient.RollBack; // will run an error - but this code is correct
|
|
end;
|
|
finally
|
|
VD.Free;
|
|
end;
|
|
end;
|
|
function Test(T: TSQLTable): boolean;
|
|
var aR,aF: integer;
|
|
begin
|
|
result := false;
|
|
if T=nil then
|
|
exit;
|
|
with TSQLTableDB.Create(Demo,[],Req,true) do
|
|
try
|
|
if (RowCount<>T.RowCount) or (FieldCount<>T.FieldCount) then begin
|
|
Check(False);
|
|
exit;
|
|
end;
|
|
for aR := 0 to RowCount do // compare all result values
|
|
for aF := 0 to FieldCount-1 do
|
|
if StrComp(pointer(Get(aR,aF)),pointer(T.Get(aR,aF)))<>0 then begin
|
|
Check(False);
|
|
exit;
|
|
end;
|
|
result := true;
|
|
finally
|
|
Free;
|
|
T.Free;
|
|
end;
|
|
end;
|
|
begin
|
|
V := TSQLRecordPeople.Create;
|
|
VA := TSQLRecordPeopleArray.Create;
|
|
{$ifndef LVCL}
|
|
VO := TSQLRecordPeopleObject.Create;
|
|
{$endif}
|
|
V2 := nil;
|
|
if not IsMemory then begin
|
|
DeleteFile('dali1.json');
|
|
DeleteFile('dali2.data');
|
|
end;
|
|
Demo.RegisterSQLFunction(TypeInfo(TIntegerDynArray),@SortDynArrayInteger,
|
|
'MyIntegerDynArrayContains');
|
|
ModelC := TSQLModel.Create(
|
|
[TSQLRecordPeople, {$ifdef INCLUDE_FTS3} TSQLFTSTest, {$endif}
|
|
TSQLASource, TSQLADest, TSQLADests, TSQLRecordPeopleArray
|
|
{$ifndef LVCL}, TSQLRecordPeopleObject{$endif},
|
|
TSQLRecordDali1,TSQLRecordDali2],'root');
|
|
ModelC.VirtualTableRegister(TSQLRecordDali1,TSQLVirtualTableJSON);
|
|
ModelC.VirtualTableRegister(TSQLRecordDali2,TSQLVirtualTableBinary);
|
|
try
|
|
Client := TSQLRestClientDB.Create(ModelC,nil,Demo,TSQLRestServerTest,true);
|
|
try
|
|
Client.Server.DB.Synchronous := smOff;
|
|
with Client.fServer.Model do
|
|
for i := 0 to high(Tables) do
|
|
if not CheckFailed(GetTableIndex(Tables[i])=i) then
|
|
Check(GetTableIndex(Tables[i].SQLTableName)=i);
|
|
// direct client access test
|
|
Client.fServer.CreateMissingTables(0); // NEED Dest,Source,Dests,...
|
|
Check(Client.SetUser('User','synopse')); // use default user
|
|
DaVinci := WinAnsiToUtf8('da Vin'+chr(231)+'i');
|
|
Check(Client.Retrieve('LastName='''+DaVinci+'''',V));
|
|
Check(V.FirstName='Leonardo1');
|
|
Check(V.LastName=DaVinci);
|
|
Check(V.YearOfBirth=1452);
|
|
Check(V.YearOfDeath=1519);
|
|
checks(false,Client,'Retrieve');
|
|
Check(V.ID=6,'check RETRIEVE/GET');
|
|
Check(Client.Delete(TSQLRecordPeople,V.ID),'check DELETE');
|
|
Check(not Client.Retrieve(V.ID,V),'now this record must not be available');
|
|
Check(Client.Add(V,true)>0,'check ADD/PUT');
|
|
checks(false,Client,'check created value is well retrieved');
|
|
checks(false,Client,'check caching');
|
|
V2 := V.CreateCopy as TSQLRecordPeople;
|
|
Check(V2.SameValues(V));
|
|
V2.Free;
|
|
V2 := TSQLRecordPeople.Create(Client,V.ID);
|
|
Check(V2.SameValues(V));
|
|
Check(Client.Retrieve(V.ID,V2,true),'with LOCK');
|
|
Check(V2.SameValues(V));
|
|
V.FirstName := 'Leonard';
|
|
Check(Client.Update(V));
|
|
Check(Client.UnLock(V),'unlock');
|
|
checks(true,Client,'check UPDATE/POST');
|
|
if Client.SessionUser=nil then // only if has the right for EngineExecute
|
|
Check(Client.EngineExecute('VACUUM;'),'check direct EngineExecute') else
|
|
Check(Client.Server.EngineExecuteAll('VACUUM;'));
|
|
Check(V2.FirstName='Leonardo1');
|
|
Check(not V2.SameValues(V),'V and V2 must differ');
|
|
Check(Client.UpdateFromServer([V2],Refreshed));
|
|
Check(Refreshed,'V2 value will be synchronized with V');
|
|
Check(V2.SameValues(V));
|
|
Check(Client.UpdateFromServer([V2],Refreshed));
|
|
Check(not Refreshed);
|
|
Req := StringReplace(Req,'*',TSQLRecordPeople.RecordProps.SQLTableSimpleFields[true,false],[]);
|
|
s := WinAnsiToUtf8('LastName=''M'+chr(244)+'net'' ORDER BY FirstName');
|
|
J := Client.List([TSQLRecordPeople],'*',s);
|
|
Check(Client.UpdateFromServer([J],Refreshed));
|
|
Check(not Refreshed);
|
|
Check(Test(J),'incorrect TSQLTableJSON');
|
|
Check(Client.OneFieldValues(TSQLRecordPeople,'ID','LastName=:("Dali"):',IntArray));
|
|
Check(length(IntArray)=1001);
|
|
for i := 0 to high(IntArray) do
|
|
Check(Client.OneFieldValue(TSQLRecordPeople,'LastName',IntArray[i])='Dali');
|
|
Check(Client.TransactionBegin(TSQLRecordPeople)); // for UpdateBlob() below
|
|
for i := 0 to high(IntArray) do begin
|
|
Check(Client.RetrieveBlob(TSQLRecordPeople,IntArray[i],'Data',Data));
|
|
Check(Length(Data)=sizeof(BlobDali));
|
|
Check(CompareMem(pointer(Data),@BlobDali,sizeof(BlobDali)));
|
|
Check(Client.RetrieveBlob(TSQLRecordPeople,IntArray[i],'Data',DataS));
|
|
Check((DataS.Size=4) and (PCardinal(DataS.Memory)^=$E7E0E961));
|
|
DataS.Free;
|
|
Check(Client.UpdateBlob(TSQLRecordPeople,IntArray[i],'Data',@IntArray[i],4));
|
|
Check(Client.RetrieveBlob(TSQLRecordPeople,IntArray[i],'Data',Data));
|
|
Check((length(Data)=4) and (PInteger(pointer(Data))^=IntArray[i]));
|
|
V2.ID := IntArray[i]; // debug use - do NOT set ID in your programs!
|
|
Check(V2.DataAsHex(Client)=SynCommons.BinToHex(Data));
|
|
a := Random;
|
|
b := Random;
|
|
Check(SameValue(TSQLRecordPeople.Sum(Client,a,b),a+b,1E-10));
|
|
end;
|
|
Client.Commit;
|
|
Check(Client.TransactionBegin(TSQLRecordPeopleArray));
|
|
V2.FillPrepare(Client,'LastName=:("Dali"):');
|
|
n := 0;
|
|
while V2.FillOne do begin
|
|
VA.FillFrom(V2); // fast copy some content from TSQLRecordPeople
|
|
inc(n);
|
|
if n and 31=0 then begin
|
|
VA.UTF8 := '';
|
|
VA.DynArray('Ints').Add(n);
|
|
Curr := n*0.01;
|
|
VA.DynArray(2).Add(Curr);
|
|
FV.Major := n;
|
|
FV.Minor := n+2000;
|
|
FV.Release := n+3000;
|
|
FV.Build := n+4000;
|
|
str(n,FV.Main);
|
|
str(n+1000,FV.Detailed);
|
|
VA.DynArray('FileVersion').Add(FV);
|
|
end else
|
|
str(n,VA.fUTF8);
|
|
{$ifdef PUBLISHRECORD}
|
|
VA.fRec.nPhrase := n;
|
|
VA.fRec.nCol := n*2;
|
|
VA.fRec.hits[2].docs_with_hits := n*3;
|
|
{$endif PUBLISHRECORD}
|
|
Check(Client.Add(VA,true)=n);
|
|
end;
|
|
Client.Commit;
|
|
{$ifndef LVCL}
|
|
if Client.TransactionBegin(TSQLRecordPeopleObject) then
|
|
try
|
|
V2.FillPrepare(Client,'LastName=:("Morse"):');
|
|
n := 0;
|
|
while V2.FillOne do begin
|
|
VO.FillFrom(V2); // fast copy some content from TSQLRecordPeople
|
|
inc(n);
|
|
VO.Persistent.One.Color := n+100;
|
|
VO.Persistent.One.Length := n;
|
|
VO.Persistent.One.Name := Int32ToUtf8(n);
|
|
if n and 31=0 then begin
|
|
VO.UTF8.Add(VO.Persistent.One.Name);
|
|
with VO.Persistent.Coll.Add do begin
|
|
Color := n+1000;
|
|
Length := n*2;
|
|
Name := Int32ToUtf8(n*3);
|
|
end;
|
|
end;
|
|
Check(Client.Add(VO,true)=n);
|
|
end;
|
|
Client.Commit;
|
|
except
|
|
Client.RollBack;
|
|
end;
|
|
{$endif}
|
|
{$ifdef INCLUDE_FTS3}
|
|
TestFTS3(Client);
|
|
{$endif}
|
|
TestDynArray(Client);
|
|
{$ifndef LVCL}
|
|
TestObject(Client);
|
|
{$endif}
|
|
TestMany(Client);
|
|
// RegisterVirtualTableModule(TSQLVirtualTableJSON) already done
|
|
TestVirtual(Client,false,'Virtual Table access via SQLite',TSQLRecordDali1);
|
|
TestVirtual(Client,false,'Virtual Table access via SQLite',TSQLRecordDali2);
|
|
TestVirtual(Client,true,'Direct Virtual Table access',TSQLRecordDali1);
|
|
TestVirtual(Client,true,'Direct Virtual Table access',TSQLRecordDali2);
|
|
// remote client access test
|
|
Check(Client.fServer.ExportServerNamedPipe('Test'),'declare Test server');
|
|
ClientDist := TSQLRestClientURINamedPipe.Create(ModelC,'Test');
|
|
try
|
|
Check(ClientDist.SetUser('User','synopse'));
|
|
{$ifdef INCLUDE_FTS3}
|
|
TestFTS3(ClientDist);
|
|
{$endif}TestDynArray(ClientDist);
|
|
{$ifndef LVCL}
|
|
TestObject(ClientDist);
|
|
{$endif}TestMany(ClientDist);
|
|
TestVirtual(ClientDist,false,'Remote Virtual Table access via SQLite',TSQLRecordDali1);
|
|
TestVirtual(ClientDist,false,'Remote Virtual Table access via SQLite',TSQLRecordDali2);
|
|
TestVirtual(ClientDist,true,'Remote Direct Virtual Table access',TSQLRecordDali1);
|
|
TestVirtual(ClientDist,true,'Remote Direct Virtual Table access',TSQLRecordDali2);
|
|
Check(Test(ClientDist.List([TSQLRecordPeople],'*',s)),'through URI and JSON');
|
|
for i := 0 to high(IntArray) do begin
|
|
Check(ClientDist.RetrieveBlob(TSQLRecordPeople,IntArray[i],'Data',Data));
|
|
Check((length(Data)=4) and (PInteger(pointer(Data))^=IntArray[i]));
|
|
V2.ID := IntArray[i]; // debug use - do NOT set ID in your programs!
|
|
Check(V2.DataAsHex(ClientDist)=SynCommons.BinToHex(Data));
|
|
a := Random;
|
|
b := Random;
|
|
CheckSame(TSQLRecordPeople.Sum(Client,a,b),a+b);
|
|
end;
|
|
V.FirstName := 'Leonardo1';
|
|
Check(ClientDist.Update(V));
|
|
checks(false,ClientDist,'check remote UPDATE/POST');
|
|
V.FirstName := 'Leonard';
|
|
Check(ClientDist.Update(V));
|
|
checks(true,ClientDist,'check remote UPDATE/POST');
|
|
// time := GetTickCount; while time=GetTickCount do; time := GetTickCount;
|
|
for i := 1 to 400 do // speed test: named pipes are OK
|
|
checks(true,ClientDist,'caching speed test');
|
|
// writeln('NamedPipe connection time is ',GetTickCount-time,'ms');
|
|
finally
|
|
ClientDist.Free;
|
|
end;
|
|
if IsMemory then begin // this is a bit time consuming, so do it once
|
|
Server := TSQLRestServerTest.Create(ModelC,false);
|
|
try
|
|
Server.NoAJAXJSON := true;
|
|
DeleteFile('People.json');
|
|
DeleteFile('People.data');
|
|
Server.StaticDataCreate(TSQLRecordPeople,'People.data',true);
|
|
JS := Demo.ExecuteJSON('SELECT * From People');
|
|
aStatic := Server.StaticDataServer[TSQLRecordPeople] as TSQLRestServerStaticInMemory;
|
|
Check(aStatic<>nil);
|
|
aStatic.LoadFromJSON(JS); // test Add()
|
|
for i := 0 to aStatic.Count-1 do begin
|
|
Check(Client.Retrieve(aStatic.ID[i],V),'test statement+bind speed');
|
|
Check(V.SameRecord(aStatic.Items[i]),'static retrieve');
|
|
end;
|
|
// test our 'REST-minimal' SELECT statement SQL engine
|
|
Server.URI(Client.SessionSign('/root/People?select=%2A&where=id%3D012'),'GET','',s,h,@SUPERVISOR_ACCESS_RIGHTS);
|
|
Check(Hash32(S)=$9B10BE36);
|
|
Server.URI(Client.SessionSign('/root/People?select=%2A&where=id%3D:(012):'),'GET','',s,h,@SUPERVISOR_ACCESS_RIGHTS);
|
|
Check(Hash32(S)=$9B10BE36);
|
|
Server.URI(Client.SessionSign('/root/People?select=%2A&where=LastName%3D%22M%C3%B4net%22'),'GET','',s,h,@SUPERVISOR_ACCESS_RIGHTS);
|
|
Check(Hash32(S)=$7D90E964);
|
|
Server.URI(Client.SessionSign('/root/People?select=%2A&where=YearOfBirth%3D1873'),'GET','',s,h,@SUPERVISOR_ACCESS_RIGHTS);
|
|
Check(Hash32(S)=$F0818745);
|
|
Server.URI(Client.SessionSign('/root/People?select=%2A'),'GET','',s,h,@SUPERVISOR_ACCESS_RIGHTS);
|
|
Check(Hash32(S)=$17AE45E3);
|
|
// test Retrieve() and Delete()
|
|
Server.ExportServer; // initialize URIRequest() with the aStatic database
|
|
USEFASTMM4ALLOC := true; // getmem() is 2x faster than GlobalAlloc()
|
|
ClientDist := TSQLRestClientURIDll.Create(ModelC,URIRequest);
|
|
try
|
|
SetLength(IntArray,(aStatic.Count-1)shr 2);
|
|
for i := 0 to high(IntArray) do begin
|
|
IntArray[i] := aStatic.ID[i*4];
|
|
Check(ClientDist.Retrieve(IntArray[i],V));
|
|
Check(V.SameRecord(aStatic.Items[i*4]));
|
|
end;
|
|
Check(V.FillPrepare(Client,IntArray));
|
|
for i := 0 to High(IntArray) do begin
|
|
Check(V.FillOne);
|
|
Check(V.ID=IntArray[i]);
|
|
Check(V.SameRecord(aStatic.Items[i*4]));
|
|
end;
|
|
V.FillClose; // so that BatchUpdate(V) below will set all fields
|
|
if ClientDist.TransactionBegin(TSQLRecordPeople) then
|
|
try
|
|
for i := 0 to high(IntArray) do
|
|
Check(ClientDist.Delete(TSQLRecordPeople,IntArray[i]));
|
|
for i := 0 to high(IntArray) do
|
|
Check(not ClientDist.Retrieve(IntArray[i],V));
|
|
for i := 0 to aStatic.Count-1 do begin
|
|
Check(ClientDist.Retrieve(aStatic.ID[i],V));
|
|
V.YearOfBirth := Random(MaxInt)-Random(MaxInt);
|
|
Check(ClientDist.Update(V));
|
|
Check(ClientDist.Retrieve(aStatic.ID[i],V));
|
|
Check(V.SameRecord(aStatic.Items[i]));
|
|
end;
|
|
ClientDist.Commit;
|
|
except
|
|
ClientDist.RollBack;
|
|
end else
|
|
Check(False,'TransactionBegin');
|
|
// test BATCH sequence usage
|
|
if ClientDist.TransactionBegin(TSQLRecordPeople) then
|
|
try
|
|
Check(ClientDist.BatchStart(TSQLRecordPeople));
|
|
n := 0;
|
|
for i := 0 to aStatic.Count-1 do
|
|
if i and 7=0 then begin
|
|
IntArray[n] := aStatic.ID[i];
|
|
inc(n);
|
|
end;
|
|
for i := 0 to n-1 do
|
|
Check(ClientDist.BatchDelete(IntArray[i])=i);
|
|
nupd := 0;
|
|
for i := 0 to aStatic.Count-1 do
|
|
if i and 7<>0 then begin // not yet deleted in BATCH mode
|
|
Check(ClientDist.Retrieve(aStatic.ID[i],V));
|
|
V.YearOfBirth := 1800+nupd;
|
|
Check(ClientDist.BatchUpdate(V)=nupd+n);
|
|
inc(nupd);
|
|
end;
|
|
V.LastName := 'New';
|
|
for i := 0 to 1000 do begin
|
|
V.FirstName := RandomUTF8(10);
|
|
V.YearOfBirth := i+1000;
|
|
Check(ClientDist.BatchAdd(V,true)=n+nupd+i);
|
|
end;
|
|
Check(ClientDist.BatchSend(Results)=200);
|
|
Check(Length(Results)=9260);
|
|
ClientDist.Commit;
|
|
for i := 0 to n-1 do
|
|
Check(not ClientDist.Retrieve(IntArray[i],V),'BatchDelete');
|
|
for i := 0 to high(Results) do
|
|
if i<nupd+n then
|
|
Check(Results[i]=200) else begin
|
|
Check(Results[i]>0);
|
|
ndx := aStatic.IDToIndex(Results[i]);
|
|
Check(ndx>=0);
|
|
with TSQLRecordPeople(aStatic.Items[ndx]) do begin
|
|
Check(LastName='New','BatchAdd');
|
|
Check(YearOfBirth=1000+i-nupd-n);
|
|
end;
|
|
end;
|
|
for i := 0 to aStatic.Count-1 do
|
|
with TSQLRecordPeople(aStatic.Items[i]) do
|
|
if LastName='New' then
|
|
break else
|
|
Check(YearOfBirth=1800+i,'BatchUpdate');
|
|
except
|
|
ClientDist.RollBack;
|
|
end else
|
|
Check(False,'TransactionBegin');
|
|
// test BATCH update from partial FillPrepare
|
|
V.FillPrepare(ClientDist,'LastName=?',['New'],'ID,YearOfBirth');
|
|
if ClientDist.TransactionBegin(TSQLRecordPeople) then
|
|
try
|
|
Check(ClientDist.BatchStart(TSQLRecordPeople));
|
|
n := 0;
|
|
V.LastName := 'NotTransmitted';
|
|
while V.FillOne do begin
|
|
Check(V.LastName='NotTransmitted');
|
|
Check(V.YearOfBirth=n+1000);
|
|
V.YearOfBirth := n;
|
|
ClientDist.BatchUpdate(V); // will update only V.YearOfBirth
|
|
inc(n);
|
|
end;
|
|
Check(n=1001);
|
|
SetLength(Results,0);
|
|
Check(ClientDist.BatchSend(Results)=200);
|
|
Check(length(Results)=1001);
|
|
for i := 0 to high(Results) do
|
|
Check(Results[i]=200);
|
|
ClientDist.Commit;
|
|
except
|
|
ClientDist.RollBack;
|
|
end else
|
|
Check(False,'TransactionBegin');
|
|
V.FillPrepare(ClientDist,'LastName=?',['New'],'YearOfBirth');
|
|
n := 0;
|
|
while V.FillOne do begin
|
|
Check(V.LastName='NotTransmitted');
|
|
Check(V.YearOfBirth=n);
|
|
V.YearOfBirth := 1000;
|
|
inc(n);
|
|
end;
|
|
Check(n=length(Results));
|
|
V.FillClose;
|
|
finally
|
|
ClientDist.Free;
|
|
end;
|
|
aStatic.Modified := true;
|
|
aStatic.UpdateFile; // force People.data file content write
|
|
aStatic.FileName := 'People.json';
|
|
aStatic.BinaryFile := false;
|
|
aStatic.Modified := true; // so aStatic.Free will write People.json file
|
|
USEFASTMM4ALLOC := false;
|
|
finally
|
|
Server.Free;
|
|
end;
|
|
end;
|
|
finally
|
|
Client.Free;
|
|
end;
|
|
finally
|
|
ModelC.Free;
|
|
V.Free;
|
|
V2.Free;
|
|
VA.Free;
|
|
{$ifndef LVCL}
|
|
VO.Free;
|
|
{$endif}
|
|
FreeAndNil(Demo);
|
|
end;
|
|
if not IsMemory then begin
|
|
ChangeSQLEncryptTablePassWord(TempFileName,'NewPass',''); // uncrypt file
|
|
Check(IsSQLite3File(TempFileName));
|
|
CreateSQLEncryptTable(''); // reset the global password, if we work after test
|
|
end;
|
|
end;
|
|
|
|
procedure TTestSQLite3Engine._TSQLTableJSON;
|
|
var J: TSQLTableJSON;
|
|
aR, aF: integer;
|
|
Comp: TUTF8Compare;
|
|
begin
|
|
J := TSQLTableJSON.Create([],'',JS);
|
|
try
|
|
if JS<>'' then // avoid memory leak
|
|
with TSQLTableDB.Create(Demo,[],Req,true) do
|
|
try
|
|
Check(RowCount=J.RowCount);
|
|
Check(FieldCount=J.FieldCount);
|
|
for aR := 0 to RowCount do
|
|
for aF := 0 to FieldCount-1 do
|
|
if (aR>0) and (aF=3) then // aF=3=Blob
|
|
Check(GetBlob(aR,aF)=J.GetBlob(aR,aF)) else begin
|
|
Check((GetW(aR,aF)=J.GetW(aR,aF)) and
|
|
(GetA(aR,aF)=J.GetA(aR,aF)) and
|
|
(length(GetW(aR,aF))shr 1=LengthW(aR,aF)),
|
|
Format('Get() in Row=%d Field=%d',[aR,aF]));
|
|
if (aR>0) and (aF>3) then
|
|
Check(GetDateTime(aR,af)=J.GetDateTime(aR,aF));
|
|
end;
|
|
finally
|
|
Free;
|
|
end;
|
|
Demo.Execute('VACUUM;');
|
|
with TSQLTableDB.Create(Demo,[],Req,true) do // re-test after VACCUM
|
|
try
|
|
Check(RowCount=J.RowCount);
|
|
Check(FieldCount=J.FieldCount);
|
|
for aR := 0 to RowCount do
|
|
for aF := 0 to FieldCount-1 do // aF=3=Blob
|
|
Check((aF=3) or (StrIComp(Get(aR,aF),J.Get(aR,aF))=0));
|
|
finally
|
|
Free;
|
|
end;
|
|
for aF := 0 to J.FieldCount-1 do begin
|
|
J.SortFields(aF);
|
|
Comp := J.SortCompare(aF);
|
|
if @Comp<>nil then // BLOB field will be ignored
|
|
for aR := 1 to J.RowCount-1 do // ensure data sorted in increasing order
|
|
Check(Comp(pointer(J.Get(aR,aF)),pointer(J.Get(aR+1,aF)))<=0,'SortCompare');
|
|
end;
|
|
finally
|
|
J.Free;
|
|
end;
|
|
end;
|
|
|
|
{$ifdef UNICODE}
|
|
{$WARNINGS ON} // don't care about implicit string cast in tests
|
|
{$endif}
|
|
|
|
|
|
{ TSQLRestServerTest }
|
|
|
|
function TSQLRestServerTest.DataAsHex(var aParams: TSQLRestServerCallBackParams): Integer;
|
|
var aData: TSQLRawBlob;
|
|
begin
|
|
result := 404; // invalid Request
|
|
if (self=nil) or (aParams.Table=nil) or
|
|
not aParams.Table.InheritsFrom(TSQLRecord) or
|
|
(aParams.Context.ID<0) then
|
|
exit; // we need a valid record and its ID
|
|
if not RetrieveBlob(TSQLRecordPeople,aParams.Context.ID,'Data',aData) then
|
|
exit; // impossible to retrieve the Data BLOB field
|
|
aParams.Resp := JSONEncodeResult([SynCommons.BinToHex(aData)]);
|
|
// idem: aResp := JSONEncode(['result',BinToHex(aRecord.fData)],TempMemoryStream);
|
|
result := 200; // success
|
|
end;
|
|
|
|
function TSQLRestServerTest.Sum(var aParams: TSQLRestServerCallBackParams): Integer;
|
|
var a,b: Extended;
|
|
begin
|
|
if not UrlDecodeNeedParameters(aParams.Parameters,'A,B') then begin
|
|
result := 404; // invalid Request
|
|
exit;
|
|
end;
|
|
while aParams.Parameters<>nil do begin
|
|
UrlDecodeExtended(aParams.Parameters,'A=',a);
|
|
UrlDecodeExtended(aParams.Parameters,'B=',b,@aParams.Parameters);
|
|
end;
|
|
aParams.Resp := JSONEncodeResult([a+b]);
|
|
// same as : aResp := JSONEncode(['result',a+b],TempMemoryStream);
|
|
result := 200; // success
|
|
end;
|
|
|
|
|
|
{$ifndef LVCL}
|
|
|
|
{ TSQLRecordPeopleObject }
|
|
|
|
constructor TSQLRecordPeopleObject.Create;
|
|
begin
|
|
inherited;
|
|
fPersistent := TCollTst.Create;
|
|
fUTF8 := TRawUTF8List.Create;
|
|
end;
|
|
|
|
destructor TSQLRecordPeopleObject.Destroy;
|
|
begin
|
|
Persistent.Free;
|
|
UTF8.Free;
|
|
inherited;
|
|
end;
|
|
|
|
{ TCollTests }
|
|
|
|
function TCollTests.Add: TCollTest;
|
|
begin
|
|
result := inherited Add as TCollTest;
|
|
end;
|
|
|
|
function TCollTests.GetCollItem(Index: Integer): TCollTest;
|
|
begin
|
|
result := Items[Index] as TCollTest;
|
|
end;
|
|
|
|
{ TCollTst }
|
|
|
|
constructor TCollTst.Create;
|
|
begin
|
|
inherited;
|
|
fColl := TCollTests.Create(TCollTest);
|
|
fTCollTest := TCollTest.Create(nil);
|
|
end;
|
|
|
|
destructor TCollTst.Destroy;
|
|
begin
|
|
fColl.Free;
|
|
fTCollTest.Free;
|
|
fStr.Free;
|
|
inherited;
|
|
end;
|
|
|
|
{$endif LVCL}
|
|
|
|
function RegisterVirtualTableModule(aModule: TSQLVirtualTableClass; aDatabase: TSQLDataBase): TSQLVirtualTableModule;
|
|
begin
|
|
result := TSQLVirtualTableModuleSQLite3.Create(aModule,nil);
|
|
TSQLVirtualTableModuleSQLite3(result).DB := aDataBase;
|
|
end;
|
|
|
|
|
|
end.
|
|
|
|
|