xtool/contrib/mORMot/SQLite3/mORMotDB.pas

2194 lines
88 KiB
ObjectPascal

/// Virtual Tables for external DB access for mORMot
// - this unit is a part of the freeware Synopse mORMot framework,
// licensed under a MPL/GPL/LGPL tri-license; version 1.18
unit mORMotDB;
{
This file is part of Synopse mORMot framework.
Synopse mORMot framework. Copyright (C) 2022 Arnaud Bouchez
Synopse Informatique - https://synopse.info
*** BEGIN LICENSE BLOCK *****
Version: MPL 1.1/GPL 2.0/LGPL 2.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the License.
The Original Code is Synopse mORMot framework.
The Initial Developer of the Original Code is Arnaud Bouchez.
Portions created by the Initial Developer are Copyright (C) 2022
the Initial Developer. All Rights Reserved.
Contributor(s):
- Maciej Izak (hnb)
- yoanq
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 *****
}
{$I Synopse.inc} // define HASINLINE CPU32 CPU64 OWNNORMTOUPPER
interface
uses
{$ifdef MSWINDOWS}
Windows,
{$else}
{$ifdef KYLIX3}
LibC,
{$endif}
{$ifdef FPC}
SynFPCLinux,
{$endif}
{$endif}
SysUtils,
Classes,
SynCommons,
SynLog,
SynTable, // for TSynTableStatement
mORMot,
SynDB;
type
TSQLRestStorageExternal = class;
/// event handler called to customize the computation of a new ID
// - should set Handled=TRUE if a new ID has been computed and returned
// - Handled=FALSE would let the default ID computation take place
// - note that execution of this method would be protected by a mutex, so
// it would be thread-safe
TOnEngineAddComputeID = function(Sender: TSQLRestStorageExternal;
var Handled: Boolean): TID of object;
/// REST server with direct access to a SynDB-based external database
// - handle all REST commands, using the external SQL database connection,
// and prepared statements
// - is used by TSQLRestServer.URI for faster RESTful direct access
// - for JOINed SQL statements, the external database is also defined as
// a SQLite3 virtual table, via the TSQLVirtualTableExternal[Cursor] classes
TSQLRestStorageExternal = class(TSQLRestStorage)
protected
/// values retrieved from fStoredClassProps.ExternalDB settings
fTableName: RawUTF8;
fProperties: TSQLDBConnectionProperties;
fSelectOneDirectSQL, fSelectAllDirectSQL, fSelectTableHasRowsSQL: RawUTF8;
fRetrieveBlobFieldsSQL, fUpdateBlobfieldsSQL: RawUTF8;
// ID handling during Add/Insert
fEngineAddUseSelectMaxID: Boolean;
fEngineLockedMaxID: TID;
fOnEngineAddComputeID: TOnEngineAddComputeID;
fEngineAddForcedID: TID;
/// external column layout as retrieved by fProperties
// - used internaly to guess e.g. if the column is indexed
// - fFieldsExternal[] contains the external table info, and the internal
// column name is available via fFieldsExternalToInternal[]
fFieldsExternal: TSQLDBColumnDefineDynArray;
/// gives the index of each fFieldsExternal[] item in Props.Fields[]
// - is >=0 for index in Props.Fields[], -1 for RowID/ID, -2 if unknown
// - use InternalFieldNameToFieldExternalIndex() to convert from column name
fFieldsExternalToInternal: TIntegerDynArray;
/// gives the index of each in Props.Fields[]+1 in fFieldsExternal[]
// - expects [0] of RowID/ID, [1..length(fFieldNames)] for others
fFieldsInternalToExternal: TIntegerDynArray;
// multi-thread BATCH process is secured via Lock/UnLock critical section
fBatchMethod: TSQLURIMethod;
fBatchCapacity, fBatchCount: integer;
// BATCH sending uses TEXT storage for direct sending to database driver
fBatchValues: TRawUTF8DynArray;
fBatchIDs: TIDDynArray;
/// get fFieldsExternal[] index using fFieldsExternalToInternal[] mapping
// - do handle ID/RowID fields and published methods
function InternalFieldNameToFieldExternalIndex(
const InternalFieldName: RawUTF8): integer;
/// create, prepare and bound inlined parameters to a thread-safe statement
// - this implementation will call the ThreadSafeConnection virtual method,
// then bound inlined parameters as :(1234): and call its Execute method
// - should return nil on error, and not raise an exception
function PrepareInlinedForRows(const aSQL: RawUTF8): ISQLDBStatement;
/// overloaded method using FormatUTF8() and binding SynDB parameters
function PrepareDirectForRows(SQLFormat: PUTF8Char;
const Args, Params: array of const): ISQLDBStatement;
/// create, prepare, bound inlined parameters and execute a thread-safe statement
// - this implementation will call the ThreadSafeConnection virtual method,
// then bound inlined parameters as :(1234): and call its Execute method
// - should return nil on error, and not raise an exception
function ExecuteInlined(const aSQL: RawUTF8;
ExpectResults: Boolean): ISQLDBRows; overload;
/// overloaded method using FormatUTF8() and inlined parameters
function ExecuteInlined(SQLFormat: PUTF8Char; const Args: array of const;
ExpectResults: Boolean): ISQLDBRows; overload;
/// overloaded method using FormatUTF8() and binding SynDB parameters
function ExecuteDirect(SQLFormat: PUTF8Char; const Args, Params: array of const;
ExpectResults: Boolean): ISQLDBRows;
/// overloaded method using FormatUTF8() and binding SynDB parameters
function ExecuteDirectSQLVar(SQLFormat: PUTF8Char; const Args: array of const;
var Params: TSQLVarDynArray; const LastIntegerParam: Int64;
ParamsMatchCopiableFields: boolean): boolean;
// overridden methods calling the external engine with SQL via Execute
function EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; override;
function EngineExecute(const aSQL: RawUTF8): boolean; override;
function EngineLockedNextID: TID; virtual;
function EngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; override;
function EngineUpdate(TableModelIndex: integer; ID: TID; const
SentData: RawUTF8): boolean; override;
function EngineDeleteWhere(TableModelIndex: integer; const SQLWhere: RawUTF8;
const IDs: TIDDynArray): boolean; override;
function EngineList(const SQL: RawUTF8; ForceAJAX: Boolean=false;
ReturnedRowCount: PPtrInt=nil): RawUTF8; override;
// BLOBs should be access directly, not through slower JSON Base64 encoding
function EngineRetrieveBlob(TableModelIndex: integer; aID: TID;
BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean; override;
function EngineUpdateBlob(TableModelIndex: integer; aID: TID;
BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean; override;
function EngineSearchField(const FieldName: ShortString;
const FieldValue: array of const; out ResultID: TIDDynArray): boolean;
// overridden method returning TRUE for next calls to EngineAdd/Update/Delete
// will properly handle operations until InternalBatchStop is called
// BatchOptions is ignored with external DB (syntax are too much specific)
function InternalBatchStart(Method: TSQLURIMethod;
BatchOptions: TSQLRestBatchOptions): boolean; override;
// internal method called by TSQLRestServer.RunBatch() to process fast sending
// to remote database engine (e.g. Oracle bound arrays or MS SQL Bulk insert)
procedure InternalBatchStop; override;
/// called internally by EngineAdd/EngineUpdate/EngineDelete in batch mode
procedure InternalBatchAdd(const aValue: RawUTF8; const aID: TID);
/// TSQLRestServer.URI use it for Static.EngineList to by-pass virtual table
// - overridden method to handle most potential simple queries, e.g. like
// $ SELECT Field1,RowID FROM table WHERE RowID=... AND/OR/NOT Field2=
// - change 'RowID' into 'ID' column name, internal field names into
// mapped external field names ('AS [InternalFieldName]' if needed), and
// SQLTableName into fTableName
// - any 'LIMIT #' clause will be changed into the appropriate SQL statement
// - handle also statements to avoid slow virtual table full scan, e.g.
// $ SELECT count(*) FROM table
function AdaptSQLForEngineList(var SQL: RawUTF8): boolean; override;
/// run INSERT of UPDATE from the corresponding JSON object
// - Occasion parameter shall be only either soInsert or soUpate
// - each JSON field will be bound with the proper SQL type corresponding to
// the real external table columns (e.g. as TEXT for variant)
// - returns 0 on error, or the Updated/Inserted ID
function ExecuteFromJSON(const SentData: RawUTF8; Occasion: TSQLOccasion;
UpdatedID: TID): TID;
/// compute the INSERT or UPDATE statement as decoded from a JSON object
function JSONDecodedPrepareToSQL(var Decoder: TJSONObjectDecoder;
out ExternalFields: TRawUTF8DynArray; out Types: TSQLDBFieldTypeArray;
Occasion: TSQLOccasion; BatchOptions: TSQLRestBatchOptions;
BoundArray: boolean): RawUTF8;
function GetConnectionProperties: TSQLDBConnectionProperties;
/// check rpmClearPoolOnConnectionIssue in fStoredClassMapping.Options
function HandleClearPoolOnConnectionIssue: boolean;
public
/// initialize the remote database connection
// - you should not use this, but rather call VirtualTableExternalRegister()
// - RecordProps.ExternalDatabase will map the associated TSQLDBConnectionProperties
// - RecordProps.ExternalTableName will retrieve the real full table name,
// e.g. including any databas<e schema prefix
constructor Create(aClass: TSQLRecordClass; aServer: TSQLRestServer); override;
/// finalize the remote database connection
destructor Destroy; override;
/// delete a row, calling the external engine with SQL
// - made public since a TSQLRestStorage instance may be created
// stand-alone, i.e. without any associated Model/TSQLRestServer
function EngineDelete(TableModelIndex: integer; ID: TID): boolean; override;
/// search for a numerical field value
// - return true on success (i.e. if some values have been added to ResultID)
// - store the results into the ResultID dynamic array
function SearchField(const FieldName: RawUTF8; FieldValue: Int64;
out ResultID: TIDDynArray): boolean; overload; override;
/// search for a field value, according to its SQL content representation
// - return true on success (i.e. if some values have been added to ResultID)
// - store the results into the ResultID dynamic array
function SearchField(const FieldName, FieldValue: RawUTF8;
out ResultID: TIDDynArray): boolean; overload; override;
/// overridden method for direct external database engine call
function TableRowCount(Table: TSQLRecordClass): Int64; override;
/// overridden method for direct external database engine call
function TableHasRows(Table: TSQLRecordClass): boolean; override;
/// begin a transaction (implements REST BEGIN Member)
// - to be used to speed up some SQL statements like Insert/Update/Delete
// - must be ended with Commit on success
// - must be aborted with Rollback if any SQL statement failed
// - return true if no transaction is active, false otherwise
function TransactionBegin(aTable: TSQLRecordClass;
SessionID: cardinal=1): boolean; override;
/// end a transaction (implements REST END Member)
// - write all pending SQL statements to the external database
procedure Commit(SessionID: cardinal=1; RaiseException: boolean=false); override;
/// abort a transaction (implements REST ABORT Member)
// - restore the previous state of the database, before the call to TransactionBegin
procedure RollBack(SessionID: cardinal=1); override;
/// overridden method for direct external database engine call
function UpdateBlobFields(Value: TSQLRecord): boolean; override;
/// overridden method for direct external database engine call
function RetrieveBlobFields(Value: TSQLRecord): boolean; override;
/// update a field value of the external database
function EngineUpdateField(TableModelIndex: integer;
const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; override;
/// update a field value of the external database
function EngineUpdateFieldIncrement(TableModelIndex: integer; ID: TID;
const FieldName: RawUTF8; Increment: Int64): boolean; override;
/// create one index for all specific FieldNames at once
// - this method will in fact call the SQLAddIndex method, if the index
// is not already existing
// - for databases which do not support indexes on BLOB fields (i.e. all
// engine but SQLite3), such FieldNames will be ignored
function CreateSQLMultiIndex(Table: TSQLRecordClass; const FieldNames: array of RawUTF8;
Unique: boolean; IndexName: RawUTF8=''): boolean; override;
/// this method is called by TSQLRestServer.EndCurrentThread method just
// before a thread is finished to ensure that the associated external DB
// connection will be released for this thread
// - this overridden implementation will clean thread-specific connections,
// i.e. call TSQLDBConnectionPropertiesThreadSafe.EndCurrentThread method
// - this method shall be called directly, nor from the main thread
procedure EndCurrentThread(Sender: TThread); override;
/// reset the internal cache of external table maximum ID
// - next EngineAdd/BatchAdd will execute SELECT max(ID) FROM externaltable
// - is a lighter alternative to EngineAddUseSelectMaxID=TRUE, since this
// method may be used only once, when some records have been inserted into
// the external database outside this class scope (e.g. by legacy code)
procedure EngineAddForceSelectMaxID;
/// compute the SQL query corresponding to a prepared request
// - can be used internally e.g. for debugging purposes
function ComputeSQL(const Prepared: TSQLVirtualTablePrepared): RawUTF8;
/// retrieve the REST server instance corresponding to an external TSQLRecord
// - just map aServer.StaticVirtualTable[] and will return nil if not
// a TSQLRestStorageExternal
// - you can use it e.g. to call MapField() method in a fluent interface
class function Instance(aClass: TSQLRecordClass;
aServer: TSQLRestServer): TSQLRestStorageExternal;
/// retrieve the external database connection associated to a TSQLRecord
// - just map aServer.StaticVirtualTable[] and will return nil if not
// a TSQLRestStorageExternal
class function ConnectionProperties(aClass: TSQLRecordClass;
aServer: TSQLRestServer): TSQLDBConnectionProperties; overload;
/// disable internal ID generation for INSERT
// - by default, a new ID will be set (either with 'select max(ID)' or via
// the OnEngineLockedNextID event)
// - if the client supplies a forced ID within its JSON content, it would
// be used for adding
// - define this property to a non 0 value if no such ID is expected to be
// supplied, but a fixed "fake ID" is returned by the Add() method; at
// external DB level, no such ID field would be computed nor set at INSERT -
// this feature may be useful when working with a legacy database - of
// course any ID-based ORM method would probably fail to work
property EngineAddForcedID: TID read fEngineAddForcedID write fEngineAddForcedID;
/// define an alternate method of compute the ID for INSERT
// - by default, a new ID will be with 'select max(ID)', and an internal
// counter (unless EngineAddUseSelectMaxID is true)
// - you can specify a custom callback, which may compute the ID as
// expected (e.g. using a SQL sequence)
property OnEngineAddComputeID: TOnEngineAddComputeID read
fOnEngineAddComputeID write fOnEngineAddComputeID;
published
/// the associated external SynDB database connection properties
property Properties: TSQLDBConnectionProperties read GetConnectionProperties;
/// by default, any INSERT will compute the new ID from an internal variable
// - it is very fast and reliable, unless external IDs can be created
// outside this engine
// - you can set EngineAddUseSelectMaxID=true to execute a slower
// 'select max(ID) from TableName' SQL statement before each EngineAdd()
// - a lighter alternative may be to call EngineAddForceSelectMaxID only
// when required, i.e. when the external DB has just been modified
// by a third-party/legacy SQL process
property EngineAddUseSelectMaxID: Boolean read fEngineAddUseSelectMaxID
write fEngineAddUseSelectMaxID;
end;
/// A Virtual Table cursor for reading a TSQLDBStatement content
// - this is the cursor class associated to TSQLVirtualTableExternal
TSQLVirtualTableCursorExternal = class(TSQLVirtualTableCursor)
protected
fStatement: ISQLDBStatement;
fSQL: RawUTF8;
fHasData: boolean;
// on exception, release fStatement and optionally clear the pool
procedure HandleClearPoolOnConnectionIssue;
public
/// finalize the external cursor by calling ReleaseRows
destructor Destroy; override;
/// called to begin a search in the virtual table, creating a SQL query
// - the TSQLVirtualTablePrepared parameters were set by
// TSQLVirtualTable.Prepare and will contain both WHERE and ORDER BY statements
// (retrieved by x_BestIndex from a TSQLite3IndexInfo structure)
// - Prepared will contain all prepared constraints and the corresponding
// expressions in the Where[].Value field
// - will move cursor to first row of matching data
// - will return false on low-level database error (but true in case of a
// valid call, even if HasData will return false, i.e. no data match)
// - all WHERE and ORDER BY clauses are able to be translated into a plain
// SQL statement calling the external DB engine
// - will create the internal fStatement from a SQL query, bind the
// parameters, then execute it, ready to be accessed via HasData/Next
function Search(const Prepared: TSQLVirtualTablePrepared): boolean; override;
/// called to retrieve a column value of the current data row
// - if aColumn=VIRTUAL_TABLE_ROWID_COLUMN(-1), will return the row ID
// as varInt64 into aResult
// - will return false in case of an error, true on success
function Column(aColumn: integer; var aResult: TSQLVar): boolean; override;
/// called after Search() to check if there is data to be retrieved
// - should return false if reached the end of matching data
function HasData: boolean; override;
/// called to go to the next row of matching data
// - should return false on low-level database error (but true in case of a
// valid call, even if HasData will return false, i.e. no data match)
function Next: boolean; override;
/// read-only access to the SELECT statement
property SQL: RawUTF8 read fSQL;
end;
/// A SynDB-based virtual table for accessing any external database
// - for ORM access, you should use VirtualTableExternalRegister method to
// associate this virtual table module to any TSQLRecord class
// - transactions are handled by this module, according to the external database
TSQLVirtualTableExternal = class(TSQLVirtualTable)
public { overridden methods }
/// returns the main specifications of the associated TSQLVirtualTableModule
// - this is a read/write table, without transaction (yet), associated to the
// TSQLVirtualTableCursorExternal cursor type, with 'External' as module name
// and TSQLRestStorageExternal as the related static class
// - no particular class is supplied here, since it will depend on the
// associated Static TSQLRestStorageExternal instance
class procedure GetTableModuleProperties(
var aProperties: TVirtualTableModuleProperties); override;
/// called to determine the best way to access the virtual table
// - will prepare the request for TSQLVirtualTableCursor.Search()
// - this overridden method will let the external DB engine perform the search,
// using a standard SQL "SELECT * FROM .. WHERE .. ORDER BY .." statement
// - in Where[], Expr must be set to not 0 if needed for Search method,
// and OmitCheck always set to true since double check is not necessary
// - OmitOrderBy will be set to true since double sort is not necessary
// - EstimatedCost/EstimatedRows will receive the estimated cost, with
// lowest value if fStatic.fFieldsExternal[].ColumnIndexed is set
// (i.e. if column has an index)
function Prepare(var Prepared: TSQLVirtualTablePrepared): boolean; override;
/// called when a DROP TABLE statement is executed against the virtual table
// - returns true on success, false otherwise
function Drop: boolean; override;
/// called to delete a virtual table row
// - returns true on success, false otherwise
function Delete(aRowID: Int64): boolean; override;
/// called to insert a virtual table row content
// - column order follows the Structure method, i.e. StoredClassProps.Fields[] order
// - returns true on success, false otherwise
// - returns the just created row ID in insertedRowID on success
function Insert(aRowID: Int64; var Values: TSQLVarDynArray;
out insertedRowID: Int64): boolean; override;
/// called to update a virtual table row content
// - column order follows the Structure method, i.e. StoredClassProps.Fields[] order
// - returns true on success, false otherwise
function Update(oldRowID, newRowID: Int64;
var Values: TSQLVarDynArray): boolean; override;
end;
/// register on the Server-side an external database for an ORM class
// - will associate the supplied class with a TSQLVirtualTableExternal module
// (calling aModel.VirtualTableRegister method), even if the class does not
// inherit from TSQLRecordVirtualTableAutoID (it can be any plain TSQLRecord or
// TSQLRecordMany sub-class for instance)
// - note that TSQLModel.Create() will reset all supplied classes to be defined
// as non virtual (i.e. Kind=rSQLite3)
// - this function shall be called BEFORE TSQLRestServer.Create (the server-side
// ORM must know if the database is to be managed as internal or external)
// - this function (and the whole unit) is NOT to be used on the client-side
// - the TSQLDBConnectionProperties instance should be shared by all classes,
// and released globaly when the ORM is no longer needed
// - the full table name, as expected by the external database, could be
// provided here (SQLTableName will be used internaly as table name when
// called via the associated SQLite3 Virtual Table) - if no table name is
// specified (''), will use SQLTableName (e.g. 'Customer' for 'TSQLCustomer')
// - typical usage is therefore for instance:
// ! Props := TOleDBMSSQLConnectionProperties.Create('.\SQLEXPRESS','AdventureWorks2008R2','','');
// ! Model := TSQLModel.Create([TSQLCustomer],'root');
// ! VirtualTableExternalRegister(Model,TSQLCustomer,Props,'Sales.Customer');
// ! Server := TSQLRestServerDB.Create(aModel,'application.db'),true)
// - the supplied aExternalDB parameter is stored within aClass.RecordProps, so
// the instance must stay alive until all database access to this external table
// is finished (e.g. use a private/protected property)
// - aMappingOptions can be specified now, or customized later
// - server-side may omit a call to VirtualTableExternalRegister() if the need of
// an internal database is expected: it will allow custom database configuration
// at runtime, depending on the customer's expectations (or license)
// - after registration, you can tune the field-name mapping by calling
// ! aModel.Props[aClass].ExternalDB.MapField(..)
function VirtualTableExternalRegister(aModel: TSQLModel; aClass: TSQLRecordClass;
aExternalDB: TSQLDBConnectionProperties; const aExternalTableName: RawUTF8='';
aMappingOptions: TSQLRecordPropertiesMappingOptions=[]): boolean; overload;
/// register several tables of the model to be external
// - just a wrapper over the overloaded VirtualTableExternalRegister() method
function VirtualTableExternalRegister(aModel: TSQLModel;
const aClass: array of TSQLRecordClass; aExternalDB: TSQLDBConnectionProperties;
aMappingOptions: TSQLRecordPropertiesMappingOptions=[]): boolean; overload;
/// register one table of the model to be external, with optional mapping
// - this method would allow to chain MapField() or MapAutoKeywordFields
// definitions, in a fluent interface:
function VirtualTableExternalMap(aModel: TSQLModel;
aClass: TSQLRecordClass; aExternalDB: TSQLDBConnectionProperties;
const aExternalTableName: RawUTF8='';
aMapping: TSQLRecordPropertiesMappingOptions=[]): PSQLRecordPropertiesMapping;
type
/// all possible options for VirtualTableExternalRegisterAll/TSQLRestExternalDBCreate
// - by default, TSQLAuthUser and TSQLAuthGroup tables will be handled via the
// external DB, but you can avoid it for speed when handling session and security
// by setting regDoNotRegisterUserGroupTables
// - you can set regMapAutoKeywordFields to ensure that the mapped field names
// won't conflict with a SQL reserved keyword on the external database by
// mapping a name with a trailing '_' character for the external column
// - regClearPoolOnConnectionIssue will call ClearConnectionPool when a
// connection-linked exception is discovered
TVirtualTableExternalRegisterOption = (
regDoNotRegisterUserGroupTables,
regMapAutoKeywordFields,
regClearPoolOnConnectionIssue
);
/// set of options for VirtualTableExternalRegisterAll/TSQLRestExternalDBCreate functions
TVirtualTableExternalRegisterOptions = set of TVirtualTableExternalRegisterOption;
/// register all tables of the model to be external, with some options
// - by default, all tables are handled by the SQLite3 engine, unless they
// are explicitely declared as external via VirtualTableExternalRegister: this
// function can be used to register all tables to be handled by an external DBs
// - this function shall be called BEFORE TSQLRestServer.Create (the server-side
// ORM must know if the database is to be managed as internal or external)
// - this function (and the whole unit) is NOT to be used on the client-side
// - the TSQLDBConnectionProperties instance should be shared by all classes,
// and released globaly when the ORM is no longer needed
// - by default, TSQLAuthUser and TSQLAuthGroup tables will be handled via the
// external DB, but you can avoid it for speed when handling session and security
// by setting regDoNotRegisterUserGroupTables in aExternalOptions
// - other aExternalOptions can be defined to tune the ORM process e.g. about
// mapping or connection loss detection
// - after registration, you can tune the field-name mapping by calling
// ! aModel.Props[aClass].ExternalDB.MapField(..)
function VirtualTableExternalRegisterAll(aModel: TSQLModel;
aExternalDB: TSQLDBConnectionProperties;
aExternalOptions: TVirtualTableExternalRegisterOptions): boolean; overload;
/// register all tables of the model to be external
// - mainly for retro-compatibility with existing code
// - just a wrapper around the VirtualTableExternalRegisterAll() overloaded
// function with some boolean flags instead of TVirtualTableExternalRegisterOptions
function VirtualTableExternalRegisterAll(aModel: TSQLModel;
aExternalDB: TSQLDBConnectionProperties; DoNotRegisterUserGroupTables: boolean=false;
ClearPoolOnConnectionIssue: boolean=false): boolean; overload;
/// create a new TSQLRest instance, and possibly an external database, from its
// Model and stored values
// - if aDefinition.Kind matches a TSQLRest registered class, one new instance
// of this kind will be created and returned
// - if aDefinition.Kind is a registered TSQLDBConnectionProperties class name,
// it will instantiate an in-memory TSQLRestServerDB or a TSQLRestServerFullMemory
// instance, then call VirtualTableExternalRegisterAll() on this connection
// - will return nil if the supplied aDefinition does not match any registered
// TSQLRest or TSQLDBConnectionProperties types
function TSQLRestExternalDBCreate(aModel: TSQLModel;
aDefinition: TSynConnectionDefinition; aHandleAuthentication: boolean;
aExternalOptions: TVirtualTableExternalRegisterOptions): TSQLRest; overload;
implementation
function VirtualTableExternalRegister(aModel: TSQLModel; aClass: TSQLRecordClass;
aExternalDB: TSQLDBConnectionProperties; const aExternalTableName: RawUTF8;
aMappingOptions: TSQLRecordPropertiesMappingOptions): boolean;
var ExternalTableName: RawUTF8;
Props: TSQLModelRecordProperties;
begin
result := False;
if (aModel=nil) or (aClass=nil) or (aExternalDB=nil) then
exit; // avoid GPF
Props := aModel.Props[aClass];
if Props=nil then
exit; // if aClass is not part of the model
Props.Kind := rCustomAutoID; // force creation use of SQLite3 virtual table
if aExternalTableName='' then
ExternalTableName := Props.Props.SQLTableName else
ExternalTableName := aExternalTableName;
result := aModel.VirtualTableRegister(aClass,TSQLVirtualTableExternal,
aExternalDB.SQLFullTableName(ExternalTableName),aExternalDB,aMappingOptions);
end;
function VirtualTableExternalRegister(aModel: TSQLModel;
const aClass: array of TSQLRecordClass; aExternalDB: TSQLDBConnectionProperties;
aMappingOptions: TSQLRecordPropertiesMappingOptions): boolean;
var i: integer;
begin
result := true;
for i := 0 to High(aClass) do
if not VirtualTableExternalRegister(aModel,aClass[i],
aExternalDB,'',aMappingOptions) then
result := false;
end;
function VirtualTableExternalRegisterAll(aModel: TSQLModel;
aExternalDB: TSQLDBConnectionProperties;
DoNotRegisterUserGroupTables, ClearPoolOnConnectionIssue: boolean): boolean;
var opt: TVirtualTableExternalRegisterOptions;
begin
opt := []; // to call the overloaded function below with proper options
if DoNotRegisterUserGroupTables then
include(opt,regDoNotRegisterUserGroupTables);
if ClearPoolOnConnectionIssue then
include(opt,regClearPoolOnConnectionIssue);
result := VirtualTableExternalRegisterAll(aModel,aExternalDB,opt);
end;
function VirtualTableExternalRegisterAll(aModel: TSQLModel;
aExternalDB: TSQLDBConnectionProperties;
aExternalOptions: TVirtualTableExternalRegisterOptions): boolean;
var i: PtrInt;
rec: TSQLRecordClass;
opt: TSQLRecordPropertiesMappingOptions;
begin
result := (aModel<>nil) and (aExternalDB<>nil);
if not result then
exit; // avoid GPF
opt := [];
if regClearPoolOnConnectionIssue in aExternalOptions then
include(opt,rpmClearPoolOnConnectionIssue);
for i := 0 to high(aModel.Tables) do begin
rec := aModel.Tables[i];
if (regDoNotRegisterUserGroupTables in aExternalOptions) and
(rec.InheritsFrom(TSQLAuthGroup) or rec.InheritsFrom(TSQLAuthUser)) then
continue else
if not VirtualTableExternalRegister(aModel,rec,aExternalDB,'',opt) then
result := false else
if regMapAutoKeywordFields in aExternalOptions then
aModel.TableProps[i].ExternalDB.MapAutoKeywordFields;
end;
end;
function VirtualTableExternalMap(aModel: TSQLModel;
aClass: TSQLRecordClass; aExternalDB: TSQLDBConnectionProperties;
const aExternalTableName: RawUTF8;
aMapping: TSQLRecordPropertiesMappingOptions): PSQLRecordPropertiesMapping;
begin
if VirtualTableExternalRegister(aModel,aClass,aExternalDB,aExternalTableName,aMapping) then
result := @aModel.Props[aClass].ExternalDB else
result := nil;
end;
function TSQLRestExternalDBCreate(aModel: TSQLModel;
aDefinition: TSynConnectionDefinition; aHandleAuthentication: boolean;
aExternalOptions: TVirtualTableExternalRegisterOptions): TSQLRest;
var propsClass: TSQLDBConnectionPropertiesClass;
props: TSQLDBConnectionProperties;
begin
result := nil;
if aDefinition=nil then
exit;
propsClass := TSQLDBConnectionProperties.ClassFrom(aDefinition);
if propsClass<>nil then begin
props := nil;
try // aDefinition.Kind was a TSQLDBConnectionProperties -> all external DB
props := propsClass.Create(aDefinition.ServerName,aDefinition.DatabaseName,
aDefinition.User,aDefinition.PassWordPlain);
VirtualTableExternalRegisterAll(aModel,props,aExternalOptions);
result := TSQLRestServer.CreateInMemoryForAllVirtualTables(
aModel,aHandleAuthentication);
except
FreeAndNil(result);
props.Free; // avoid memory leak
end;
end else
// not external DB -> try if aDefinition.Kind is a TSQLRest class
result := TSQLRest.CreateTryFrom(aModel,aDefinition,aHandleAuthentication);
end;
{ TSQLRestStorageExternal }
constructor TSQLRestStorageExternal.Create(aClass: TSQLRecordClass;
aServer: TSQLRestServer);
procedure FieldsInternalInit;
var i,n,int: integer;
begin
n := length(fFieldsExternal);
SetLength(fFieldsExternalToInternal,n);
with fStoredClassMapping^ do begin
SetLength(fFieldsInternalToExternal,length(ExtFieldNames)+1);
for i := 0 to high(fFieldsInternalToExternal) do
fFieldsInternalToExternal[i] := -1;
for i := 0 to n-1 do begin
int := ExternalToInternalIndex(fFieldsExternal[i].ColumnName);
fFieldsExternalToInternal[i] := int;
inc(int); // fFieldsInternalToExternal[0]=RowID, then follows fFieldsExternal[]
if int>=0 then
fFieldsInternalToExternal[int] := i;
end;
end;
end;
function PropInfoToExternalField(Prop: TSQLPropInfo;
var Column: TSQLDBColumnCreate): boolean;
const
mORMotType: array[TSQLFieldType] of TSQLDBFieldType =
// ftUnknown is used for Int32 values, ftInt64 for Int64 values
(ftUnknown, // sftUnknown
ftUTF8, // sftAnsiText
ftUTF8, // sftUTF8Text
ftUnknown, // sftEnumerate
ftInt64, // sftSet
ftInt64, // sftInteger
ftInt64, // sftID
ftInt64, // sftRecord
ftUnknown, // sftBoolean
ftDouble, // sftFloat
ftDate, // sftDateTime
ftInt64, // sftTimeLog
ftCurrency, // sftCurrency
ftUTF8, // sftObject
{$ifndef NOVARIANTS}
ftUTF8, // sftVariant
ftUTF8, // sftNullable (retrieved from Prop.SQLFieldTypeStored)
{$endif NOVARIANTS}
ftBlob, // sftBlob
ftBlob, // sftBlobDynArray
ftBlob, // sftBlobCustom
ftUTF8, // sftUTF8Comp
ftInt64, // sftMany
ftInt64, // sftModTime
ftInt64, // sftCreateTime
ftInt64, // sftTID
ftInt64, // sftRecordVersion
ftInt64, // sftSessionUserID
ftDate, // sftDateTimeMS
ftInt64, // sftUnixTime
ftInt64); // sftUnixMSTime
begin
if Prop.SQLFieldType in [sftUnknown,sftMany] then begin
result := false;
exit; // ignore unknown/virtual fields
end;
Column.DBType := mORMotType[Prop.SQLFieldTypeStored];
Column.Name := fStoredClassMapping^.ExtFieldNames[Prop.PropertyIndex];
if Column.DBType=ftUTF8 then
Column.Width := Prop.FieldWidth else
Column.Width := 0;
Column.Unique := aIsUnique in Prop.Attributes;
Column.PrimaryKey := false;
result := true;
end;
var SQL: RawUTF8;
i,f: integer;
nfo: TSQLPropInfo;
Field: TSQLDBColumnCreate;
TableCreated,FieldAdded: Boolean;
CreateColumns: TSQLDBColumnCreateDynArray;
options: TSQLRecordPropertiesMappingOptions;
log: TSynLog;
procedure GetFields;
begin
fProperties.GetFields(UnQuotedSQLSymbolName(fTableName),fFieldsExternal);
log.Log(sllDebug,'GetFields',TypeInfo(TSQLDBColumnDefineDynArray),fFieldsExternal,self);
end;
function FieldsExternalIndexOf(const ColName: RawUTF8): integer;
begin
if rpmMissingFieldNameCaseSensitive in options then begin
for result := 0 to high(fFieldsExternal) do
if fFieldsExternal[result].ColumnName=ColName then
exit;
end else
for result := 0 to high(fFieldsExternal) do
if IdemPropNameU(fFieldsExternal[result].ColumnName,ColName) then
exit;
result := -1;
end;
begin
{$ifdef WITHLOG}
log := Owner.LogClass.Add;
log.Enter('Create %',[aClass],self);
{$else}
log := nil;
{$endif}
inherited Create(aClass,aServer);
// initialize external DB properties
options := fStoredClassMapping^.Options;
fTableName := fStoredClassMapping^.TableName;
fProperties := fStoredClassMapping^.ConnectionProperties as TSQLDBConnectionProperties;
log.Log(sllInfo,'% as % % Server=%',[StoredClass,fTableName,fProperties,Owner],self);
if fProperties=nil then
raise EBusinessLayerException.CreateUTF8(
'%.Create: no external DB defined for %',[self,StoredClass]);
// ensure external field names are compatible with the external DB keywords
for f := 0 to StoredClassRecordProps.Fields.Count-1 do begin
nfo := StoredClassRecordProps.Fields.List[f];
if nfo.SQLFieldType in COPIABLE_FIELDS then begin // ignore sftMany
SQL := fStoredClassMapping^.ExtFieldNames[f];
if rpmQuoteFieldName in options then
fStoredClassMapping^.MapField(nfo.Name,'"'+SQL+'"') else
if fProperties.IsSQLKeyword(SQL) then begin
log.Log(sllWarning,'%.%: Field name % is not compatible with %',
[fStoredClass,nfo.Name,SQL,fProperties.DBMSEngineName],self);
if rpmAutoMapKeywordFields in options then begin
log.Log(sllWarning,'-> %.% mapped to %_',[fStoredClass,nfo.Name,SQL],self);
fStoredClassMapping^.MapField(nfo.Name,SQL+'_');
end else
log.Log(sllWarning,'-> you should better use MapAutoKeywordFields',self);
end;
end;
end;
// create corresponding external table if necessary, and retrieve its fields info
TableCreated := false;
GetFields;
if not (rpmNoCreateMissingTable in options) then
if fFieldsExternal=nil then begin
// table is not yet existing -> try to create it
with aClass.RecordProps do begin
SetLength(CreateColumns,Fields.Count+1);
CreateColumns[0].Name := fStoredClassMapping^.RowIDFieldName;
CreateColumns[0].DBType := ftInt64;
CreateColumns[0].Unique := true;
CreateColumns[0].NonNullable := true;
CreateColumns[0].PrimaryKey := true;
f := 1;
for i := 0 to Fields.Count-1 do
if PropInfoToExternalField(Fields.List[i],CreateColumns[f]) then
inc(f);
if f<>Length(CreateColumns) then
SetLength(CreateColumns,f); // just ignore non handled field types
end;
SQL := fProperties.SQLCreate(fTableName,CreateColumns,false);
if SQL<>'' then
if ExecuteDirect(pointer(SQL),[],[],false)<>nil then begin
GetFields;
if fFieldsExternal=nil then
raise EORMException.CreateUTF8('%.Create: external table creation % failed:'+
' GetFields() returned nil - SQL="%"',[self,StoredClass,fTableName,SQL]);
TableCreated := true;
end;
end;
FieldsInternalInit;
// create any missing field if necessary
if not (rpmNoCreateMissingField in options) then
if not TableCreated then begin
FieldAdded := false;
with StoredClassRecordProps do
for f := 0 to Fields.Count-1 do
if Fields.List[f].SQLFieldType in COPIABLE_FIELDS then // ignore sftMany
/// real database columns exist for Simple + Blob fields (not Many)
if FieldsExternalIndexOf(fStoredClassMapping^.ExtFieldNamesUnQuotedSQL[f])<0 then begin
// add new missing Field
Finalize(Field);
FillcharFast(Field,sizeof(Field),0);
if PropInfoToExternalField(Fields.List[f],Field) then begin
SQL := fProperties.SQLAddColumn(fTableName,Field);
if (SQL<>'') and (ExecuteDirect(pointer(SQL),[],[],false)<>nil) then
FieldAdded := true else
raise EORMException.CreateUTF8('%.Create: %: unable to create external '+
'missing field %.% - SQL="%"',
[self,StoredClass,fTableName,Fields.List[f].Name,SQL]);
end;
end;
if FieldAdded then begin
GetFields; // get from DB after ALTER TABLE
FieldsInternalInit;
end;
end;
// compute the SQL statements used internaly for external DB requests
with fStoredClassMapping^ do begin
fSelectOneDirectSQL := FormatUTF8('select % from % where %=?',
[SQL.TableSimpleFields[true,false],fTableName,RowIDFieldName]);
fSelectAllDirectSQL := FormatUTF8('select %,% from %',
[SQL.InsertSet,RowIDFieldName,fTableName]);
fRetrieveBlobFieldsSQL := InternalCSVToExternalCSV(
StoredClassRecordProps.SQLTableRetrieveBlobFields);
fUpdateBlobFieldsSQL := InternalCSVToExternalCSV(
StoredClassRecordProps.SQLTableUpdateBlobFields,'=?,','=?');
end;
fSelectTableHasRowsSQL := FormatUTF8('select ID from % limit 1',
[StoredClassRecordProps.SQLTableName]);
AdaptSQLForEngineList(fSelectTableHasRowsSQL);
end;
destructor TSQLRestStorageExternal.Destroy;
begin
inherited Destroy;
end;
function TSQLRestStorageExternal.AdaptSQLForEngineList(var SQL: RawUTF8): boolean;
var Stmt: TSynTableStatement;
W: TTextWriter;
limit: TSQLDBDefinitionLimitClause;
limitSQL,name: RawUTF8;
f,n: integer;
tmp: TTextWriterStackBuffer;
begin
result := false;
if SQL='' then
exit;
Stmt := TSynTableStatement.Create(SQL,fStoredClassRecordProps.Fields.IndexByName,
fStoredClassRecordProps.SimpleFieldsBits[soSelect]);
try
if (Stmt.SQLStatement='') or // parsing failed
not IdemPropNameU(Stmt.TableName,fStoredClassRecordProps.SQLTableName) then begin
InternalLog('AdaptSQLForEngineList: complex statement -> switch to '+
'SQLite3 virtual engine - check efficiency',[],sllDebug);
exit;
end;
if Stmt.Offset<>0 then begin
InternalLog('AdaptSQLForEngineList: unsupported OFFSET for [%]',
[SQL],sllWarning);
exit;
end;
if Stmt.Limit=0 then
limit.Position := posNone else begin
limit := fProperties.SQLLimitClause(Stmt);
if limit.Position=posNone then begin
InternalLog('AdaptSQLForEngineList: unknown % LIMIT syntax for [%]',
[ToText(fProperties.DBMS)^,SQL],sllWarning);
exit;
end;
if limit.Position = posOuter then
FormatUTF8(limit.InsertFmt,['%', Stmt.Limit],limitSQL) else
FormatUTF8(limit.InsertFmt,[Stmt.Limit],limitSQL);
end;
W := TTextWriter.CreateOwnedStream(tmp);
try
W.AddShort('select ');
if limit.Position=posSelect then
W.AddString(limitSQL);
for f := 0 to high(Stmt.Select) do
with Stmt.Select[f] do begin
if FunctionName<>'' then begin
W.AddString(FunctionName);
W.Add('(');
end;
if FunctionKnown=funcCountStar then
W.Add('*') else begin
W.AddString(fStoredClassMapping^.FieldNameByIndex(Field-1));
W.AddString(SubField);
end;
if FunctionName<>'' then
W.Add(')');
if ToBeAdded<>0 then begin
if ToBeAdded>0 then
W.Add('+');
W.Add(ToBeAdded);
end;
if Alias<>'' then begin
W.AddShort(' as ');
W.AddString(Alias);
end else
if not (Field in fStoredClassMapping^.FieldNamesMatchInternal) then begin
if Field=0 then
name := 'ID' else // RowID may be reserved (e.g. for Oracle)
name := fStoredClassRecordProps.Fields.List[Field-1].Name;
W.AddShort(' as ');
if (FunctionName='') or (FunctionKnown in [funcDistinct,funcMax]) then
W.AddString(name) else begin
W.Add('"');
W.AddString(FunctionName);
W.Add('(');
W.AddString(name);
W.Add(')','"');
end;
end;
W.Add(',');
end;
W.CancelLastComma;
W.AddShort(' from ');
W.AddString(fTableName);
n := length(Stmt.Where);
if n=0 then begin
if limit.Position=posWhere then begin
W.AddShort(' where ');
W.AddString(limitSQL);
end;
end else begin
dec(n);
W.AddShort(' where ');
if limit.Position=posWhere then begin
W.AddString(limitSQL);
W.AddShort(' and ');
end;
for f := 0 to n do
with Stmt.Where[f] do begin
if (FunctionName<>'') or (Operator>high(DB_SQLOPERATOR)) then begin
InternalLog('AdaptSQLForEngineList: unsupported function %() for [%]',
[FunctionName,SQL],sllWarning);
exit;
end;
if f>0 then
if JoinedOR then
W.AddShort(' or ') else
W.AddShort(' and ');
if NotClause then
W.AddShort('not ');
if ParenthesisBefore<>'' then
W.AddString(ParenthesisBefore);
W.AddString(fStoredClassMapping^.FieldNameByIndex(Field-1));
W.AddString(SubField);
W.AddString(DB_SQLOPERATOR[Operator]);
if not (Operator in [opIsNull, opIsNotNull]) then
W.AddNoJSONEscape(ValueSQL,ValueSQLLen);
if ParenthesisAfter<>'' then
W.AddString(ParenthesisAfter);
end;
end;
if Stmt.GroupByField<>nil then begin
W.AddShort(' group by ');
for f := 0 to high(Stmt.GroupByField) do begin
W.AddString(fStoredClassMapping^.FieldNameByIndex(Stmt.GroupByField[f]-1));
W.Add(',');
end;
W.CancelLastComma;
end;
if Stmt.OrderByField<>nil then begin
W.AddShort(' order by ');
for f := 0 to high(Stmt.OrderByField) do begin
W.AddString(fStoredClassMapping^.FieldNameByIndex(Stmt.OrderByField[f]-1));
W.Add(',');
end;
W.CancelLastComma;
if Stmt.OrderByDesc then
W.AddShort(' desc');
end;
if limit.Position=posAfter then
W.AddString(limitSQL);
W.SetText(SQL);
if limit.Position=posOuter then
SQL := FormatUTF8(limitSQL,[SQL]);
result := true;
finally
W.Free;
end;
finally
Stmt.Free;
end;
end;
function TSQLRestStorageExternal.EngineLockedNextID: TID;
procedure RetrieveFromDB;
// fProperties.SQLCreate: ID Int64 PRIMARY KEY -> compute unique RowID
// (not all DB engines handle autoincrement feature - e.g. Oracle does not)
var rows: ISQLDBRows;
begin
rows := ExecuteDirect('select max(%) from %',
[fStoredClassMapping^.RowIDFieldName,fTableName],[],true);
if (rows<>nil) and rows.Step then
fEngineLockedMaxID := rows.ColumnInt(0) else
fEngineLockedMaxID := 0;
rows.ReleaseRows;
end;
var handled: boolean;
begin
if fEngineAddForcedID<>0 then begin
result := fEngineAddForcedID;
exit;
end;
if Assigned(fOnEngineAddComputeID) then begin
result := fOnEngineAddComputeID(self,handled);
if handled then
exit;
end;
if (fEngineLockedMaxID=0) or EngineAddUseSelectMaxID then
RetrieveFromDB;
inc(fEngineLockedMaxID);
result := fEngineLockedMaxID;
end;
function TSQLRestStorageExternal.InternalBatchStart(
Method: TSQLURIMethod; BatchOptions: TSQLRestBatchOptions): boolean;
const BATCH: array[mPOST..mDELETE] of TSQLDBStatementCRUD = (
cCreate, cUpdate, cDelete);
begin
result := false; // means BATCH mode not supported
if (self<>nil) and (method in [mPOST..mDELETE]) and
(BATCH[method] in fProperties.BatchSendingAbilities) then begin
StorageLock(true,'InternalBatchStart'); // protected by try..finally in TSQLRestServer.RunBatch
try
if fBatchMethod<>mNone then
raise EORMException.CreateUTF8('Missing previous %.InternalBatchStop(%)',
[self,StoredClass]);
fBatchMethod := Method;
fBatchCount := 0;
result := true; // means BATCH mode is supported
finally
if not result then
StorageUnLock;
end;
end;
end;
procedure TSQLRestStorageExternal.InternalBatchStop;
var i,j,n,max,BatchBegin,BatchEnd,ValuesMax: integer;
Query: ISQLDBStatement;
NotifySQLEvent: TSQLEvent;
SQL: RawUTF8;
P: PUTF8Char;
Fields, ExternalFields: TRawUTF8DynArray;
Types: TSQLDBFieldTypeArray;
Values: TRawUTF8DynArrayDynArray;
Occasion: TSQLOccasion;
Decode: TJSONObjectDecoder;
tmp: TSynTempBuffer;
begin
if fBatchMethod=mNone then
raise EORMException.CreateUTF8('%.InternalBatchStop(%).BatchMethod=mNone',
[self,StoredClass]);
try
if fBatchCount=0 then
exit; // nothing to do
if (Owner<>nil) and (fBatchMethod=mDelete) then // notify BEFORE deletion
for i := 0 to fBatchCount-1 do
Owner.InternalUpdateEvent(seDelete,fStoredClassProps.TableIndex,fBatchIDs[i],'',nil);
with fProperties do
if BatchMaxSentAtOnce>0 then
max := BatchMaxSentAtOnce else
max := 1000;
BatchBegin := 0;
BatchEnd := fBatchCount-1;
repeat
case fBatchMethod of
mPost, mPut: begin
assert(fBatchIDs<>nil);
BatchEnd := fBatchCount-1;
for i := BatchBegin to BatchEnd do begin
tmp.Init(fBatchValues[i]);
try
P := tmp.buf;
while P^ in [#1..' ','{','['] do inc(P);
if fBatchMethod=mPost then
Occasion := soInsert else
Occasion := soUpdate;
case Occasion of
soInsert: // mPost=INSERT with the supplied fields and computed ID
Decode.Decode(P,nil,pQuoted,fBatchIDs[i],true);
soUpdate: // mPut=UPDATE with the supplied fields and ID set appart
Decode.Decode(P,nil,pQuoted,0,true);
end;
RecordVersionFieldHandle(Occasion,Decode);
if Fields=nil then begin
Decode.AssignFieldNamesTo(Fields);
SQL := JSONDecodedPrepareToSQL(
Decode,ExternalFields,Types,Occasion,[],{array=}true);
SetLength(Values,Decode.FieldCount);
ValuesMax := fBatchCount-BatchBegin;
if ValuesMax>max then
ValuesMax := max;
for j := 0 to Decode.FieldCount-1 do
SetLength(Values[j],ValuesMax);
end else
if not Decode.SameFieldNames(Fields) then
break; // this item would break the SQL statement
n := i-BatchBegin;
for j := 0 to high(Fields) do
Values[j,n] := Decode.FieldValues[j]; // regroup by parameter
if Occasion=soUpdate then // ?=ID parameter
Values[length(Fields),n] := Int64ToUtf8(fBatchIDs[i]); // D2007 fails with var
BatchEnd := i; // mark fBatchValues[i] has to be copied in Values[]
if n+1>=max then
break; // do not send too much items at once, for better speed
finally
tmp.Done;
end;
end;
end;
mDelete: begin
if cPostgreBulkArray in fProperties.BatchSendingAbilities then
// for SynDBPostgres array binding
SQL := 'delete from % where %=ANY(?)' else
// regular SQL
SQL := 'delete from % where %=?';
SQL := FormatUTF8(SQL,[fTableName,fStoredClassMapping^.RowIDFieldName]);
n := BatchEnd-BatchBegin+1;
if n+1>=max then begin
n := max; // do not send too much items at once, for better speed
BatchEnd := BatchBegin+max-1;
end;
SetLength(Values,1);
SetLength(Values[0],n);
for i := 0 to n-1 do
Values[0,i] := Int64ToUTF8(fBatchIDs[BatchBegin+i]); // var fails on D2007
end;
end;
n := BatchEnd-BatchBegin+1;
if n<=0 then
break;
try
if (fBatchMethod=mPost) and Assigned(fProperties.OnBatchInsert) then
// use multiple insert dedicated function if available
fProperties.OnBatchInsert(
fProperties,fTableName,ExternalFields,Types,n,Values) else begin
// use array binding
Query := fProperties.NewThreadSafeStatementPrepared(SQL,{results=}false,{except=}true);
case fBatchMethod of
mPost, mPut:
for i := 0 to high(Values) do
Query.BindArray(i+1,Types[i],Values[i],n);
mDelete:
Query.BindArray(1,ftInt64,Values[0],n);
end;
Query.ExecutePrepared;
Query.ReleaseRows;
Query := nil;
end;
except
Query := nil;
HandleClearPoolOnConnectionIssue;
raise;
end;
if Owner<>nil then begin
// add/update/delete should flush DB cache
Owner.FlushInternalDBCache;
// force deletion coherency
if fBatchMethod=mDelete then
for i := 0 to high(Values) do
Owner.AfterDeleteForceCoherency(
fStoredClassProps.TableIndex,GetInt64(pointer(Values[i])));
end;
Fields := nil; // force new sending block
BatchBegin := BatchEnd+1;
until BatchBegin>=fBatchCount;
if Owner<>nil then begin
if fBatchMethod in [mPost,mPut] then begin
if fBatchMethod=mPost then
NotifySQLEvent := seAdd else
NotifySQLEvent := seUpdate;
for i := 0 to fBatchCount-1 do
Owner.InternalUpdateEvent(NotifySQLEvent,fStoredClassProps.TableIndex,
fBatchIDs[i],fBatchValues[i],nil);
end;
Owner.FlushInternalDBCache;
end;
finally
fBatchValues := nil;
fBatchIDs := nil;
fBatchCount := 0;
fBatchCapacity := 0;
fBatchMethod := mNone;
StorageUnLock;
end;
end;
procedure TSQLRestStorageExternal.InternalBatchAdd(
const aValue: RawUTF8; const aID: TID);
begin
if fBatchCount>=fBatchCapacity then begin
fBatchCapacity := fBatchCapacity+64+fBatchCount shr 3;
SetLength(fBatchIDs,fBatchCapacity);
if aValue<>'' then
SetLength(fBatchValues,fBatchCapacity);
end;
if aValue<>'' then
fBatchValues[fBatchCount] := aValue;
fBatchIDs[fBatchCount] := aID;
inc(fBatchCount);
end;
function TSQLRestStorageExternal.EngineAdd(TableModelIndex: integer;
const SentData: RawUTF8): TID;
begin
if (TableModelIndex<0) or (fModel.Tables[TableModelIndex]<>fStoredClass) then
result := 0 else // avoid GPF
if fBatchMethod<>mNone then
if fBatchMethod<>mPOST then
result := 0 else begin
if not JSONGetID(pointer(SentData),result) then
result := EngineLockedNextID else
if result>fEngineLockedMaxID then
fEngineLockedMaxID := result;
InternalBatchAdd(SentData,result);
end else begin
result := ExecuteFromJSON(SentData,soInsert,0);
// UpdatedID=0 -> insert with EngineLockedNextID
if (result>0) and (Owner<>nil) then begin
if EngineAddForcedID=0 then // only worth it if result is a true ID
Owner.InternalUpdateEvent(seAdd,TableModelIndex,result,SentData,nil);
Owner.FlushInternalDBCache;
end;
end;
end;
function TSQLRestStorageExternal.EngineUpdate(TableModelIndex: integer; ID: TID;
const SentData: RawUTF8): boolean;
begin
if (ID<=0) or (TableModelIndex<0) or (Model.Tables[TableModelIndex]<>fStoredClass) then
result := false else
if fBatchMethod<>mNone then
if fBatchMethod<>mPUT then
result := false else begin
InternalBatchAdd(SentData,ID);
result := true;
end else begin
result := ExecuteFromJSON(SentData,soUpdate,ID)=ID;
if result and (Owner<>nil) then begin
Owner.InternalUpdateEvent(seUpdate,TableModelIndex,ID,SentData,nil);
Owner.FlushInternalDBCache;
end;
end;
end;
function TSQLRestStorageExternal.EngineDelete(TableModelIndex: integer; ID: TID): boolean;
begin
if (ID<=0) or (TableModelIndex<0) or (Model.Tables[TableModelIndex]<>fStoredClass) then
result := false else
if fBatchMethod<>mNone then
if fBatchMethod<>mDELETE then
result := false else begin
InternalBatchAdd('',ID);
result := true;
end else begin
if Owner<>nil then // notify BEFORE deletion
Owner.InternalUpdateEvent(seDelete,TableModelIndex,ID,'',nil);
result := ExecuteDirect('delete from % where %=?',
[fTableName,fStoredClassMapping^.RowIDFieldName],[ID],false)<>nil;
if result and (Owner<>nil) then
Owner.FlushInternalDBCache;
end;
end;
function TSQLRestStorageExternal.EngineDeleteWhere(TableModelIndex: integer;
const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean;
const CHUNK_SIZE = 200;
var i,n,chunk,pos: integer;
rowid: RawUTF8;
begin
result := false;
if (IDs=nil) or (TableModelIndex<0) or (Model.Tables[TableModelIndex]<>fStoredClass) then
exit;
n := length(IDs);
if fBatchMethod<>mNone then
if fBatchMethod<>mDELETE then
exit else
for i := 0 to n-1 do
InternalBatchAdd('',IDs[i]) else begin
if Owner<>nil then // notify BEFORE deletion
for i := 0 to n-1 do
Owner.InternalUpdateEvent(seDelete,TableModelIndex,IDs[i],'',nil);
rowid := fStoredClassMapping^.RowIDFieldName;
pos := 0;
repeat // delete by chunks using primary key
chunk := n-pos;
if chunk=0 then
break;
if chunk>CHUNK_SIZE then
chunk := CHUNK_SIZE;
if ExecuteInlined('delete from % where % in (%)',[fTableName,rowid,
Int64DynArrayToCSV(pointer(@IDs[pos]),chunk)],false)=nil then
exit;
inc(pos,chunk);
until false;
if Owner<>nil then
Owner.FlushInternalDBCache;
end;
result := true;
end;
function TSQLRestStorageExternal.EngineList(const SQL: RawUTF8;
ForceAJAX: Boolean; ReturnedRowCount: PPtrInt): RawUTF8;
var Stmt: ISQLDBStatement;
begin
result := '';
if ReturnedRowCount<>nil then
raise ESQLDBException.CreateUTF8('%.EngineList(ReturnedRowCount<>nil) for %',
[self,StoredClass]);
Stmt := PrepareInlinedForRows(SQL);
if Stmt<>nil then
try
Stmt.ExecutePreparedAndFetchAllAsJSON(
ForceAJAX or (Owner=nil) or not Owner.NoAJAXJSON, result);
except
Stmt := nil;
HandleClearPoolOnConnectionIssue;
end;
end;
function TSQLRestStorageExternal.EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8;
var Stmt: ISQLDBStatement;
begin // TableModelIndex is not useful here
result := '';
if (self=nil) or (ID<=0) then
exit;
Stmt := PrepareDirectForRows(pointer(fSelectOneDirectSQL),[],[ID]);
if Stmt<>nil then
try
Stmt.ExecutePreparedAndFetchAllAsJSON(true,result); // Expanded=true -> '[{"ID":10,...}]'#10
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);
except
Stmt := nil;
HandleClearPoolOnConnectionIssue;
end;
end;
function TSQLRestStorageExternal.EngineExecute(const aSQL: RawUTF8): boolean;
begin
if aSQL='' then
result := false else
result := ExecuteInlined(aSQL,false)<>nil;
end;
function TSQLRestStorageExternal.TableHasRows(Table: TSQLRecordClass): boolean;
var rows: ISQLDBRows;
begin
if (self=nil) or (Table<>fStoredClass) then
result := false else begin
rows := ExecuteDirect(pointer(fSelectTableHasRowsSQL),[],[],true);
if rows=nil then
result := false else begin
result := rows.Step;
rows.ReleaseRows;
end;
end;
end;
function TSQLRestStorageExternal.TableRowCount(Table: TSQLRecordClass): Int64;
var rows: ISQLDBRows;
begin
if (self=nil) or (Table<>fStoredClass) then
result := 0 else begin
rows := ExecuteDirect('select count(*) from %',[fTableName],[],true);
if rows=nil then
result := 0 else begin
if not rows.Step then
result := 0 else
result := rows.ColumnInt(0);
rows.ReleaseRows;
end;
end;
end;
function TSQLRestStorageExternal.EngineRetrieveBlob(TableModelIndex: integer; aID: TID;
BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean;
var rows: ISQLDBRows;
begin
result := false;
if (aID<=0) or not BlobField^.IsBlob or
(TableModelIndex<0) or (Model.Tables[TableModelIndex]<>fStoredClass) then
exit;
with fStoredClassMapping^ do
rows := ExecuteDirect('select % from % where %=?',[InternalToExternal(BlobField^.Name),
fTableName,RowIDFieldName],[aID],{results=}true);
if (rows<>nil) and rows.Step then
try
BlobData := rows.ColumnBlob(0);
rows.ReleaseRows;
rows := nil;
result := true; // success
except
rows := nil;
HandleClearPoolOnConnectionIssue;
end;
end;
function TSQLRestStorageExternal.RetrieveBlobFields(Value: TSQLRecord): boolean;
var rows: ISQLDBRows;
f: Integer;
data: TSQLVar;
temp: RawByteString;
begin
result := false;
if (Value<>nil) and (Value.ID>0) and (PSQLRecordClass(Value)^=fStoredClass) then
with Value.RecordProps do
if BlobFields<>nil then begin
rows := ExecuteDirect('select % from % where %=?',
[fRetrieveBlobFieldsSQL,fTableName,fStoredClassMapping^.RowIDFieldName],
[Value.ID],true);
if (rows<>nil) and rows.Step then
try
for f := 0 to High(BlobFields) do begin
rows.ColumnToSQLVar(f,data,temp);
BlobFields[f].SetFieldSQLVar(Value,data);
end;
rows.ReleaseRows;
rows := nil;
result := true; // success
except
HandleClearPoolOnConnectionIssue;
end;
end;
end;
function TSQLRestStorageExternal.EngineUpdateField(TableModelIndex: integer;
const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean;
var rows: ISQLDBRows;
ExtWhereFieldName, JSON: RawUTF8;
begin
if (TableModelIndex<0) or (Model.Tables[TableModelIndex]<>fStoredClass) then
result := false else
with fStoredClassMapping^ do begin
ExtWhereFieldName := InternalToExternal(WhereFieldName);
result := ExecuteInlined('update % set %=:(%): where %=:(%):',
[fTableName,InternalToExternal(SetFieldName),SetValue,
ExtWhereFieldName,WhereValue],false)<>nil;
if result and (Owner<>nil) then begin
if Owner.InternalUpdateEventNeeded(TableModelIndex) then begin
rows := ExecuteInlined('select % from % where %=:(%):',
[RowIDFieldName,fTableName,ExtWhereFieldName,WhereValue],true);
if rows=nil then
exit;
JSONEncodeNameSQLValue(SetFieldName,SetValue,JSON);
while rows.Step do
Owner.InternalUpdateEvent(seUpdate,TableModelIndex,rows.ColumnInt(0),JSON,nil);
rows.ReleaseRows;
end;
Owner.FlushInternalDBCache;
end;
end;
end;
function TSQLRestStorageExternal.EngineUpdateFieldIncrement(TableModelIndex: integer;
ID: TID; const FieldName: RawUTF8; Increment: Int64): boolean;
var extField: RawUTF8;
Value: Int64;
begin
result := false;
if (ID<=0) or (TableModelIndex<0) or (Model.Tables[TableModelIndex]<>fStoredClass) then
exit;
if (Owner<>nil) and Owner.InternalUpdateEventNeeded(TableModelIndex) then
result := OneFieldValue(fStoredClass,FieldName,'ID=?',[],[ID],Value) and
UpdateField(fStoredClass,ID,FieldName,[Value+Increment]) else
try
with fStoredClassMapping^ do begin
extField := InternalToExternal(FieldName);
result := ExecuteInlined('update % set %=%+:(%): where %=:(%):',
[fTableName,extField,extField,Increment,RowIDFieldName,ID],false)<>nil;
end;
if result and (Owner<>nil) then
Owner.FlushInternalDBCache;
except
HandleClearPoolOnConnectionIssue;
end;
end;
function TSQLRestStorageExternal.EngineUpdateBlob(TableModelIndex: integer; aID: TID;
BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean;
var Statement: ISQLDBStatement;
AffectedField: TSQLFieldBits;
begin
result := false;
if (aID<=0) or not BlobField^.IsBlob or
(TableModelIndex<0) or (Model.Tables[TableModelIndex]<>fStoredClass) then
exit;
try
if Owner<>nil then
Owner.FlushInternalDBCache;
with fStoredClassMapping^ do
Statement := fProperties.NewThreadSafeStatementPrepared(
'update % set %=? where %=?',[fTableName,InternalToExternal(BlobField^.Name),
RowIDFieldName],{results=}false,{except=}true);
if Statement<>nil then begin
if BlobData='' then
Statement.BindNull(1) else
Statement.BindBlob(1,BlobData); // fast explicit BindBlob() call
Statement.Bind(2,aID);
Statement.ExecutePrepared;
if Owner<>nil then begin
fStoredClassRecordProps.FieldBitsFromBlobField(BlobField,AffectedField);
Owner.InternalUpdateEvent(seUpdateBlob,TableModelIndex,aID,'',@AffectedField);
Owner.FlushInternalDBCache;
end;
result := true; // success
end;
except
Statement := nil;
HandleClearPoolOnConnectionIssue; // leave result=false to notify error
end;
end;
function TSQLRestStorageExternal.UpdateBlobFields(Value: TSQLRecord): boolean;
var f: integer;
aID: TID;
temp: array of RawByteString;
Params: TSQLVarDynArray;
begin
result := false;
if (Value<>nil) and (PSQLRecordClass(Value)^=fStoredClass) then
with Value.RecordProps do
if BlobFields<>nil then begin
aID := Value.ID;
if aID<=0 then
exit;
if Owner<>nil then
Owner.FlushInternalDBCache;
SetLength(Params,length(BlobFields));
SetLength(temp,length(BlobFields));
for f := 0 to high(Params) do
BlobFields[f].GetFieldSQLVar(Value,Params[f],temp[f]);
result := ExecuteDirectSQLVar('update % set % where %=?',
[fTableName,fUpdateBlobFieldsSQL,fStoredClassMapping^.RowIDFieldName],
Params,aID,false);
if result and (Owner<>nil) then begin
Owner.InternalUpdateEvent(seUpdateBlob,fStoredClassProps.TableIndex,aID,'',
@fStoredClassRecordProps.FieldBits[sftBlob]);
Owner.FlushInternalDBCache;
end;
end else
result := true; // as TSQLRest.UpdateblobFields()
end;
function TSQLRestStorageExternal.PrepareInlinedForRows(const aSQL: RawUTF8): ISQLDBStatement;
var stmt: ISQLDBStatement;
begin
result := nil; // returns nil interface on error
if self=nil then
exit;
try
stmt := fProperties.PrepareInlined(aSQL,true);
if (stmt<>nil) and (sftDateTimeMS in fStoredClassRecordProps.HasTypeFields) then
stmt.ForceDateWithMS := true;
result := stmt;
except
stmt := nil;
HandleClearPoolOnConnectionIssue;
end;
end;
function TSQLRestStorageExternal.ExecuteInlined(const aSQL: RawUTF8;
ExpectResults: Boolean): ISQLDBRows;
var stmt: ISQLDBStatement;
begin
result := nil; // returns nil interface on error
if self=nil then
exit;
if not ExpectResults and (Owner<>nil) then
Owner.FlushInternalDBCache; // add/update/delete should flush DB cache
try
stmt := fProperties.PrepareInlined(aSQL,ExpectResults);
if stmt=nil then
exit;
if ExpectResults and (sftDateTimeMS in fStoredClassRecordProps.HasTypeFields) then
stmt.ForceDateWithMS := true;
stmt.ExecutePrepared;
if not ExpectResults then
stmt.ReleaseRows;
result := stmt;
except
stmt := nil;
HandleClearPoolOnConnectionIssue; // leave result=nil to notify error
end;
end;
function TSQLRestStorageExternal.ExecuteInlined(SQLFormat: PUTF8Char;
const Args: array of const; ExpectResults: Boolean): ISQLDBRows;
begin
result := ExecuteInlined(FormatUTF8(SQLFormat,Args),ExpectResults);
end;
function TSQLRestStorageExternal.PrepareDirectForRows(SQLFormat: PUTF8Char;
const Args, Params: array of const): ISQLDBStatement;
var stmt: ISQLDBStatement;
begin
result := nil;
if self<>nil then
try
stmt := fProperties.NewThreadSafeStatementPrepared(SQLFormat,Args,{results=}true,{except=}true);
if stmt=nil then
exit;
stmt.Bind(Params);
if sftDateTimeMS in fStoredClassRecordProps.HasTypeFields then
stmt.ForceDateWithMS := true;
result := stmt;
except
stmt := nil;
HandleClearPoolOnConnectionIssue;
end;
end;
function TSQLRestStorageExternal.ExecuteDirect(SQLFormat: PUTF8Char;
const Args, Params: array of const; ExpectResults: Boolean): ISQLDBRows;
var stmt: ISQLDBStatement;
begin
result := nil;
if self=nil then
exit;
if not ExpectResults and (Owner<>nil) then
Owner.FlushInternalDBCache; // add/update/delete should flush DB cache
try
stmt := fProperties.NewThreadSafeStatementPrepared(SQLFormat,Args,ExpectResults,{except=}true);
stmt.Bind(Params);
if ExpectResults and (sftDateTimeMS in fStoredClassRecordProps.HasTypeFields) then
stmt.ForceDateWithMS := true;
stmt.ExecutePrepared;
if IdemPChar(SQLFormat, 'DROP TABLE ') then begin
fEngineLockedMaxID := 0;
end;
result := stmt;
except
stmt := nil;
HandleClearPoolOnConnectionIssue; // leave result=nil to notify error
end;
end;
function TSQLRestStorageExternal.ExecuteDirectSQLVar(SQLFormat: PUTF8Char;
const Args: array of const; var Params: TSQLVarDynArray; const LastIntegerParam: Int64;
ParamsMatchCopiableFields: boolean): boolean;
var stmt: ISQLDBStatement;
ParamsCount, f: integer;
begin
result := false;
if Self<>nil then
try
stmt := fProperties.NewThreadSafeStatementPrepared(SQLFormat,Args,{results=}false,{except=}true);
if stmt=nil then
exit;
ParamsCount := length(Params);
if ParamsMatchCopiableFields and
(ParamsCount<>Length(fStoredClassRecordProps.CopiableFields)) then
raise EORMException.CreateUTF8('%.ExecuteDirectSQLVar(ParamsMatchCopiableFields) for %',
[self,StoredClass]);
for f := 0 to ParamsCount-1 do
if ParamsMatchCopiableFields and
(fStoredClassRecordProps.CopiableFields[f].SQLFieldTypeStored in
[sftDateTime,sftDateTimeMS]) and
(Params[f].VType=ftUTF8) then
stmt.BindDateTime(f+1,Iso8601ToDateTimePUTF8Char(Params[f].VText)) else
stmt.Bind(f+1,Params[f]);
if LastIntegerParam<>0 then
stmt.Bind(ParamsCount+1,LastIntegerParam);
stmt.ExecutePrepared;
result := true;
except
stmt := nil;
HandleClearPoolOnConnectionIssue; // leave result=false to notify error
end;
end;
function TSQLRestStorageExternal.EngineSearchField(const FieldName: ShortString;
const FieldValue: array of const; out ResultID: TIDDynArray): boolean;
var n: Integer;
rows: ISQLDBRows;
begin
result := false;
try
n := 0;
rows := ExecuteDirect('select % from % where %=?',
[fStoredClassMapping^.RowIDFieldName,fTableName,FieldName],FieldValue,true);
if rows<>nil then begin
while rows.Step do
AddInt64(TInt64DynArray(ResultID),n,rows.ColumnInt(0));
rows.ReleaseRows;
end;
SetLength(ResultID,n);
result := n>0;
except
rows := nil;
HandleClearPoolOnConnectionIssue; // leave result=false to notify error
end;
end;
function TSQLRestStorageExternal.SearchField(const FieldName: RawUTF8;
FieldValue: Int64; out ResultID: TIDDynArray): boolean;
begin
result := EngineSearchField(FieldName,[FieldValue],ResultID);
end;
function TSQLRestStorageExternal.SearchField(const FieldName, FieldValue: RawUTF8;
out ResultID: TIDDynArray): boolean;
begin
result := EngineSearchField(FieldName,[FieldValue],ResultID);
end;
function TSQLRestStorageExternal.TransactionBegin(
aTable: TSQLRecordClass; SessionID: cardinal): boolean;
begin
if (aTable=fStoredClass) and inherited TransactionBegin(aTable,SessionID) then
result := fProperties.SharedTransaction(SessionID,transBegin)<>nil else
result := false;
end;
procedure TSQLRestStorageExternal.Commit(SessionID: cardinal; RaiseException: boolean);
const ACTION: array[boolean] of TSQLDBSharedTransactionAction = (
transCommitWithoutException, transCommitWithException);
begin
inherited Commit(SessionID,RaiseException);
// reset fTransactionActive + write all TSQLVirtualTableJSON
fProperties.SharedTransaction(SessionID,ACTION[RaiseException]);
end;
procedure TSQLRestStorageExternal.RollBack(SessionID: cardinal);
begin
inherited RollBack(SessionID); // reset fTransactionActive
fProperties.SharedTransaction(SessionID,transRollback);
end;
function TSQLRestStorageExternal.CreateSQLMultiIndex(
Table: TSQLRecordClass; const FieldNames: array of RawUTF8;
Unique: boolean; IndexName: RawUTF8): boolean;
var SQL: RawUTF8;
ExtFieldNames: TRawUTF8DynArray;
IntFieldIndex: TIntegerDynArray;
Descending: boolean;
i,n,extfield: integer;
begin
result := false;
Descending := false;
n := length(FieldNames);
if (self=nil) or (fProperties=nil) or (Table<>fStoredClass) or (n<=0) then
exit;
fStoredClassMapping^.InternalToExternalDynArray(FieldNames,ExtFieldNames,@IntFieldIndex);
if n=1 then begin // handle case of index over a single column
if IntFieldIndex[0]<0 then // ID/RowID?
case fProperties.DBMS of
dSQLite, // SQLite3 always generates an index for ID/RowID
dPostgreSQL,dMSSQL,dMySQL,dOracle,dNexusDB: begin // as most DB on primary key
result := true;
exit;
end;
dFirebird: // see http://www.firebirdfaq.org/faq205
Descending := true;
end;
if not Descending then begin // we identify just if indexed, not the order
extfield := fFieldsInternalToExternal[IntFieldIndex[0]+1];
if (extfield>=0) and (fFieldsExternal[extfield].ColumnIndexed) then begin
result := true; // column already indexed
exit;
end;
end;
end;
if not (fProperties.DBMS in DB_HANDLEINDEXONBLOBS) then
// BLOB fields cannot be indexed (only in SQLite3+PostgreSQL)
for i := 0 to n-1 do begin
extfield := fFieldsInternalToExternal[IntFieldIndex[i]+1];
if (extfield>=0) and
(fFieldsExternal[extfield].ColumnType in [ftBlob,ftUTF8]) and
(fFieldsExternal[extfield].ColumnLength<=0) then begin
if i=0 then
exit; // impossible to create an index with no field!
SetLength(ExtFieldNames,i); // truncate index to the last indexable field
break;
end;
end;
SQL := fProperties.SQLAddIndex(fTableName,ExtFieldNames,Unique,Descending,IndexName);
if (SQL='') or (ExecuteDirect(pointer(SQL),[],[],false)=nil) then
exit;
result := true;
extfield := fFieldsInternalToExternal[IntFieldIndex[0]+1];
if extfield>=0 then // mark first column as indexed by now
fFieldsExternal[extfield].ColumnIndexed := true;
end;
class function TSQLRestStorageExternal.Instance(
aClass: TSQLRecordClass; aServer: TSQLRestServer): TSQLRestStorageExternal;
begin
if (aClass=nil) or (aServer=nil) then
result := nil else begin
result := TSQLRestStorageExternal(aServer.StaticVirtualTable[aClass]);
if result<>nil then
if not result.InheritsFrom(TSQLRestStorageExternal) then
result := nil;
end;
end;
class function TSQLRestStorageExternal.ConnectionProperties(
aClass: TSQLRecordClass; aServer: TSQLRestServer): TSQLDBConnectionProperties;
begin
result := Instance(aClass,aServer).GetConnectionProperties;
end;
function TSQLRestStorageExternal.GetConnectionProperties: TSQLDBConnectionProperties;
begin
if self=nil then
result := nil else
result := fProperties;
end;
function TSQLRestStorageExternal.HandleClearPoolOnConnectionIssue: boolean;
var conn: TSQLDBConnection;
begin
result := false;
if (self<>nil) and (fStoredClassMapping<>nil) and
(rpmClearPoolOnConnectionIssue in fStoredClassMapping.Options) then begin
conn := fProperties.ThreadSafeConnection;
if conn.LastErrorWasAboutConnection then begin
InternalLog('HandleClearPoolOnConnectionIssue: ClearConnectionPool after %',
[conn.LastErrorException],sllDB);
fProperties.ClearConnectionPool;
result := true;
end;
end;
end;
function TSQLRestStorageExternal.ExecuteFromJSON(
const SentData: RawUTF8; Occasion: TSQLOccasion; UpdatedID: TID): TID;
var Decoder: TJSONObjectDecoder;
SQL: RawUTF8;
Types: TSQLDBFieldTypeArray;
ExternalFields: TRawUTF8DynArray;
InsertedID: TID;
F: integer;
stmt: ISQLDBStatement;
begin
result := 0;
StorageLock(false,'ExecuteFromJson'); // avoid race condition against max(ID)
try
case Occasion of
soInsert:
if not JSONGetID(pointer(SentData),InsertedID) then
// no specified "ID":... field value -> compute next
InsertedID := EngineLockedNextID else
if InsertedID>fEngineLockedMaxID then
fEngineLockedMaxID := InsertedID;
soUpdate:
if UpdatedID<>0 then
InsertedID := 0 else
raise ESQLDBException.CreateUTF8('%.ExecuteFromJSON(%,soUpdate,UpdatedID=%)',
[self,StoredClass,UpdatedID]);
else raise ESQLDBException.CreateUTF8('%.ExecuteFromJSON(%,Occasion=%)?',
[self,StoredClass,ToText(Occasion)^]);
end;
// decode fields
if (fEngineAddForcedID<>0) and (InsertedID=fEngineAddForcedID) then
Decoder.Decode(SentData,nil,pNonQuoted,0,true) else
Decoder.Decode(SentData,nil,pNonQuoted,InsertedID,true);
if (Decoder.FieldCount=0) and (Occasion=soUpdate) then begin
result := UpdatedID; // SentData='' -> no column to update
exit;
end;
RecordVersionFieldHandle(Occasion,Decoder);
// compute SQL statement and associated bound parameters
SQL := JSONDecodedPrepareToSQL(
Decoder,ExternalFields,Types,Occasion,[],{array=}false);
if Occasion=soUpdate then // Int64ToUTF8(var) fails on D2007
Decoder.FieldValues[Decoder.FieldCount-1] := Int64ToUTF8(UpdatedID);
// execute statement
try
stmt := fProperties.NewThreadSafeStatementPrepared(SQL,{results=}false,{except=}true);
if stmt=nil then
exit;
for F := 0 to Decoder.FieldCount-1 do
if Decoder.FieldTypeApproximation[F]=ftaNull then
stmt.BindNull(F+1) else
stmt.Bind(F+1,Types[F],Decoder.FieldValues[F],true);
stmt.ExecutePrepared;
except
stmt := nil;
HandleClearPoolOnConnectionIssue;
exit; // leave result=0
end;
// mark success
if UpdatedID=0 then
result := InsertedID else
result := UpdatedID;
finally
StorageUnLock;
end;
end;
procedure TSQLRestStorageExternal.EndCurrentThread(Sender: TThread);
begin
if fProperties.InheritsFrom(TSQLDBConnectionPropertiesThreadSafe) then
TSQLDBConnectionPropertiesThreadSafe(fProperties).EndCurrentThread;
end;
function TSQLRestStorageExternal.InternalFieldNameToFieldExternalIndex(
const InternalFieldName: RawUTF8): integer;
begin
result := fStoredClassRecordProps.Fields.IndexByNameOrExcept(InternalFieldName);
result := fFieldsInternalToExternal[result+1];
end;
function TSQLRestStorageExternal.JSONDecodedPrepareToSQL(
var Decoder: TJSONObjectDecoder; out ExternalFields: TRawUTF8DynArray;
out Types: TSQLDBFieldTypeArray; Occasion: TSQLOccasion;
BatchOptions: TSQLRestBatchOptions; BoundArray: boolean): RawUTF8;
var f,k: Integer;
begin
SetLength(ExternalFields,Decoder.FieldCount);
for f := 0 to Decoder.FieldCount-1 do begin
k := fStoredClassRecordProps.Fields.IndexByNameOrExcept(Decoder.FieldNames[f]);
ExternalFields[f] := fStoredClassMapping^.FieldNameByIndex(k);
k := fFieldsInternalToExternal[k+1]; // retrieve exact Types[f] from SynDB
if k<0 then
raise ESQLDBException.CreateUTF8(
'%.JSONDecodedPrepareToSQL(%): No column for [%] field in table %',
[self,StoredClass,Decoder.FieldNames[f],fTableName]);
Types[f] := fFieldsExternal[k].ColumnType;
end;
// compute SQL statement and associated bound parameters
Decoder.DecodedFieldNames := pointer(ExternalFields);
if BoundArray and (cPostgreBulkArray in fProperties.BatchSendingAbilities) then
// SynDBPostgres array binding e.g. via 'insert into ... values (unnest...)'
Decoder.DecodedFieldTypesToUnnest := @Types;
result := Decoder.EncodeAsSQLPrepared(fTableName,Occasion,
fStoredClassMapping^.RowIDFieldName,BatchOptions);
if Occasion=soUpdate then
if Decoder.FieldCount=MAX_SQLFIELDS then
raise EParsingException.CreateUTF8('Too many fields for '+
'%.JSONDecodedPrepareToSQL',[self]) else begin
Types[Decoder.FieldCount] := ftInt64; // add "where ID=?" parameter
inc(Decoder.FieldCount);
end;
end;
procedure TSQLRestStorageExternal.EngineAddForceSelectMaxID;
begin
StorageLock(true,'EngineAddForceSelectMaxID');
fEngineLockedMaxID := 0;
StorageUnLock;
end;
const
SQL_OPER_WITH_PARAM: array[soEqualTo..soGreaterThanOrEqualTo] of RawUTF8 = (
'=?','<>?','<?','<=?','>?','>=?');
function TSQLRestStorageExternal.ComputeSQL(
const Prepared: TSQLVirtualTablePrepared): RawUTF8;
var i: integer;
constraint: PSQLVirtualTablePreparedConstraint;
{$ifdef SQLVIRTUALLOGS}
log: RawUTF8;
{$endif}
begin
result := fSelectAllDirectSQL;
for i := 0 to Prepared.WhereCount-1 do begin
constraint := @Prepared.Where[i];
{$ifdef SQLVIRTUALLOGS}
log := FormatUTF8('% [column=% oper=%]',
[log,constraint^.Column,ToText(constraint^.Operation)^]);
{$endif}
if constraint^.Operation>high(SQL_OPER_WITH_PARAM) then
exit; // invalid specified operator -> abort search
if i=0 then
result := result+' where ' else
result := result+' and ';
if fStoredClassMapping^.AppendFieldName(constraint^.Column,result) then
exit; // invalid column index -> abort search
result := result+SQL_OPER_WITH_PARAM[constraint^.Operation];
end;
// e.g. 'select FirstName,..,ID from PeopleExternal where FirstName=? and LastName=?'
for i := 0 to Prepared.OrderByCount-1 do
with Prepared.OrderBy[i] do begin
if i=0 then
result := result+' order by ' else
result := result+', ';
if fStoredClassMapping^.AppendFieldName(Column,result) then
exit; // invalid column index -> abort search
if Desc then
result := result+' desc';
end;
{$ifdef SQLVIRTUALLOGS}
SQLite3Log.Add.Log(sllDebug,'%.ComputeSQL [%] %',[ClassType,result,log],self);
{$endif}
end;
{ TSQLVirtualTableCursorExternal }
procedure TSQLVirtualTableCursorExternal.HandleClearPoolOnConnectionIssue;
begin
fStatement := nil;
fHasData := false;
if (self<>nil) and (Table<>nil) and (Table.Static<>nil) then
(Table.Static as TSQLRestStorageExternal).HandleClearPoolOnConnectionIssue;
end;
destructor TSQLVirtualTableCursorExternal.Destroy;
begin
if fStatement <> nil then
fStatement.ReleaseRows;
inherited Destroy;
end;
function TSQLVirtualTableCursorExternal.Column(aColumn: integer;
var aResult: TSQLVar): boolean;
var n: cardinal;
begin
result := false;
if (self<>nil) and (fStatement<>nil) then
try
n := fStatement.ColumnCount-1;
if aColumn=VIRTUAL_TABLE_ROWID_COLUMN then
aColumn := n else // RowID is latest column (select %,RowID from..)
if cardinal(aColumn)>=n then
exit; // error if aColumn is out of range
fStatement.ColumnToSQLVar(aColumn,aResult,fColumnTemp);
result := aResult.VType<>ftUnknown;
except
HandleClearPoolOnConnectionIssue;
end;
end;
function TSQLVirtualTableCursorExternal.HasData: boolean;
begin
result := (self<>nil) and (fStatement<>nil) and fHasData;
end;
function TSQLVirtualTableCursorExternal.Next: boolean;
begin
result := false;
if (self<>nil) and (fStatement<>nil) then
try
fHasData := fStatement.Step;
result := true; // success (may be with no more data)
except
HandleClearPoolOnConnectionIssue;
end;
end;
function TSQLVirtualTableCursorExternal.Search(
const Prepared: TSQLVirtualTablePrepared): boolean;
var i: integer;
storage: TSQLRestStorageExternal;
begin
result := false;
if (Self<>nil) and (Table<>nil) and (Table.Static<>nil) then begin
storage := Table.Static as TSQLRestStorageExternal;
{$ifndef SQLVIRTUALLOGS}
if fSQL='' then
{$endif}
fSQL := storage.ComputeSQL(Prepared);
try
fStatement := storage.fProperties.NewThreadSafeStatementPrepared(
fSQL,{results=}true,{except=}true);
if fStatement<>nil then begin
if sftDateTimeMS in storage.fStoredClassRecordProps.HasTypeFields then
fStatement.ForceDateWithMS := true;
for i := 1 to Prepared.WhereCount do
fStatement.Bind(i,Prepared.Where[i-1].Value);
fStatement.ExecutePrepared;
result := Next; // on execution success, go to the first row
end;
except
self.HandleClearPoolOnConnectionIssue;
end;
end;
end;
{ TSQLVirtualTableExternal }
function TSQLVirtualTableExternal.Drop: boolean;
begin
if (self=nil) or (Static=nil) then
result := false else
with Static as TSQLRestStorageExternal do
result := ExecuteDirect('drop table %',[fTableName],[],false)<>nil;
end;
class procedure TSQLVirtualTableExternal.GetTableModuleProperties(
var aProperties: TVirtualTableModuleProperties);
begin
aProperties.Features := [vtWrite];
aProperties.CursorClass := TSQLVirtualTableCursorExternal;
aProperties.StaticClass := TSQLRestStorageExternal;
end;
function TSQLVirtualTableExternal.Prepare(var Prepared: TSQLVirtualTablePrepared): boolean;
var i, col: integer;
Fields: TSQLPropInfoList;
begin
result := inherited Prepare(Prepared); // set costFullScan or costPrimaryIndex
if result and (Static<>nil) then
with Static as TSQLRestStorageExternal do begin
// mark Where[] clauses will be handled by SQL
Fields := StoredClassRecordProps.Fields;
result := false;
for i := 0 to Prepared.WhereCount-1 do
with Prepared.Where[i] do
if (Column<>VIRTUAL_TABLE_IGNORE_COLUMN) and
(Operation<=high(SQL_OPER_WITH_PARAM)) then begin
if Column=VIRTUAL_TABLE_ROWID_COLUMN then // is an indexed primary key
Prepared.EstimatedCost := costPrimaryIndex else begin
if cardinal(Column)>=cardinal(Fields.Count) then
exit; // invalid column index -> abort query
col := fFieldsInternalToExternal[Column+1];
if col<0 then
exit; // column not known in the external database -> abort query
if fFieldsExternal[col].ColumnIndexed then begin
if Prepared.EstimatedCost<costSecondaryIndex then
Prepared.EstimatedCost := costSecondaryIndex;
end else
if Prepared.EstimatedCost<costScanWhere then
Prepared.EstimatedCost := costScanWhere;
end;
OmitCheck := true; // search handled via SQL query
Value.VType := ftNull; // caller vt_BestIndex() expects <> ftUnknown
end;
// check the OrderBy[] clauses
if Prepared.OrderByCount>0 then begin
for i := 0 to Prepared.OrderByCount-1 do
with Prepared.OrderBy[i] do
if (Column<>VIRTUAL_TABLE_ROWID_COLUMN) and
(cardinal(Column)>=cardinal(Fields.Count)) then
exit; // invalid column index -> abort query
Prepared.OmitOrderBy := true; // order handled via SQL query
end;
result := true; // success
end;
end;
// here below, virtual write operations do not call Engine*() but direct SQL
// -> InternalUpdateEvent() were already called by MainEngine*() methods
function TSQLVirtualTableExternal.Delete(aRowID: Int64): boolean;
begin
if (self<>nil) and (Static<>nil) and (aRowID>0) then
with Static as TSQLRestStorageExternal do
result := ExecuteDirect('delete from % where %=?',
[fTableName,fStoredClassMapping^.RowIDFieldName],[aRowID],false)<>nil else
result := false;
end;
function TSQLVirtualTableExternal.Insert(aRowID: Int64;
var Values: TSQLVarDynArray; out insertedRowID: Int64): boolean;
begin // aRowID is just ignored here since IDs are always auto calculated
result := false;
if (self<>nil) and (Static<>nil) then
with Static as TSQLRestStorageExternal do begin
StorageLock(false,'Insert'); // to avoid race condition against max(RowID)
try
insertedRowID := EngineLockedNextID;
with fStoredClassMapping^ do
result := ExecuteDirectSQLVar('insert into % (%,%) values (%,?)',
[fTableName,SQL.InsertSet,RowIDFieldName,CSVOfValue('?',length(Values))],
Values,insertedRowID,true);
finally
StorageUnLock;
end;
end;
end;
function TSQLVirtualTableExternal.Update(oldRowID, newRowID: Int64;
var Values: TSQLVarDynArray): boolean;
begin
if (self<>nil) and (Static<>nil) and
(oldRowID=newRowID) and (newRowID>0) then // don't allow ID change
with Static as TSQLRestStorageExternal, fStoredClassMapping^ do
result := ExecuteDirectSQLVar('update % set % where %=?',
[fTableName,SQL.UpdateSetAll,RowIDFieldName],Values,oldRowID,true) else
result := false;
end;
initialization
// all our SynDB related functions shall log to main TSQLLog
SynDBLog := TSQLLog;
end.