2194 lines
88 KiB
ObjectPascal
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.
|