/// Common ORM and SOA classes 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 mORMot; (* This file is part of Synopse mORMot framework. Synopse mORMot framework. Copyright (C) 2020 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) 2020 the Initial Developer. All Rights Reserved. Contributor(s): Alexander (chaa) Alfred Glaenzer (alf) Daniel Kuettner DigDiver EgorovAlex Emanuele (lele9) Esmond Goran Despalatovic (gigo) Jordi Tudela Jean-Baptiste Roussia (jbroussia) Lagodny Maciej Izak (hnb) Martin Suer Michalis Kamburelis MilesYou Ondrej Pavel Mashlyakovskii (mpv) Sabbiolina Svetozar Belic (transmogrifix) Uian2000 Vadim Orel 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 {.$define PUREPASCAL} // define for debugg, not on production {$ifdef MSWINDOWS} {.$define ANONYMOUSNAMEDPIPE} // if defined, the security attributes of the named pipe will use an // anonymous connection - it should allow access to a service initialized // named pipe on a remote computer. // - I tried to implement the code as detailed in this Microsoft article: // http://support.microsoft.com/kb/813414 but it didn't work as // expected: see our forum https://synopse.info/forum/viewtopic.php?id=43 // - don't define it, because it's still buggy, and consider using HTTP // connection for remote access over the network {$define NOSECURITYFORNAMEDPIPECLIENTS} // define this may avoid issues with Delphi XE+ for obscure reasons {$define SSPIAUTH} // if defined, the Windows built-in authentication will be used // along with the usual one // - If you pass to TSQLRestClientURI.SetUser an empty string as user name, // the Windows authentication will be performed // - In this case, in table TSQLAuthUser should be an entry for the // windows user, with the LoginName in form 'DomainName\UserName' // - Setting NOSSPIAUTH conditional will disable this feature {$else} {$define GSSAPIAUTH} // if defined, the Kerberos authentication (using gssapi library) will be used // along with the usual one // - In this case, in table TSQLAuthUser should be an entry for the // domain user, with the LoginName in form 'username@DOMAIN.TLD' // - Linux server should joind a domain before using this // - KRB5_KTNAME environment variable or ServerForceKeytab shoud point // to valid readable keytab file with correct server credentials // - For more information on how to prepare server read an article on how to // configure MS SQL Server on Linux to use Windows authentication // https://www.mssqltips.com/sqlservertip/5075/configure-sql-server-on-linux-to-use-windows-authentication // - Setting NOGSSAPIAUTH conditional will disable this feature // Limitations: // - no NTLM support - this is a deprecated and vulnerable protocol {$endif} {$ifdef KYLIX3} {$define NOGSSAPIAUTH} // SynGSSAPI.pas unit is not Kylix-compatible {$endif} {$ifdef Android} {$define NOGSSAPIAUTH} // SynGSSAPI.pas unit is not Android-compatible [anymore] {$endif} {$ifdef SSPIAUTH} {$undef GSSAPIAUTH} // exclusive {$ifdef NOSSPIAUTH} {$undef SSPIAUTH} // force disable {$undef DOMAINAUTH} {$else} {$define DOMAINAUTH} {$endif} {$endif} {$ifdef GSSAPIAUTH} {$undef SSPIAUTH} // exclusive {$ifdef NOGSSAPIAUTH} {$undef GSSAPIAUTH} // force disable {$undef DOMAINAUTH} {$else} {$define DOMAINAUTH} {$endif} {$endif} interface uses {$ifdef MSWINDOWS} Windows, Messages, {$endif} {$ifdef KYLIX3} Types, LibC, SynKylix, {$endif} {$ifdef UNICODE} Generics.Collections, {$endif} Classes, SynZip, // use crc32 for TSQLRestClientURI.SetUser {$ifndef LVCL} SyncObjs, // for TEvent Contnrs, // for TObjectList {$ifndef NOVARIANTS} Variants, {$endif} {$endif LVCL} SysUtils, {$ifdef SSPIAUTH} SynSSPI, SynSSPIAuth, {$endif} {$ifdef GSSAPIAUTH} SynGSSAPI, SynGSSAPIAuth, {$endif} {$ifdef FPC} SynFPCTypInfo, // small wrapper unit around FPC's TypInfo.pp {$endif} SynCommons, SynTable, // for SynTable, TSynFilter and TSynValidate SynLog, SynCrypto, // SHA-256 and IProtocol SynTests; // for mocks integration { ************ low level types and constants for handling JSON and fields } { Why use JSON? (extracted from the main framework documentation) - The JavaScript Object Notation (JSON) is a lightweight computer data interchange format - Like XML, it's a text-based, human-readable format for representing simple data structures and associative arrays (called objects) - It's easier to read, quicker to implement and smaller in size than XML - It's a very efficient format for cache - It's natively supported by the JavaScript language, making it a perfect serialization format for any Ajax application - The JSON format is specified in http://tools.ietf.org/html/rfc4627 - The default text encoding for both JSON and SQLite3 is UTF-8, which allows the full Unicode charset to be stored and communicated - It is the default data format used by ASP.NET AJAX services created in Windows Communication Foundation (WCF) since .NET framework 3.5 - For binary blob transmission, we simply encode the binary data as hexa using the SQLite3 BLOB literals format : hexadecimal data preceded by a single "x" or "X" character (for example: X'53514C697465'), or Base64 encoding - see BlobToTSQLRawBlob() function } const /// maximum number of Tables in a Database Model // - this constant is used internaly to optimize memory usage in the // generated asm code // - you should not change it to a value lower than expected in an existing // database (e.g. as expected by TSQLAccessRights or such) MAX_SQLTABLES = 256; type /// this is the type to be used for our ORM primary key, i.e. TSQLRecord.ID // - it maps the SQLite3 RowID definition // - when converted to plain TSQLRecord published properties, you may loose // some information under Win32 when stored as a 32-bit pointer // - could be defined as value in a TSQLRecord property as such: // ! property AnotherRecord: TID read fAnotherRecord write fAnotherRecord; TID = type Int64; /// a pointer to a ORM primary key, i.e. TSQLRecord.ID: TID PID = ^TID; /// used to store a dynamic array of ORM primary keys, i.e. TSQLRecord.ID TIDDynArray = array of TID; /// pointer to a dynamic array of ORM primary keys, i.e. TSQLRecord.ID PIDDynArray = ^TIDDynArray; /// used to store bit set for all available Tables in a Database Model TSQLFieldTables = set of 0..MAX_SQLTABLES-1; /// a String used to store the BLOB content // - equals RawByteString for byte storage, to force no implicit charset // conversion, whatever the codepage of the resulting string is // - will identify a sftBlob field type, if used to define such a published // property // - by default, the BLOB fields are not retrieved or updated with raw // TSQLRest.Retrieve() method, that is "Lazy loading" is enabled by default // for blobs, unless TSQLRestClientURI.ForceBlobTransfert property is TRUE // (for all tables), or ForceBlobTransfertTable[] (for a particular table); // so use RetrieveBlob() methods for handling BLOB fields // - could be defined as value in a TSQLRecord property as such: // ! property Blob: TSQLRawBlob read fBlob write fBlob; TSQLRawBlob = type RawByteString; /// a reference to another record in any table in the database Model // - stored as a 64-bit signed integer (just like the TID type) // - type cast any value of TRecordReference with the RecordRef object below // for easy access to its content // - use TSQLRest.Retrieve(Reference) to get a record value // - don't change associated TSQLModel tables order, since TRecordReference // depends on it to store the Table type in its highest bits // - when the pointed record will be deleted, this property will be set to 0 // by TSQLRestServer.AfterDeleteForceCoherency() // - could be defined as value in a TSQLRecord property as such: // ! property AnotherRecord: TRecordReference read fAnotherRecord write fAnotherRecord; TRecordReference = type Int64; /// a reference to another record in any table in the database Model // - stored as a 64-bit signed integer (just like the TID type) // - type cast any value of TRecordReference with the RecordRef object below // for easy access to its content // - use TSQLRest.Retrieve(Reference) to get a record value // - don't change associated TSQLModel tables order, since TRecordReference // depends on it to store the Table type in its highest bits // - when the pointed record will be deleted, any record containg a matching // property will be deleted by TSQLRestServer.AfterDeleteForceCoherency() // - could be defined as value in a TSQLRecord property as such: // ! property AnotherRecord: TRecordReferenceToBeDeleted // ! read fAnotherRecord write fAnotherRecord; TRecordReferenceToBeDeleted = type TRecordReference; /// an Int64-encoded date and time of the latest update of a record // - can be used as published property field in TSQLRecord for sftModTime: // if any such property is defined in the table, it will be auto-filled with // the server timestamp corresponding to the latest record update // - use internally for computation an abstract "year" of 16 months of 32 days // of 32 hours of 64 minutes of 64 seconds - faster than TDateTime // - use TimeLogFromDateTime/TimeLogToDateTime/TimeLogNow/Iso8601ToTimeLog // functions, or type-cast the value with a TTimeLogBits memory structure for // direct access to its bit-oriented content (or via PTimeLogBits pointer) // - could be defined as value in a TSQLRecord property as such: // ! property LastModif: TModTime read fLastModif write fLastModif; TModTime = type TTimeLog; /// an Int64-encoded date and time of the record creation // - can be used as published property field in TSQLRecord for sftCreateTime: // if any such property is defined in the table, it will be auto-filled with // the server timestamp corresponding to the record creation // - use internally for computation an abstract "year" of 16 months of 32 days // of 32 hours of 64 minutes of 64 seconds - faster than TDateTime // - use TimeLogFromDateTime/TimeLogToDateTime/TimeLogNow/Iso8601ToTimeLog // functions, or type-cast the value with a TTimeLogBits memory structure for // direct access to its bit-oriented content (or via PTimeLogBits pointer) // - could be defined as value in a TSQLRecord property as such: // ! property CreatedAt: TModTime read fCreatedAt write fCreatedAt; TCreateTime = type TTimeLog; /// the Int64/TID of the TSQLAuthUser currently logged // - can be used as published property field in TSQLRecord for sftSessionUserID: // if any such property is defined in the table, it will be auto-filled with // the current TSQLAuthUser.ID value at update, or 0 if no session is running // - could be defined as value in a TSQLRecord property as such: // ! property User: TSessionUserID read fUser write fUser; TSessionUserID = type TID; /// a monotonic version number, used to track changes on a table // - add such a published field to any TSQLRecord will allow tracking of // record modifications - note that only a single field of this type should // be defined for a given record // - note that this published field is NOT part of the record "simple fields": // by default, the version won't be retrieved from the DB, nor will be sent // from a client - the Engine*() CRUD method will take care of computing the // monotonic version number, just before storage to the persistence engine // - such a field will use a separated TSQLRecordTableDeletion table to // track the deleted items // - could be defined as value in a TSQLRecord property as such: // ! property TrackedVersion: TRecordVersion read fVersion write fVersion; TRecordVersion = type Int64; /// the available types for any SQL field property, as managed with the // database driver // - sftUnknown: unknown or not defined field type // - sftAnsiText: a WinAnsi encoded TEXT, forcing a NOCASE collation // (TSQLRecord Delphi property was declared as AnsiString or string before // Delphi 2009) // - sftUTF8Text is UTF-8 encoded TEXT, forcing a SYSTEMNOCASE collation, // i.e. using UTF8IComp() (TSQLRecord property was declared as RawUTF8, // RawUnicode or WideString - or string in Delphi 2009+) - you may inherit // from TSQLRecordNoCase to use the NOCASE standard SQLite3 collation //- sftEnumerate is an INTEGER value corresponding to an index in any // enumerate Delphi type; storage is an INTEGER value (fast, easy and size // efficient); at display, this integer index will be converted into the // left-trimed lowercased chars of the enumerated type text conversion: // TOpenType(1) = otDone -> 'Done' /// - sftSet is an INTEGER value corresponding to a bitmapped set of // enumeration; storage is an INTEGER value (fast, easy and size efficient); // displayed as an integer by default, sets with an enumeration type with // up to 64 elements is allowed yet (stored as an Int64) // - sftInteger is an INTEGER (Int64 precision, as expected by SQLite3) field // - sftID is an INTEGER field pointing to the ID/RowID of another record of // a table, defined by the class type of the TSQLRecord inherited property; // coherency is always ensured: after a delete, all values pointing to // it is reset to 0 // - sftRecord is an INTEGER field pointing to the ID/RowID of another // record: TRecordReference=Int64 Delphi property which can be typecasted to // RecordRef; coherency is always ensured: after a delete, all values // pointing to it are reset to 0 by the ORM // - sftBoolean is an INTEGER field for a boolean value: 0 is FALSE, // anything else TRUE (encoded as JSON 'true' or 'false' constants) // - sftFloat is a FLOAT (floating point double precision, cf. SQLite3) // field, defined as double (or single) published properties definition // - sftDateTime is a ISO 8601 encoded (SQLite3 compatible) TEXT field, // corresponding to a TDateTime Delphi property: a ISO8601 collation is // forced for such column, for proper date/time sorting and searching // - sftDateTimeMS is a ISO 8601 encoded (SQLite3 compatible) TEXT field, // corresponding to a TDateTimeMS Delphi property, i.e. a TDateTime with // millisecond resolution, serialized with '.sss' suffix: a ISO8601 collation // is forced for such column, for proper date/time sorting and searching // - sftTimeLog is an INTEGER field for coding a date and time (not SQLite3 // compatible), which should be defined as TTimeLog=Int64 Delphi property, // ready to be typecasted to the TTimeLogBits optimized type for efficient // timestamp storage, with a second resolution // - sftCurrency is a FLOAT containing a 4 decimals floating point value, // compatible with the Currency Delphi type, which minimizes rounding errors // in monetary calculations which may occur with sftFloat type // - sftObject is a TEXT containing an ObjectToJSON serialization, able to // handle published properties of any not TPersistent as JSON object, // TStrings or TRawUTF8List as JSON arrays of strings, TCollection or // TObjectList as JSON arrays of JSON objects // - sftVariant is a TEXT containing a variant value encoded as JSON: // string values are stored between quotes, numerical values directly stored, // and JSON objects or arrays will be handled as TDocVariant custom types // - sftNullable is a INTEGER/DOUBLE/TEXT field containing a NULLable value, // stored as a local variant property, identifying TNullableInteger, // TNullableBoolean, TNullableFloat, TNullableCurrency, // TNullableDateTime, TNullableTimeLog and TNullableUTF8Text types // - sftBlob is a BLOB field (TSQLRawBlob Delphi property), and won't be // retrieved by default (not part of ORM "simple types"), to save bandwidth // - sftBlobDynArray is a dynamic array, stored as BLOB field: this kind of // property will be retrieved by default, i.e. is recognized as a "simple // field", and will use Base64 encoding during JSON transmission, or a true // JSON array, depending on the database back-end (e.g. MongoDB) // - sftBlobCustom is a custom property, stored as BLOB field: such // properties are defined by adding a TSQLPropInfoCustom instance, overriding // TSQLRecord.InternalRegisterCustomProperties virtual method - they will // be retrieved by default, i.e. recognized as "simple fields" // - sftUTF8Custom is a custom property, stored as JSON in a TEXT field, // defined by overriding TSQLRecord.InternalRegisterCustomProperties // virtual method, and adding a TSQLPropInfoCustom instance, e.g. via // RegisterCustomPropertyFromTypeName() or RegisterCustomPropertyFromRTTI(); // they will be retrieved by default, i.e. recognized as "simple fields" // - sftMany is a 'many to many' field (TSQLRecordMany Delphi property); // nothing is stored in the table row, but in a separate pivot table: so // there is nothing to retrieve here; in contrast to other TSQLRecord // published properties, which contains an INTEGER ID, the TSQLRecord.Create // will instanciate a true TSQLRecordMany instance to handle this pivot table // via its dedicated ManyAdd/FillMany/ManySelect methods - as a result, such // properties won't be retrieved by default, i.e. not recognized as "simple // fields" unless you used the dedicated methods // - sftModTime is an INTEGER field containing the TModTime value, aka time // of the record latest update; TModTime (just like TTimeLog or TCreateTime) // published property can be typecasted to the TTimeLogBits memory structure; // the value of this field is automatically updated with the current // date and time each time a record is updated (with external DB, it will // use the Server time, as retrieved from SynDB) - see ComputeFieldsBeforeWrite // virtual method of TSQLRecord; note also that only RESTful PUT/POST access // will change this field value: manual SQL statements (like // 'UPDATE Table SET Column=0') won't change its content; note also that // this is automated on Delphi client side, so only within TSQLRecord ORM use // (a pure AJAX application should fill such fields explicitely before sending) // - sftCreateTime is an INTEGER field containing the TCreateTime time // of the record creation; TCreateTime (just like TTimeLog or TModTime) // published property can be typecasted to the TTimeLogBits memory structure; // the value of this field is automatically updated with the current // date and time when the record is created (with external DB, it will // use the Server time, as retrieved from SynDB) - see ComputeFieldsBeforeWrite // virtual method of TSQLRecord; note also that only RESTful PUT/POST access // will set this field value: manual SQL statements (like // 'INSERT INTO Table ...') won't set its content; note also that this is // automated on Delphi client side, so only within TSQLRecord ORM use (a // pure AJAX application should fill such fields explicitely before sending) // - sftTID is an INTEGER field containing a TID pointing to another record; // since regular TSQLRecord published properties (i.e. sftID kind of field) // can not be greater than 2,147,483,647 (i.e. a signed 32-bit value) under // Win32, defining TID published properties will allow to store the ID // as signed 64-bit, e.g. up to 9,223,372,036,854,775,808; despite to // sftID kind of record, coherency is NOT ensured: after a deletion, all // values pointing to are NOT reset to 0 - it is up to your business logic // to ensure data coherency as expected // - sftRecordVersion is an INTEGER field containing a TRecordVersion // monotonic number: adding such a published field to any TSQLRecord will // allow tracking of record modifications, at storage level; by design, // such a field won't be part of "simple types", so won't be transmitted // between the clients and the server, but will be updated at any write // operation by the low-level Engine*() storage methods - such a field // will use a TSQLRecordTableDeletion table to track the deleted items // - sftSessionUserID is an INTEGER field containing the TSQLAuthUser.ID // of the record modification; the value of this field is automatically // updated with the current User ID of the active session; note also that // only RESTful PUT/POST access will change this field value: manual SQL // statements (like 'UPDATE Table SET Column=0') won't change its content; // this is automated on Delphi client side, so only within TSQLRecord ORM use // (a pure AJAX application should fill such fields explicitely before sending) // - sftUnixTime is an INTEGER field for coding a date and time as second-based // Unix Time (SQLite3 compatible), which should be defined as TUnixTime=Int64 // TSQLRecord property // - sftUnixMSTime is an INTEGER field for coding a date and time as // millisecond-based Unix Time (JavaScript compatible), which should be // defined as TUnixMSTime=Int64 TSQLRecord property // - WARNING: do not change the order of items below, otherwise some methods // (like TSQLRecordProperties.CheckBinaryHeader) may be broken and fail TSQLFieldType = ( sftUnknown, sftAnsiText, sftUTF8Text, sftEnumerate, sftSet, sftInteger, sftID, sftRecord, sftBoolean, sftFloat, sftDateTime, sftTimeLog, sftCurrency, sftObject, {$ifndef NOVARIANTS} sftVariant, sftNullable, {$endif} sftBlob, sftBlobDynArray, sftBlobCustom, sftUTF8Custom, sftMany, sftModTime, sftCreateTime, sftTID, sftRecordVersion, sftSessionUserID, sftDateTimeMS, sftUnixTime, sftUnixMSTime); /// set of available SQL field property types TSQLFieldTypes = set of TSQLFieldType; //// a fixed array of SQL field property types TSQLFieldTypeArray = array[0..MAX_SQLFIELDS] of TSQLFieldType; /// contains the parameters used for sorting // - FieldCount is 0 if was never sorted // - used to sort data again after a successfull data update with TSQLTableJSON.FillFrom() TSQLTableSortParams = record Comp: TUTF8Compare; FieldCount, FieldIndex: integer; FieldType: TSQLFieldType; Asc: boolean; end; /// used to define the triggered Event types for TNotifySQLEvent // - some Events can be triggered via TSQLRestServer.OnUpdateEvent when // a Table is modified, and actions can be authorized via overriding the // TSQLRest.RecordCanBeUpdated method // - OnUpdateEvent is called BEFORE deletion, and AFTER insertion or update; it // should be used only server-side, not to synchronize some clients: the framework // is designed around a stateless RESTful architecture (like HTTP/1.1), in which // clients ask the server for refresh (see TSQLRestClientURI.UpdateFromServer) // - is used also by TSQLRecord.ComputeFieldsBeforeWrite virtual method TSQLEvent = ( seAdd, seUpdate, seDelete, seUpdateBlob); /// used to define the triggered Event types for TSQLRecordHistory // - TSQLRecordHistory.History will be used for heArchiveBlob // - TSQLRecordHistory.SentDataJSON will be used for other kind of events TSQLHistoryEvent = ( heAdd, heUpdate, heDelete, heArchiveBlob); /// used to defined the CRUD associated SQL statement of a command // - used e.g. by TSQLRecord.GetJSONValues methods and SimpleFieldsBits[] array // (in this case, soDelete is never used, since deletion is global for all fields) // - also used for cache content notification TSQLOccasion = ( soSelect, soInsert, soUpdate, soDelete); /// used to defined a set of CRUD associated SQL statement of a command TSQLOccasions = set of TSQLOccasion; const /// kind of fields not retrieved during normal query, update or adding // - by definition, BLOB are excluded to save transmission bandwidth // - by design, TSQLRecordMany properties are stored in an external pivot table // - by convenience, the TRecordVersion number is for internal use only NOT_SIMPLE_FIELDS: TSQLFieldTypes = [sftUnknown,sftBlob,sftMany,sftRecordVersion]; /// kind of fields which can be copied from one TSQLRecord instance to another COPIABLE_FIELDS: TSQLFieldTypes = [low(TSQLFieldType)..high(TSQLFieldType)] - [sftUnknown, sftMany]; /// kind of DB fields which will contain TEXT content when converted to JSON TEXT_DBFIELDS: TSQLDBFieldTypes = [ftUTF8,ftDate]; /// kind of fields which will contain pure TEXT values // - independently from the actual storage level // - i.e. will match RawUTF8, string, UnicodeString, WideString properties RAWTEXT_FIELDS: TSQLFieldTypes = [sftAnsiText,sftUTF8Text]; /// kind of fields which will be stored as TEXT values // - i.e. RAWTEXT_FIELDS and TDateTime/TDateTimeMS STRING_FIELDS: TSQLFieldTypes = [sftAnsiText,sftUTF8Text,sftUTF8Custom, sftDateTime,sftDateTimeMS]; {$ifndef NOVARIANTS} /// the SQL field property types with their TNullable* equivalency // - those types may be stored in a variant published property, e.g. // ! property Int: TNullableInteger read fInt write fInt; // ! property Txt: TNullableUTF8Text read fTxt write fTxt; // ! property Txt: TNullableUTF8Text index 32 read fTxt write fTxt; NULLABLE_TYPES = [sftInteger,sftBoolean,sftEnumerate,sftFloat,sftCurrency, sftDateTime,sftTimeLog,sftUTF8Text]; {$endif NOVARIANTS} /// similar to AddInt64() function, but for a TIDDynArray // - some random GPF were identified with AddInt64(TInt64DynArray(Values),...) // with the Delphi Win64 compiler procedure AddID(var Values: TIDDynArray; var ValuesCount: integer; Value: TID); overload; /// similar to AddInt64() function, but for a TIDDynArray // - some random GPF were identified with AddInt64(TInt64DynArray(Values),...) // with the Delphi Win64 compiler procedure AddID(var Values: TIDDynArray; Value: TID); overload; type /// the available options for TSQLRest.BatchStart() process // - boInsertOrIgnore will create 'INSERT OR IGNORE' statements instead of // plain 'INSERT' - by now, only the direct mORMotSQLite3 engine supports it // - boInsertOrUpdate will create 'INSERT OR REPLACE' statements instead of // plain 'INSERT' - by now, only the direct mORMotSQLite3 engine supports it // - boExtendedJSON will force the JSON to unquote the column names, // e.g. writing col1:...,col2:... instead of "col1":...,"col2"... // - boPostNoSimpleFields will avoid to send a TSQLRestBach.Add() with simple // fields as "SIMPLE":[val1,val2...] or "SIMPLE@tablename":[val1,val2...], // without the field names // - boPutNoCacheFlush won't force the associated Cache entry to be flushed: // it is up to the caller to ensure cache coherency // - boRollbackOnError will raise an exception and Rollback any transaction // if any step failed - default if to continue batch processs, but setting // a value <> 200/HTTP_SUCCESS in Results[] TSQLRestBatchOption = ( boInsertOrIgnore, boInsertOrReplace, boExtendedJSON, boPostNoSimpleFields, boPutNoCacheFlush, boRollbackOnError); /// a set of options for TSQLRest.BatchStart() process // - TJSONObjectDecoder will use it to compute the corresponding SQL TSQLRestBatchOptions = set of TSQLRestBatchOption; /// define how TJSONObjectDecoder.Decode() will handle JSON string values TJSONObjectDecoderParams = (pInlined, pQuoted, pNonQuoted); /// define how TJSONObjectDecoder.FieldTypeApproximation[] is identified TJSONObjectDecoderFieldType = ( ftaNumber,ftaBoolean,ftaString,ftaDate,ftaNull,ftaBlob,ftaObject,ftaArray); /// JSON object decoding and SQL generation, in the context of ORM process // - this is the main process for marshalling JSON into SQL statements // - used e.g. by GetJSONObjectAsSQL() function or ExecuteFromJSON and // InternalBatchStop methods {$ifdef USERECORDWITHMETHODS}TJSONObjectDecoder = record {$else}TJSONObjectDecoder = object{$endif} public /// contains the decoded field names FieldNames: array[0..MAX_SQLFIELDS-1] of RawUTF8; /// contains the decoded field values FieldValues: array[0..MAX_SQLFIELDS-1] of RawUTF8; /// Decode() will set each field type approximation // - will recognize also JSON_BASE64_MAGIC/JSON_SQLDATE_MAGIC prefix FieldTypeApproximation: array[0..MAX_SQLFIELDS-1] of TJSONObjectDecoderFieldType; /// number of fields decoded in FieldNames[] and FieldValues[] FieldCount: integer; /// set to TRUE if parameters are to be :(...): inlined InlinedParams: TJSONObjectDecoderParams; /// internal pointer over field names to be used after Decode() call // - either FieldNames, either Fields[] array as defined in Decode(), or // external names as set by TSQLRestStorageExternal.JSONDecodedPrepareToSQL DecodedFieldNames: PRawUTF8Array; /// the ID=.. value as sent within the JSON object supplied to Decode() DecodedRowID: TID; /// internal pointer over field types to be used after Decode() call // - to create 'INSERT INTO ... SELECT UNNEST(...)' or 'UPDATE ... FROM // SELECT UNNEST(...)' statements for very efficient bulk writes in a // PostgreSQL database // - as set by TSQLRestStorageExternal.JSONDecodedPrepareToSQL when // cPostgreBulkArray flag is detected (for SynDBPostgres) DecodedFieldTypesToUnnest: PSQLDBFieldTypeArray; /// decode the JSON object fields into FieldNames[] and FieldValues[] // - if Fields=nil, P should be a true JSON object, i.e. defined // as "COL1"="VAL1" pairs, stopping at '}' or ']'; otherwise, Fields[] // contains column names and expects a JSON array as "VAL1","VAL2".. in P // - P should be after the initial '{' or '[' character, i.e. at first field // - P returns the next object start or nil on unexpected end of input // - P^ buffer will let the JSON be decoded in-place, so consider using // the overloaded Decode(JSON: RawUTF8; ...) method // - FieldValues[] strings will be quoted and/or inlined depending on Params // - if RowID is set, a RowID column will be added within the returned content procedure Decode(var P: PUTF8Char; const Fields: TRawUTF8DynArray; Params: TJSONObjectDecoderParams; const RowID: TID=0; ReplaceRowIDWithID: Boolean=false); overload; /// decode the JSON object fields into FieldNames[] and FieldValues[] // - overloaded method expecting a RawUTF8 buffer, making a private copy // of the JSON content to avoid unexpected in-place modification, then // calling Decode(P: PUTF8Char) to perform the process procedure Decode(const JSON: RawUTF8; const Fields: TRawUTF8DynArray; Params: TJSONObjectDecoderParams; const RowID: TID=0; ReplaceRowIDWithID: Boolean=false); overload; /// can be used after Decode() to add a new field in FieldNames/FieldValues // - so that EncodeAsSQL() will include this field in the generated SQL // - caller should ensure that the FieldName is not already defined in // FieldNames[] (e.g. when the TRecordVersion field is forced) // - the caller should ensure that the supplied FieldValue will match // the quoting/inlining expectations of Decode(TJSONObjectDecoderParams) - // e.g. that string values are quoted if needed procedure AddFieldValue(const FieldName,FieldValue: RawUTF8; FieldType: TJSONObjectDecoderFieldType); /// encode as a SQL-ready INSERT or UPDATE statement // - after a successfull call to Decode() // - escape SQL strings, according to the official SQLite3 documentation // (i.e. ' inside a string is stored as '') // - if InlinedParams was TRUE, it will create prepared parameters like // 'COL1=:("VAL1"):, COL2=:(VAL2):' // - called by GetJSONObjectAsSQL() function or TSQLRestStorageExternal function EncodeAsSQL(Update: boolean): RawUTF8; /// encode as a SQL-ready INSERT or UPDATE statement with ? as values // - after a successfull call to Decode() // - FieldValues[] content will be ignored // - Occasion can be only soInsert or soUpdate // - for soUpdate, will create UPDATE ... SET ... where UpdateIDFieldName=? // - you can specify some options, e.g. boInsertOrIgnore for soInsert function EncodeAsSQLPrepared(const TableName: RawUTF8; Occasion: TSQLOccasion; const UpdateIDFieldName: RawUTF8; BatchOptions: TSQLRestBatchOptions): RawUTF8; /// encode the FieldNames/FieldValues[] as a JSON object procedure EncodeAsJSON(out result: RawUTF8); /// set the specified array to the fields names // - after a successfull call to Decode() procedure AssignFieldNamesTo(var Fields: TRawUTF8DynArray); /// returns TRUE if the specified array match the decoded fields names // - after a successfull call to Decode() function SameFieldNames(const Fields: TRawUTF8DynArray): boolean; /// search for a field name in the current identified FieldNames[] function FindFieldName(const FieldName: RawUTF8): integer; end; /// set the TID (=64-bit integer) value from the numerical text stored in P^ // - just a redirection to SynCommons.SetInt64() procedure SetID(P: PUTF8Char; var result: TID); overload; {$ifdef HASINLINENOTX86}inline;{$endif} /// set the TID (=64-bit integer) value from the numerical text stored in U // - just a redirection to SynCommons.SetInt64() procedure SetID(const U: RawByteString; var result: TID); overload; {$ifdef HASINLINENOTX86}inline;{$endif} /// TDynArraySortCompare compatible function, sorting by TSQLRecord.ID function TSQLRecordDynArrayCompare(const Item1,Item2): integer; /// TDynArrayHashOne compatible function, hashing TSQLRecord.ID function TSQLRecordDynArrayHashOne(const Elem; Hasher: THasher): cardinal; /// decode JSON fields object into an UTF-8 encoded SQL-ready statement // - this function decodes in the P^ buffer memory itself (no memory allocation // or copy), for faster process - so take care that it is an unique string // - P should be after the initial '{' or '[' character, i.e. at first field // - P contains the next object start or nil on unexpected end of input // - if Fields is void, expects expanded "COL1"="VAL1" pairs in P^, stopping at '}' or ']' // - otherwise, Fields[] contains the column names and expects "VAL1","VAL2".. in P^ // - returns 'COL1="VAL1", COL2=VAL2' if UPDATE is true (UPDATE SET format) // - returns '(COL1, COL2) VALUES ("VAL1", VAL2)' otherwise (INSERT format) // - escape SQL strings, according to the official SQLite3 documentation // (i.e. ' inside a string is stored as '') // - if InlinedParams is set, will create prepared parameters like // 'COL1=:("VAL1"):, COL2=:(VAL2):' // - if RowID is set, a RowID column will be added within the returned content function GetJSONObjectAsSQL(var P: PUTF8Char; const Fields: TRawUTF8DynArray; Update, InlinedParams: boolean; RowID: TID=0; ReplaceRowIDWithID: Boolean=false): RawUTF8; overload; /// decode JSON fields object into an UTF-8 encoded SQL-ready statement // - is used e.g. by TSQLRestServerDB.EngineAdd/EngineUpdate methods // - expect a regular JSON expanded object as "COL1"="VAL1",...} pairs // - make its own temporary copy of JSON data before calling GetJSONObjectAsSQL() above // - returns 'COL1="VAL1", COL2=VAL2' if UPDATE is true (UPDATE SET format) // - returns '(COL1, COL2) VALUES ("VAL1", VAL2)' otherwise (INSERT format) // - if InlinedParams is set, will create prepared parameters like 'COL2=:(VAL2):' // - if RowID is set, a RowID column will be added within the returned content function GetJSONObjectAsSQL(const JSON: RawUTF8; Update, InlinedParams: boolean; RowID: TID=0; ReplaceRowIDWithID: Boolean=false): RawUTF8; overload; /// get the FIRST field value of the FIRST row, from a JSON content // - e.g. useful to get an ID without converting a JSON content into a TSQLTableJSON function UnJSONFirstField(var P: PUTF8Char): RawUTF8; /// returns TRUE if the JSON content is in expanded format // - i.e. as plain [{"ID":10,"FirstName":"John","LastName":"Smith"}...] // - i.e. not as '{"fieldCount":3,"values":["ID","FirstName","LastName",...']} function IsNotAjaxJSON(P: PUTF8Char): Boolean; /// retrieve a JSON '{"Name":Value,....}' object // - P is nil in return in case of an invalid object // - returns the UTF-8 encoded JSON object, including first '{' and last '}' // - if ExtractID is set, it will contain the "ID":203 field value, and this // field won't be included in the resulting UTF-8 encoded JSON object unless // KeepIDField is true // - this function expects this "ID" property to be the FIRST in the // "Name":Value pairs, as generated by TSQLRecord.GetJSONValues(W) function JSONGetObject(var P: PUTF8Char; ExtractID: PID; var EndOfObject: AnsiChar; KeepIDField: boolean): RawUTF8; /// retrieve the ID/RowID field of a JSON object // - this function expects this "ID" property to be the FIRST in the // "Name":Value pairs, as generated by TSQLRecord.GetJSONValues(W) // - returns TRUE if a ID/RowID>0 has been found, and set ID with the value function JSONGetID(P: PUTF8Char; out ID: TID): Boolean; /// fill a TSQLRawBlob from TEXT-encoded blob data // - blob data can be encoded as SQLite3 BLOB literals (X'53514C697465' e.g.) or // or Base-64 encoded content ('\uFFF0base64encodedbinary') or plain TEXT function BlobToTSQLRawBlob(P: PUTF8Char): TSQLRawBlob; overload; {$ifdef HASINLINE}inline;{$endif} /// fill a TSQLRawBlob from TEXT-encoded blob data // - blob data can be encoded as SQLite3 BLOB literals (X'53514C697465' e.g.) or // or Base-64 encoded content ('\uFFF0base64encodedbinary') or plain TEXT procedure BlobToTSQLRawBlob(P: PUTF8Char; var result: TSQLRawBlob); overload; /// fill a TSQLRawBlob from TEXT-encoded blob data // - blob data can be encoded as SQLite3 BLOB literals (X'53514C697465' e.g.) or // or Base-64 encoded content ('\uFFF0base64encodedbinary') or plain TEXT function BlobToTSQLRawBlob(const Blob: RawByteString): TSQLRawBlob; overload; /// create a TBytes from TEXT-encoded blob data // - blob data can be encoded as SQLite3 BLOB literals (X'53514C697465' e.g.) or // or Base-64 encoded content ('\uFFF0base64encodedbinary') or plain TEXT function BlobToBytes(P: PUTF8Char): TBytes; /// create a memory stream from TEXT-encoded blob data // - blob data can be encoded as SQLite3 BLOB literals (X'53514C697465' e.g.) or // or Base-64 encoded content ('\uFFF0base64encodedbinary') or plain TEXT // - the caller must free the stream instance after use function BlobToStream(P: PUTF8Char): TStream; /// creates a TEXT-encoded version of blob data from a TSQLRawBlob // - TEXT will be encoded as SQLite3 BLOB literals (X'53514C697465' e.g.) function TSQLRawBlobToBlob(const RawBlob: TSQLRawBlob): RawUTF8; overload; {$ifdef HASINLINE}inline;{$endif} /// creates a TEXT-encoded version of blob data from a memory data // - same as TSQLRawBlob, but with direct memory access via a pointer/byte size pair // - TEXT will be encoded as SQLite3 BLOB literals (X'53514C697465' e.g.) function TSQLRawBlobToBlob(RawBlob: pointer; RawBlobLength: integer): RawUTF8; overload; /// convert a Base64-encoded content into binary hexadecimal ready for SQL // - returns e.g. X'53514C697465' procedure Base64MagicToBlob(Base64: PUTF8Char; var result: RawUTF8); /// return true if the TEXT is encoded as SQLite3 BLOB literals (X'53514C697465' e.g.) function isBlobHex(P: PUTF8Char): boolean; {$ifdef HASINLINE}inline;{$endif} /// compute the SQL corresponding to a WHERE clause // - returns directly the Where value if it starts with one the // ORDER/GROUP/LIMIT/OFFSET/JOIN keywords // - otherwise, append ' WHERE '+Where function SQLFromWhere(const Where: RawUTF8): RawUTF8; /// find out if the supplied WHERE clause starts with one of the // ORDER/GROUP/LIMIT/OFFSET/JOIN keywords function SQLWhereIsEndClause(const Where: RawUTF8): boolean; /// compute 'PropName in (...)' where clause for a SQL statement // - if Values has no value, returns '' // - if Values has a single value, returns 'PropName="Values0"' or inlined // 'PropName=:("Values0"):' if ValuesInlined is true // - if Values has more than one value, returns 'PropName in ("Values0","Values1",...)' // or 'PropName in (:("Values0"):,:("Values1"):,...)' if length(Values)MaxValue) function GetEnumNameOrd(Value: Integer): PShortString; /// get the corresponding enumeration name // - return the first one if Value is invalid (>MaxValue) // - Value will be converted to the matching ordinal value (byte or word) function GetEnumName(const Value): PShortString; {$ifdef HASINLINE}inline;{$endif} /// retrieve all element names as a dynamic array of RawUTF8 // - names could be optionally trimmed left from their initial lower chars procedure GetEnumNameAll(var result: TRawUTF8DynArray; TrimLeftLowerCase: boolean); overload; /// retrieve all element names as CSV, with optional quotes procedure GetEnumNameAll(var result: RawUTF8; const Prefix: RawUTF8=''; quotedValues: boolean=false; const Suffix: RawUTF8=''; trimedValues: boolean=false; unCamelCased: boolean=false); overload; /// retrieve all trimed element names as CSV procedure GetEnumNameTrimedAll(var result: RawUTF8; const Prefix: RawUTF8=''; quotedValues: boolean=false; const Suffix: RawUTF8=''); /// get all enumeration names as a JSON array of strings function GetEnumNameAllAsJSONArray(TrimLeftLowerCase: boolean; UnCamelCased: boolean=false): RawUTF8; /// get the corresponding enumeration ordinal value, from its name // - if EnumName does start with lowercases 'a'..'z', they will be searched: // e.g. GetEnumNameValue('sllWarning') will find sllWarning item // - if Value does not start with lowercases 'a'..'z', they will be ignored: // e.g. GetEnumNameValue('Warning') will find sllWarning item // - return -1 if not found (don't use directly this value to avoid any GPF) function GetEnumNameValue(const EnumName: ShortString): Integer; overload; {$ifdef HASINLINE}inline;{$endif} /// get the corresponding enumeration ordinal value, from its name // - if Value does start with lowercases 'a'..'z', they will be searched: // e.g. GetEnumNameValue('sllWarning') will find sllWarning item // - if Value does not start with lowercases 'a'..'z', they will be ignored: // e.g. GetEnumNameValue('Warning') will find sllWarning item // - return -1 if not found (don't use directly this value to avoid any GPF) function GetEnumNameValue(Value: PUTF8Char): Integer; overload; {$ifdef HASINLINE}inline;{$endif} /// get the corresponding enumeration ordinal value, from its name // - if Value does start with lowercases 'a'..'z', they will be searched: // e.g. GetEnumNameValue('sllWarning') will find sllWarning item // - if AlsoTrimLowerCase is TRUE, and EnumName does not start with // lowercases 'a'..'z', they will be ignored: e.g. GetEnumNameValue('Warning') // will find sllWarning item // - return -1 if not found (don't use directly this value to avoid any GPF) function GetEnumNameValue(Value: PUTF8Char; ValueLen: integer; AlsoTrimLowerCase: boolean=true): Integer; overload; /// get the corresponding enumeration name, without the first lowercase chars // (otDone -> 'Done') // - Value will be converted to the matching ordinal value (byte or word) function GetEnumNameTrimed(const Value): RawUTF8; {$ifdef HASINLINE}inline;{$endif} /// get the enumeration names corresponding to a set value function GetSetNameCSV(Value: integer; SepChar: AnsiChar=','; FullSetsAsStar: boolean=false): RawUTF8; overload; /// get the enumeration names corresponding to a set value procedure GetSetNameCSV(W: TTextWriter; Value: integer; SepChar: AnsiChar=','; FullSetsAsStar: boolean=false); overload; /// get the enumeration names corresponding to a set value, as a JSON array function GetSetNameAsDocVariant(Value: integer; FullSetsAsStar: boolean=false): variant; /// get the corresponding caption name, without the first lowercase chars // (otDone -> 'Done') // - return "string" type, i.e. UnicodeString for Delphi 2009+ // - internally call UnCamelCase() then System.LoadResStringTranslate() if available // - Value will be converted to the matching ordinal value (byte or word) function GetCaption(const Value): string; /// get all caption names, ready to be display, as lines separated by #13#10 // - return "string" type, i.e. UnicodeString for Delphi 2009+ // - if UsedValuesBits is not nil, only the corresponding bits set are added function GetCaptionStrings(UsedValuesBits: Pointer=nil): string; /// add caption names, ready to be display, to a TStrings class // - add pointer(ord(element)) as Objects[] value // - if UsedValuesBits is not nil, only the corresponding bits set are added // - can be used e.g. to populate a combo box as such: // ! PTypeInfo(TypeInfo(TMyEnum))^.EnumBaseType^.AddCaptionStrings(ComboBox.Items); procedure AddCaptionStrings(Strings: TStrings; UsedValuesBits: Pointer=nil); /// get the corresponding enumeration ordinal value, from its name without // its first lowercase chars ('Done' will find otDone e.g.) // - return -1 if not found (don't use directly this value to avoid any GPF) function GetEnumNameTrimedValue(const EnumName: ShortString): Integer; overload; /// get the corresponding enumeration ordinal value, from its name without // its first lowercase chars ('Done' will find otDone e.g.) // - return -1 if not found (don't use directly this value to avoid any GPF) function GetEnumNameTrimedValue(Value: PUTF8Char; ValueLen: integer=0): Integer; overload; /// compute how many bytes this type will use to be stored as a enumerate function SizeInStorageAsEnum: Integer; /// compute how many bytes this type will use to be stored as a set function SizeInStorageAsSet: Integer; /// store an enumeration value from its ordinal representation procedure SetEnumFromOrdinal(out Value; Ordinal: Integer); end; TRecordField = record TypeInfo: PPTypeInfo; {$ifdef FPC} Offset: SizeInt; {$else} Offset: Cardinal; {$endif FPC} end; TRecordType = record {$ifdef FPC_NEWRTTI} RecInitInfo: Pointer; {$endif FPC_NEWRTTI} Size: cardinal; Count: integer; Fields: array[word] of TRecordField; end; PRecordField = ^TRecordField; PRecordType = ^TRecordType; TIntfFlag = (ifHasGuid,ifDispInterface,ifDispatch{$ifdef FPC},ifHasStrGUID{$endif}); TIntfFlags = set of TIntfFlag; /// a wrapper to interface type information, as defined by the Delphi RTTI TInterfaceTypeData = record IntfParent: PPTypeInfo; // ancestor IntfFlags: TIntfFlags; IntfGuid: TGUID; IntfUnit: ShortString; end; PInterfaceTypeData = ^TInterfaceTypeData; /// a wrapper containing type information definition // - user types defined as an alias don't have this type information: // & type NewType = OldType; // - user types defined as new types have this type information: // & type NewType = type OldType; {$ifdef FPC} {$push} {$PACKRECORDS 1} {$endif} {$ifdef USERECORDWITHMETHODS}TTypeInfo = record {$else}TTypeInfo = object{$endif} public /// the value type family Kind: TTypeKind; /// the declared name of the type ('String','Word','RawUnicode'...) Name: ShortString; /// get the class type information function ClassType: PClassType; {$ifdef HASINLINENOTX86}inline;{$endif} /// create an instance of the corresponding class // - will call TObject.Create, or TSQLRecord.Create virtual constructor // - will raise EParsingException if class cannot be constructed on the fly, // e.g. for a plain TCollectionItem class function ClassCreate: TObject; /// get the SQL type of this Delphi class type // - returns either sftObject, sftID, sftMany or sftUnknown function ClassSQLFieldType: TSQLFieldType; /// get the number of published properties in this class // - you can count the plain fields without any getter function, if you // do need only the published properties corresponding to some value // actually stored, and ignore e.g. any textual conversion function ClassFieldCount(onlyWithoutGetter: boolean): integer; /// for ordinal types, get the storage size and sign function OrdType: TOrdType; {$ifdef HASINLINE}inline;{$endif} /// return TRUE if the property is an unsigned 64-bit field function IsQWord: boolean; {$ifdef HASINLINE}inline;{$endif} /// for set types, get the type information of the corresponding enumeration function SetEnumType: PEnumType; /// for gloating point types, get the storage size and procision function FloatType: TFloatType; {$ifdef HASINLINE}inline;{$endif} /// get the SQL type of this Delphi type, as managed with the database driver function GetSQLFieldType: TSQLFieldType; /// fast and easy find if a class type inherits from a specific class type function InheritsFrom(AClass: TClass): boolean; /// get the enumeration type information function EnumBaseType: PEnumType; {$ifdef HASINLINENOTX86}inline;{$endif} /// get the record type information function RecordType: PRecordType; {$ifdef HASINLINENOTX86}inline;{$endif} /// get the interface type information function InterfaceType: PInterfaceTypeData; {$ifdef HASINLINENOTX86}inline;{$endif} /// get the dynamic array type information of the stored item function DynArrayItemType(aDataSize: PInteger=nil): PTypeInfo; {$ifdef HASINLINE}inline;{$endif} /// get the dynamic array size (in bytes) of the stored item function DynArrayItemSize: integer; {$ifdef HASINLINENOTX86}inline;{$endif} /// get the SQL type of the items of a dynamic array function DynArraySQLFieldType: TSQLFieldType; /// recognize most used string types, returning their code page // - will recognize TSQLRawBlob as the fake CP_SQLRAWBLOB code page // - will return the exact code page since Delphi 2009, from RTTI // - for non Unicode versions of Delphi, will recognize WinAnsiString as // CODEPAGE_US, RawUnicode as CP_UTF16, RawByteString as CP_RAWBYTESTRING, // AnsiString as 0, and any other type as RawUTF8 function AnsiStringCodePage: integer; {$ifdef HASCODEPAGE}inline;{$endif} /// get the TGUID of a given interface type information // - returns nil if this type is not an interface function InterfaceGUID: PGUID; /// get the unit name of a given interface type information // - returns '' if this type is not an interface function InterfaceUnitName: PShortString; /// get the ancestor/parent of a given interface type information // - returns nil if this type has no parent function InterfaceAncestor: PTypeInfo; /// get all ancestors/parents of a given interface type information // - only ancestors with an associated TGUID will be added // - if OnlyImplementedBy is not nil, only the interface explicitly // implemented by this class will be added, and AncestorsImplementedEntry[] // will contain the corresponding PInterfaceEntry values procedure InterfaceAncestors(out Ancestors: PTypeInfoDynArray; OnlyImplementedBy: TInterfacedObjectClass; out AncestorsImplementedEntry: TPointerDynArray); {$ifdef FPC_PROVIDE_ATTR_TABLE} /// type attributes, introduced since SVN 42356-42411 (2019/07) function AttributeTable: PFPCAttributeTable; inline; {$endif FPC_PROVIDE_ATTR_TABLE} end; /// how a RTTI property definition access its value // - as returned by TPropInfo.Getter/Setter methods TPropInfoCall = ( picNone, picField, picMethod, picIndexed); /// a wrapper containing a RTTI property definition // - used for direct Delphi / UTF-8 SQL type mapping/conversion // - doesn't depend on RTL's TypInfo unit, to enhance cross-compiler support {$ifdef USERECORDWITHMETHODS}TPropInfo = packed record {$else}TPropInfo = object{$endif} { "packed" above is needed on ARM (alf) } public /// raw retrieval of the property read access definition // - note: 'var Call' generated incorrect code on Delphi XE4 -> use PMethod function Getter(Instance: TObject; Call: PMethod): TPropInfoCall; {$ifdef HASINLINE}inline;{$endif} /// raw retrieval of the property access definition function Setter(Instance: TObject; Call: PMethod): TPropInfoCall; {$ifdef HASINLINE}inline;{$endif} /// raw retrieval of tkInteger,tkEnumeration,tkSet,tkChar,tkWChar,tkBool // - rather call GetOrdValue/GetInt64Value function GetOrdProp(Instance: TObject): PtrInt; /// raw assignment of tkInteger,tkEnumeration,tkSet,tkChar,tkWChar,tkBool // - rather call SetOrdValue/SetInt64Value procedure SetOrdProp(Instance: TObject; Value: PtrInt); /// raw retrieval of tkClass function GetObjProp(Instance: TObject): TObject; /// raw retrieval of tkInt64,tkQWord // - rather call GetInt64Value function GetInt64Prop(Instance: TObject): Int64; /// raw assignment of tkInt64,tkQWord // - rather call SetInt64Value procedure SetInt64Prop(Instance: TObject; const Value: Int64); /// raw retrieval of tkLString procedure GetLongStrProp(Instance: TObject; var Value: RawByteString); /// raw assignment of tkLString procedure SetLongStrProp(Instance: TObject; const Value: RawByteString); /// raw copy of tkLString procedure CopyLongStrProp(Source,Dest: TObject); /// raw retrieval of tkString into an Ansi7String procedure GetShortStrProp(Instance: TObject; var Value: RawByteString); /// raw retrieval of tkWString procedure GetWideStrProp(Instance: TObject; var Value: WideString); /// raw assignment of tkWString procedure SetWideStrProp(Instance: TObject; const Value: WideString); {$ifdef HASVARUSTRING} /// raw retrieval of tkUString procedure GetUnicodeStrProp(Instance: TObject; var Value: UnicodeString); /// raw assignment of tkUString procedure SetUnicodeStrProp(Instance: TObject; const Value: UnicodeString); {$endif HASVARUSTRING} /// raw retrieval of tkFloat/currency // - use instead GetCurrencyValue function GetCurrencyProp(Instance: TObject): currency; /// raw assignment of tkFloat/currency procedure SetCurrencyProp(Instance: TObject; const Value: Currency); /// raw retrieval of tkFloat/double function GetDoubleProp(Instance: TObject): double; /// raw assignment of tkFloat/double procedure SetDoubleProp(Instance: TObject; Value: Double); /// raw retrieval of tkFloat - with conversion to 64-bit double // - use instead GetDoubleValue function GetFloatProp(Instance: TObject): double; /// raw assignment of tkFloat // - use instead SetDoubleValue procedure SetFloatProp(Instance: TObject; Value: TSynExtended); {$ifndef NOVARIANTS} /// raw retrieval of tkVariant procedure GetVariantProp(Instance: TObject; var result: Variant); /// raw assignment of tkVariant procedure SetVariantProp(Instance: TObject; const Value: Variant); {$endif NOVARIANTS} public /// the type definition of this property // - call TPropInfo.TypeInfo for cross-compiler access to this information PropType: PPTypeInfo; /// contains the offset of a field, or the getter method set by 'read' declaration // - if this field is 0 (no 'read' was specified), raw access methods will // use SetProc to get the field memory address to read from // - call TPropInfo.Getter for cross-compiler access to this information GetProc: PtrUInt; /// contains the offset of a field, or the setter method set by 'write' declaration // - if this field is 0 (no 'write' was specified), raw access methods will // use GetProc to get the field memory address to save into // - call TPropInfo.Setter for cross-compiler access to this information SetProc: PtrUInt; /// contains the 'stored' boolean value/method (used in TPersistent saving) // - either integer(True) - the default, integer(False), reference to a Boolean // field, or reference to a parameterless method that returns a Boolean value // - if a property is marked as "stored AS_UNIQUE" (i.e. "stored false"), // it is created as UNIQUE in the SQL database and its bit is set in // Model.fIsUnique[] // - call TPropInfo.IsStored for cross-compiler access to this information StoredProc: PtrUInt; /// contains the index value of an indexed class data property // - outside SQLite3, this can be used to define a VARCHAR() length value // for the textual field definition (sftUTF8Text/sftAnsiText); e.g. // the following will create a NAME VARCHAR(40) field: // ! Name: RawUTF8 index 40 read fName write fName; // - is used by a dynamic array property for fast usage of the // TSQLRecord.DynArray(DynArrayFieldIndex) method Index: Integer; /// contains the default value (NO_DEFAULT=$80000000 indicates none set) // when an ordinal or set property is saved as TPersistent // - see TPropInfo.DefaultOr0/DefaultOrVoid for easy use Default: Longint; /// index of the property in the current inherited class definition // - first name index at a given class level is 0 // - index is reset to 0 at every inherited class level NameIndex: SmallInt; {$ifdef FPC} /// contains the type of the GetProc/SetProc/StoredProc, see also ptxxx // - bit 0..1 GetProc e.g. PropProcs and 3=ptField // 2..3 SetProc e.g. (PropProcs shr 2) and 3=ptField // 4..5 StoredProc // 6 : true, constant index property // - rather call TPropInfo.Getter/Setter for cross-compiler access PropProcs: Byte; {$ifdef FPC_PROVIDE_ATTR_TABLE} /// property attributes, introduced since FPC SVN 42356-42411 (2019/07) AttributeTable: PFPCAttributeTable; {$endif FPC_PROVIDE_ATTR_TABLE} {$endif FPC} /// the property definition Name Name: ShortString; /// the type information of this property // - will de-reference the PropType pointer on Delphi and newer FPC compilers function TypeInfo: PTypeInfo; {$ifdef HASINLINENOTX86}inline;{$endif} /// get the next property information // - no range check: use ClassProp()^.PropCount to determine the properties count // - get the first PPropInfo with ClassProp()^.PropList function Next: PPropInfo; {$ifdef HASINLINENOTX86}inline;{$endif} /// return FALSE (AS_UNIQUE) if was marked as "stored AS_UNIQUE" // (i.e. "stored false"), or TRUE by default // - if Instance=nil, will work only at RTTI level, not with field or method // (and will return TRUE if nothing is defined in the RTTI) function IsStored(Instance: TObject): boolean; /// copy a published property value from one instance to another // - this method use direct copy of the low-level binary content, and is // therefore faster than a SetValue(Dest,GetValue(Source)) call // - if DestInfo is nil, it will assume DestInfo=@self procedure CopyValue(Source, Dest: TObject; DestInfo: PPropInfo=nil); /// create a new instance of a published property // - copying its properties values from a given instance of another class // - if the destination property is not of the aFrom class, it will first // search for any extact mach in the destination nested properties function CopyToNewObject(aFrom: TObject): TObject; /// compare two published properties function SameValue(Source: TObject; DestInfo: PPropInfo; Dest: TObject): boolean; /// return true if this property is a BLOB (TSQLRawBlob) function IsBlob: boolean; {$ifdef HASINLINE}inline;{$endif} /// return the Default RTTI value defined for this property, or 0 if not set function DefaultOr0: integer; {$ifdef HASINLINE}inline;{$endif} /// return TRUE if the property has its Default RTTI value, or is 0/""/nil // - will call function IsObjectDefaultOrVoid() for class properties function IsDefaultOrVoid(Instance: TObject): boolean; /// compute in how many bytes this property is stored function RetrieveFieldSize: integer; /// low-level getter of the ordinal property value of a given instance // - this method will check if the corresponding property is ordinal // - return -1 on any error function GetOrdValue(Instance: TObject): PtrInt; {$ifdef HASINLINE}inline;{$endif} /// low-level getter of the ordinal property value of a given instance // - this method will check if the corresponding property is ordinal // - ordinal properties smaller than tkInt64 will return an Int64-converted // value (e.g. tkInteger) // - return 0 on any error function GetInt64Value(Instance: TObject): Int64; /// low-level getter of the currency property value of a given instance // - this method will check if the corresponding property is exactly currency // - return 0 on any error function GetCurrencyValue(Instance: TObject): Currency; /// low-level getter of the floating-point property value of a given instance // - this method will check if the corresponding property is floating-point // - return 0 on any error function GetDoubleValue(Instance: TObject): double; /// low-level setter of the floating-point property value of a given instance // - this method will check if the corresponding property is floating-point procedure SetDoubleValue(Instance: TObject; const Value: double); /// low-level getter of the long string property value of a given instance // - this method will check if the corresponding property is a Long String, // and will return '' if it's not the case // - it will convert the property content into RawUTF8, for RawUnicode, // WinAnsiString, TSQLRawBlob and generic Delphi 6-2007 string property // - WideString and UnicodeString properties will also be UTF-8 converted procedure GetLongStrValue(Instance: TObject; var result: RawUTF8); /// low-level getter of the long string property content of a given instance // - just a wrapper around low-level GetLongStrProp() function // - call GetLongStrValue() method if you want a conversion into RawUTF8 // - will work only for Kind=tkLString procedure GetRawByteStringValue(Instance: TObject; var Value: RawByteString); /// low-level setter of the ordinal property value of a given instance // - this method will check if the corresponding property is ordinal procedure SetOrdValue(Instance: TObject; Value: PtrInt); /// low-level setter of the ordinal property value of a given instance // - this method will check if the corresponding property is ordinal procedure SetInt64Value(Instance: TObject; Value: Int64); /// low-level setter of the long string property value of a given instance // - this method will check if the corresponding property is a Long String // - it will convert the property content into RawUTF8, for RawUnicode, // WinAnsiString, TSQLRawBlob and generic Delphi 6-2007 string property // - will set WideString and UnicodeString properties from UTF-8 content procedure SetLongStrValue(Instance: TObject; const Value: RawUTF8); /// low-level setter of the string property value of a given instance // - uses the generic string type: to be used within the VCL // - this method will check if the corresponding property is a Long String // or an UnicodeString (for Delphi 2009+), and will call the corresponding // SetLongStrValue() or SetUnicodeStrValue() method procedure SetGenericStringValue(Instance: TObject; const Value: string); /// low-level getter of the long string property value of a given instance // - uses the generic string type: to be used within the VCL // - this method will check if the corresponding property is a Long String, // or an UnicodeString (for Delphi 2009+),and will return '' if it's // not the case function GetGenericStringValue(Instance: TObject): string; {$ifdef HASVARUSTRING} /// low-level setter of the Unicode string property value of a given instance // - this method will check if the corresponding property is a Unicode String procedure SetUnicodeStrValue(Instance: TObject; const Value: UnicodeString); /// low-level getter of the Unicode string property value of a given instance // - this method will check if the corresponding property is a Unicode String function GetUnicodeStrValue(Instance: TObject): UnicodeString; {$endif HASVARUSTRING} /// low-level getter of a dynamic array wrapper // - this method will NOT check if the property is a dynamic array: caller // must have already checked that PropType^^.Kind=tkDynArray function GetDynArray(Instance: TObject): TDynArray; overload; {$ifdef HASINLINE}inline;{$endif} /// low-level getter of a dynamic array wrapper // - this method will NOT check if the property is a dynamic array: caller // must have already checked that PropType^^.Kind=tkDynArray procedure GetDynArray(Instance: TObject; var result: TDynArray); overload; {$ifdef HASINLINE}inline;{$endif} /// return TRUE if this dynamic array has been registered as a T*ObjArray // - the T*ObjArray dynamic array should have been previously registered // via TJSONSerializer.RegisterObjArrayForJSON() overloaded methods function DynArrayIsObjArray: boolean; {$ifdef HASINLINE}inline;{$endif} /// return class instance creation information about a T*ObjArray // - the T*ObjArray dynamic array should have been previously registered // via TJSONSerializer.RegisterObjArrayForJSON() overloaded methods // - returns nil if the supplied type is not a registered T*ObjArray // - you can create a new item instance just by calling result^.CreateNew function DynArrayIsObjArrayInstance: PClassInstance; {$ifdef HASINLINE}inline;{$endif} /// return TRUE if the property has no getter but direct field read function GetterIsField: boolean; {$ifdef HASINLINE}inline;{$endif} /// return TRUE if the property has no setter but direct field write function SetterIsField: boolean; {$ifdef HASINLINE}inline;{$endif} /// return TRUE if the property has a write setter or direct field function WriteIsDefined: boolean; {$ifdef HASINLINE}inline;{$endif} /// returns the low-level field read address, if GetterIsField is TRUE function GetterAddr(Instance: pointer): pointer; {$ifdef HASINLINENOTX86}inline;{$endif} /// returns the low-level field write address, if SetterIsField is TRUE function SetterAddr(Instance: pointer): pointer; {$ifdef HASINLINE}inline;{$endif} /// low-level getter of the field value memory pointer // - return NIL if both getter and setter are methods function GetFieldAddr(Instance: TObject): pointer; {$ifdef HASINLINE}inline;{$endif} /// low-level setter of the property value as its default // - this method will check the property type, e.g. setting '' for strings, // and 0 for numbers, or running FreeAndNil() on any nested object (unless // FreeAndNilNestedObjects is false so that ClearObject() is used procedure SetDefaultValue(Instance: TObject; FreeAndNilNestedObjects: boolean=true); {$ifndef NOVARIANTS} /// low-level setter of the property value from a supplied variant // - will optionally make some conversion if the property type doesn't // match the variant type, e.g. a text variant could be converted to integer // when setting a tkInteger kind of property // - a tkDynArray property is expected to be a T*ObjArray and will be // converted from a TDocVariant using a newly allocated T*ObjArray procedure SetFromVariant(Instance: TObject; const Value: variant); /// low-level getter of the property value into a variant value // - a tkDynArray property is expected to be a T*ObjArray and will be // converted into a TDocVariant using a temporary JSON serialization procedure GetVariant(Instance: TObject; var Dest: variant); {$endif NOVARIANTS} /// low-level setter of the property value from its text representation /// - handle published integer, Int64, floating point values, (wide)string, // enumerates (e.g. boolean), variant properties of the object // - for variant properties, could unserialize the Text as JSON into a // TDocVariantData if TryCustomVariants (and AllowDouble) are set // - dynamic arrays are unserialized from JSON [...], unless a // TRawUTF8DynArray property has been stored as CSV procedure SetFromText(Instance: TObject; const Text: RawUTF8; TryCustomVariants: PDocVariantOptions=nil; AllowDouble: boolean=false); /// low-level appender of the property value to a text buffer // - write the published integer, Int64, floating point values, (wide)string, // enumerates (e.g. boolean), variant properties of the object // - dynamic arrays will be serialized as JSON, unless RawUTF8DynArrayAsCSV // is set, and a TRawUTF8DynArray property will be stored as CSV procedure GetToText(Instance: TObject; WR: TTextWriter; RawUTF8DynArrayAsCSV: boolean=false; Escape: TTextWriterKind=twNone); /// read an TObject published property, as saved by ObjectToJSON() function // - will use direct in-memory reference to the object, or call the corresponding // setter method (if any), creating a temporary instance via TTypeInfo.ClassCreate // - unserialize the JSON input buffer via a call to JSONToObject() // - by default, a temporary instance will be created if a published field // has a setter, and the instance is expected to be released later by the // owner class: you can set the j2oSetterExpectsToFreeTempInstance option // to let this method release it when the setter returns function ClassFromJSON(Instance: TObject; From: PUTF8Char; var Valid: boolean; Options: TJSONToObjectOptions=[]): PUTF8Char; end; {$ifdef FPC} {$pop} {$endif} /// the available methods calling conventions // - this is by design only relevant to the x86 model // - Win64 has one unique calling convention TCallingConvention = ( ccRegister, ccCdecl, ccPascal, ccStdCall, ccSafeCall {$ifdef FPC}, ccCppdecl, ccFar16, ccOldFPCCall, ccInternProc, ccSysCall, ccSoftFloat, ccMWPascal{$endif FPC}); /// the available kind of method parameters TParamFlag = (pfVar, pfConst, pfArray, pfAddress, pfReference, pfOut {$ifdef FPC} ,pfConstRef {$ifdef FPC_NEWRTTI} ,pfHidden,pfHigh,pfSelf,pfVmt,pfResult {$endif FPC_NEWRTTI} {$else} ,pfResult {$endif}); /// a set of kind of method parameters TParamFlags = set of TParamFlag; PReturnInfo = ^TReturnInfo; PCallingConvention = ^TCallingConvention; PParamInfo = ^TParamInfo; /// a wrapper around method returned result definition {$ifdef USERECORDWITHMETHODS}TReturnInfo = record {$else}TReturnInfo = object{$endif} public /// RTTI version // - 2 up to Delphi 2010, 3 for Delphi XE and up Version: byte; /// expected calling convention (only relevant for x86 mode) CallingConvention: TCallingConvention; /// the expected type of the returned function result // - is nil for procedure ReturnType: ^PTypeInfo; /// total size of data needed for stack parameters + 8 (ret-addr + pushed EBP) ParamSize: Word; /// number of expected parameters ParamCount: Byte; /// access to the first method parameter definition function Param: PParamInfo; {$ifdef HASINLINE}inline;{$endif} end; /// a wrapper around an individual method parameter definition {$ifdef USERECORDWITHMETHODS}TParamInfo = record {$else}TParamInfo = object{$endif} public /// the kind of parameter Flags: TParamFlags; /// the parameter type information ParamType: PPTypeInfo; {$ifdef FPC} ParReg: byte; Offset: longint; {$else} /// parameter offset // - 0 for EAX, 1 for EDX, 2 for ECX // - any value >= 8 for stack-based parameter Offset: Word; {$endif} /// parameter name Name: ShortString; /// get the next parameter information // - no range check: use TReturnInfo.ParamCount to determine the appropriate count function Next: PParamInfo; {$ifdef HASINLINE}inline;{$endif} end; /// a wrapper around a method definition {$ifdef USERECORDWITHMETHODS}TMethodInfo = packed record {$else}TMethodInfo = object{$endif} public {$ifdef FPC} /// method name Name: PShortString; /// the associated method code address Addr: Pointer; {$else} /// size (in bytes) of this TMethodInfo block Len: Word; /// the associated method code address Addr: Pointer; /// method name Name: ShortString; {$endif} /// retrieve the associated parameters information function ReturnInfo: PReturnInfo; {$ifdef HASINLINE}inline;{$endif} /// wrapper returning nil and avoiding a GPF if @self=nil function MethodAddr: Pointer; {$ifdef HASINLINE}inline;{$endif} end; {$ifdef FPC} const ptField = 0; ptStatic = 1; ptVirtual = 2; ptConst = 3; NO_INDEX = 0; {$PACKRECORDS DEFAULT} // back to normal alignment {$else} const NO_INDEX = longint($80000000); ptField = $ff; ptVirtual = $fe; type /// used to map a TPropInfo.GetProc/SetProc and retrieve its kind // - defined here for proper Delphi inlining PropWrap = packed record FillBytes: array [0..SizeOf(Pointer)-2] of byte; /// =$ff for a ptField address, or =$fe for a ptVirtual method Kind: byte; end; {$A+} // back to normal alignment {$endif FPC} const NO_DEFAULT = longint($80000000); type TJSONSerializer = class; /// ORM attributes for a TSQLPropInfo definition TSQLPropInfoAttribute = ( aIsUnique, aAuxiliaryRTreeField, aBinaryCollation); /// set of ORM attributes for a TSQLPropInfo definition TSQLPropInfoAttributes = set of TSQLPropInfoAttribute; /// abstract parent class to store information about a published property // - property information could be retrieved from RTTI (TSQLPropInfoRTTI*), // or be defined by code (TSQLPropInfoCustom derivated classes) when RTTI // is not available TSQLPropInfo = class protected fName: RawUTF8; fNameUnflattened: RawUTF8; fSQLFieldType: TSQLFieldType; fSQLFieldTypeStored: TSQLFieldType; fSQLDBFieldType: TSQLDBFieldType; fAttributes: TSQLPropInfoAttributes; fFieldWidth: integer; fPropertyIndex: integer; fFromRTTI: boolean; function GetNameDisplay: string; virtual; /// those two protected methods allow custom storage of binary content as text // - default implementation is to use hexa (ToSQL=true) or Base64 encodings procedure BinaryToText(var Value: RawUTF8; ToSQL: boolean; wasSQLString: PBoolean); virtual; procedure TextToBinary(Value: PUTF8Char; var result: RawByteString); virtual; function GetSQLFieldTypeName: PShortString; function GetSQLFieldRTTITypeName: RawUTF8; virtual; // overriden method shall use direct copy of the low-level binary content, // to be faster than a DestInfo.SetValue(Dest,GetValue(Source)) call procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); virtual; public /// initialize the internal fields // - should not be called directly, but with dedicated class methods like // class function TSQLPropInfoRTTI.CreateFrom() or overridden constructors constructor Create(const aName: RawUTF8; aSQLFieldType: TSQLFieldType; aAttributes: TSQLPropInfoAttributes; aFieldWidth, aPropertyIndex: integer); reintroduce; virtual; /// the property definition Name property Name: RawUTF8 read fName; /// the property definition Name, afer un-camelcase and translation property NameDisplay: string read GetNameDisplay; /// the property definition Name, with full path name if has been flattened // - if the property has been flattened (for a TSQLPropInfoRTTI), the real // full nested class will be returned, e.g. 'Address.Country.Iso' for // the 'Address_Country' flattened property name property NameUnflattened: RawUTF8 read fNameUnflattened; /// the property index in the RTTI property PropertyIndex: integer read fPropertyIndex; /// the corresponding column type, as managed by the ORM layer property SQLFieldType: TSQLFieldType read fSQLFieldType; /// the corresponding column type, as stored by the ORM layer // - match SQLFieldType, unless for SQLFieldType=sftNullable, in which this // field will contain the simple type eventually stored in the database property SQLFieldTypeStored: TSQLFieldType read fSQLFieldTypeStored; /// the corresponding column type name, as managed by the ORM layer and // retrieved by the RTTI // - returns e.g. 'sftTimeLog' property SQLFieldTypeName: PShortString read GetSQLFieldTypeName; /// the type name, as defined in the RTTI // - returns e.g. 'RawUTF8' // - will return the TSQLPropInfo class name if it is not a TSQLPropInfoRTTI property SQLFieldRTTITypeName: RawUTF8 read GetSQLFieldRTTITypeName; /// the corresponding column type, as managed for abstract database access // - TNullable* fields will report here the corresponding simple DB type, // e.g. ftInt64 for TNullableInteger (following SQLFieldTypeStored value) property SQLDBFieldType: TSQLDBFieldType read fSQLDBFieldType; /// the corresponding column type name, as managed for abstract database access function SQLDBFieldTypeName: PShortString; /// the ORM attributes of this property // - contains aIsUnique e.g for TSQLRecord published properties marked as // ! property MyProperty: RawUTF8 stored AS_UNIQUE; // (i.e. "stored false") property Attributes: TSQLPropInfoAttributes read fAttributes write fAttributes; /// the optional width of this field, in external databases // - is set e.g. by index attribute of TSQLRecord published properties as // ! property MyProperty: RawUTF8 index 10; property FieldWidth: integer read fFieldWidth; public /// convert UTF-8 encoded text into the property value // - setter method (write Set*) is called if available // - if no setter exists (no write declaration), the getted field address is used // - handle UTF-8 SQL to Delphi values conversion // - expect BLOB fields encoded as SQlite3 BLOB literals ("x'01234'" e.g.) // or base-64 encoded stream for JSON ("\uFFF0base64encodedbinary") - i.e. // both format supported by BlobToTSQLRawBlob() function // - handle TPersistent, TCollection, TRawUTF8List or TStrings with JSONToObject // - note that the supplied Value buffer won't be modified by this method: // overriden implementation should create their own temporary copy procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); virtual; abstract; /// convert UTF-8 encoded text into the property value // - just a wrapper around SetValue(...,pointer(Value),...) which may be // optimized for overriden methods procedure SetValueVar(Instance: TObject; const Value: RawUTF8; wasString: boolean); virtual; /// convert the property value into an UTF-8 encoded text // - if ToSQL is true, result is on SQL form (false->'0' e.g.) // - if ToSQL is false, result is on JSON form (false->'false' e.g.) // - BLOB field returns SQlite3 BLOB literals ("x'01234'" e.g.) if ToSQL is // true, or base-64 encoded stream for JSON ("\uFFF0base64encodedbinary") // - getter method (read Get*) is called if available // - handle Delphi values into UTF-8 SQL conversion // - sftBlobDynArray, sftBlobCustom or sftBlobRecord are returned as BLOB // litterals ("X'53514C697465'") if ToSQL is true, or base-64 encoded stream // for JSON ("\uFFF0base64encodedbinary") // - handle TPersistent, TCollection, TRawUTF8List or TStrings with ObjectToJSON function GetValue(Instance: TObject; ToSQL: boolean; wasSQLString: PBoolean=nil): RawUTF8; {$ifdef HASINLINE}inline;{$endif} /// convert the property value into an UTF-8 encoded text // - this method is the same as GetValue(), but avoid assigning the result // string variable (some speed up on multi-core CPUs, since avoid a CPU LOCK) // - this virtual method is the one to be overridden by the implementing classes procedure GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); virtual; abstract; /// normalize the content of Value, so that GetValue(Object,true) should return the // same content (true for ToSQL format) procedure NormalizeValue(var Value: RawUTF8); virtual; abstract; /// retrieve a field value into a TSQLVar value // - the temp RawByteString is used as a temporary storage for TEXT or BLOB // and should be available during all access to the TSQLVar fields procedure GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar; var temp: RawByteString); virtual; /// set a field value from a TSQLVar value function SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; virtual; /// returns TRUE if value is 0 or '' function IsValueVoid(Instance: TObject): boolean; /// append the property value into a binary buffer procedure GetBinary(Instance: TObject; W: TFileBufferWriter); virtual; abstract; /// read the property value from a binary buffer // - PEnd should point to the end of the P input buffer, to avoid any overflow // - returns next char in input buffer on success, or nil in case of invalid // content supplied e.g. function SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; virtual; abstract; /// copy a property value from one instance to another // - both objects should have the same exact property procedure CopyValue(Source, Dest: TObject); virtual; /// copy a value from one instance to another property instance // - if the property has been flattened (for a TSQLPropInfoRTTI), the real // Source/Dest instance will be used for the copy procedure CopyProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); {$ifndef NOVARIANTS} /// retrieve the property value into a Variant // - will set the Variant type to the best matching kind according to the // SQLFieldType type // - BLOB field returns SQlite3 BLOB textual literals ("x'01234'" e.g.) // - dynamic array field is returned as a variant array procedure GetVariant(Instance: TObject; var Dest: Variant); virtual; /// set the property value from a Variant value // - dynamic array field must be set from a variant array // - will set the Variant type to the best matching kind according to the // SQLFieldType type // - expect BLOB fields encoded as SQlite3 BLOB literals ("x'01234'" e.g.) procedure SetVariant(Instance: TObject; const Source: Variant); virtual; {$endif} /// compare the content of the property of two objects // - not all kind of properties are handled: only main types (like GetHash) // - if CaseInsensitive is TRUE, will apply NormToUpper[] 8 bits uppercase, // handling RawUTF8 properties just like the SYSTEMNOCASE collation // - this method should match the case-sensitivity of GetHash() // - this default implementation will call GetValueVar() for slow comparison function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; virtual; /// retrieve an unsigned 32-bit hash of the corresponding property // - not all kind of properties are handled: only main types // - if CaseInsensitive is TRUE, will apply NormToUpper[] 8 bits uppercase, // handling RawUTF8 properties just like the SYSTEMNOCASE collation // - note that this method can return a hash value of 0 // - this method should match the case-sensitivity of CompareValue() // - this default implementation will call GetValueVar() for slow computation function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; virtual; /// add the JSON content corresponding to the given property // - this default implementation will call safe but slow GetValueVar() method procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); virtual; /// returns an untyped pointer to the field property memory in a given instance function GetFieldAddr(Instance: TObject): pointer; virtual; abstract; end; /// class-reference type (metaclass) of a TSQLPropInfo information TSQLPropInfoClass = class of TSQLPropInfo; /// define how the published properties RTTI is to be interpreted // - i.e. how TSQLPropInfoList.Create() and TSQLPropInfoRTTI.CreateFrom() // will handle the incoming RTTI TSQLPropInfoListOptions = set of ( pilRaiseEORMExceptionIfNotHandled, pilAllowIDFields, pilSubClassesFlattening, pilIgnoreIfGetter, pilSingleHierarchyLevel, pilAuxiliaryFields); /// parent information about a published property retrieved from RTTI TSQLPropInfoRTTI = class(TSQLPropInfo) protected fPropInfo: PPropInfo; fPropType: PTypeInfo; fFlattenedProps: PPropInfoDynArray; fGetterIsFieldPropOffset: PtrUInt; fInPlaceCopySameClassPropOffset: PtrUInt; function GetSQLFieldRTTITypeName: RawUTF8; override; public /// this meta-constructor will create an instance of the exact descendant // of the specified property RTTI // - it will raise an EORMException in case of an unhandled type class function CreateFrom(aPropInfo: PPropInfo; aPropIndex: integer; aOptions: TSQLPropInfoListOptions; const aFlattenedProps: PPropInfoDynArray): TSQLPropInfo; /// register this class corresponding to the RTTI TypeInfo() pointer // - could be used e.g. to define custom serialization and process of // any custom type class procedure RegisterTypeInfo(aTypeInfo: Pointer); /// initialize the internal fields // - should not be called directly, but with dedicated class methods like // class function CreateFrom() constructor Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType; aOptions: TSQLPropInfoListOptions); reintroduce; virtual; {$ifndef NOVARIANTS} /// retrieve the property value into a Variant // - will set the Variant type to the best matching kind according to the // SQLFieldType type // - BLOB field returns SQlite3 BLOB textual literals ("x'01234'" e.g.) // - dynamic array field is returned as a variant array procedure GetVariant(Instance: TObject; var Dest: Variant); override; {$endif} /// generic way of implementing it function GetFieldAddr(Instance: TObject): pointer; override; /// for pilSubClassesFlattening properties, compute the actual instance // containing the property value // - if the property was not flattened, return the instance function Flattened(Instance: TObject): TObject; /// corresponding RTTI information property PropInfo: PPropInfo read fPropInfo; /// for pilSubClassesFlattening properties, the parents RTTI property FlattenedPropInfo: PPropInfoDynArray read fFlattenedProps; /// corresponding type information, as retrieved from PropInfo RTTI property PropType: PTypeInfo read fPropType; end; /// class-reference type (metaclass) of a TSQLPropInfoRTTI information TSQLPropInfoRTTIClass = class of TSQLPropInfoRTTI; TSQLPropInfoRTTIObjArray = array of TSQLPropInfoRTTI; /// information about an ordinal Int32 published property TSQLPropInfoRTTIInt32 = class(TSQLPropInfoRTTI) protected fUnsigned: boolean; procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override; public constructor Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType; aOptions: TSQLPropInfoListOptions); override; procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override; procedure GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); override; procedure GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar; var temp: RawByteString); override; function SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; override; procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override; function SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; override; function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override; function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override; procedure NormalizeValue(var Value: RawUTF8); override; procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override; end; /// information about a set published property TSQLPropInfoRTTISet = class(TSQLPropInfoRTTIInt32) protected fSetEnumType: PEnumType; public constructor Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType; aOptions: TSQLPropInfoListOptions); override; property SetEnumType: PEnumType read fSetEnumType; end; /// information about a enumeration published property // - can be either sftBoolean or sftEnumerate kind of property TSQLPropInfoRTTIEnum = class(TSQLPropInfoRTTIInt32) protected fEnumType: PEnumType; public constructor Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType; aOptions: TSQLPropInfoListOptions); override; procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override; procedure GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); override; procedure NormalizeValue(var Value: RawUTF8); override; procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override; function GetCaption(Value: RawUTF8; out IntValue: integer): string; property EnumType: PEnumType read fEnumType; end; /// information about a character published property TSQLPropInfoRTTIChar = class(TSQLPropInfoRTTIInt32) public procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override; procedure GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); override; procedure NormalizeValue(var Value: RawUTF8); override; end; /// information about an ordinal Int64 published property TSQLPropInfoRTTIInt64 = class(TSQLPropInfoRTTI) protected fIsQWord: boolean; procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override; public constructor Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType; aOptions: TSQLPropInfoListOptions); override; procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override; procedure GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); override; procedure GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar; var temp: RawByteString); override; function SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; override; procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override; function SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; override; function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override; function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override; procedure NormalizeValue(var Value: RawUTF8); override; procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override; end; /// information about a PtrInt published property, according to the native CPU // - not a real stand-alone class, but a convenient wrapper type TSQLPropInfoRTTIPtrInt = {$ifdef CPU64}TSQLPropInfoRTTIInt64{$else}TSQLPropInfoRTTIInt32{$endif}; /// information about a TTimeLog published property // - stored as an Int64, but with a specific class TSQLPropInfoRTTITimeLog = class(TSQLPropInfoRTTIInt64); /// information about a TUnixTime published property // - stored as an Int64, but with a specific class TSQLPropInfoRTTIUnixTime = class(TSQLPropInfoRTTIInt64); /// information about a TUnixMSTime published property // - stored as an Int64, but with a specific class TSQLPropInfoRTTIUnixMSTime = class(TSQLPropInfoRTTIInt64); /// information about a floating-point Double published property TSQLPropInfoRTTIDouble = class(TSQLPropInfoRTTI) protected procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override; public procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override; procedure GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); override; procedure GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar; var temp: RawByteString); override; function SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; override; procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override; function SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; override; procedure NormalizeValue(var Value: RawUTF8); override; procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override; function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override; function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override; end; /// information about a fixed-decimal Currency published property TSQLPropInfoRTTICurrency = class(TSQLPropInfoRTTIDouble) protected procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override; public procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override; procedure GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); override; procedure GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar; var temp: RawByteString); override; function SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; override; procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override; function SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; override; procedure NormalizeValue(var Value: RawUTF8); override; procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override; function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override; function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override; end; /// information about a TDateTime published property TSQLPropInfoRTTIDateTime = class(TSQLPropInfoRTTIDouble) public procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override; procedure GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); override; procedure GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar; var temp: RawByteString); override; procedure NormalizeValue(var Value: RawUTF8); override; procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override; function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override; end; /// information about a AnsiString published property TSQLPropInfoRTTIAnsi = class(TSQLPropInfoRTTI) protected fEngine: TSynAnsiConvert; procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override; public constructor Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType; aOptions: TSQLPropInfoListOptions); override; procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override; procedure SetValueVar(Instance: TObject; const Value: RawUTF8; wasString: boolean); override; procedure GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); override; procedure CopyValue(Source, Dest: TObject); override; procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override; function SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; override; procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override; procedure GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar; var temp: RawByteString); override; function SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; override; function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override; function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override; procedure NormalizeValue(var Value: RawUTF8); override; end; /// information about a RawUTF8 published property // - will also serialize a RawJSON property without JSON escape TSQLPropInfoRTTIRawUTF8 = class(TSQLPropInfoRTTIAnsi) protected procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override; public procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override; procedure SetValueVar(Instance: TObject; const Value: RawUTF8; wasString: boolean); override; procedure GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); override; procedure GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar; var temp: RawByteString); override; function SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; override; function SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; override; function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override; function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override; procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override; end; /// information about a RawUnicode published property TSQLPropInfoRTTIRawUnicode = class(TSQLPropInfoRTTIAnsi) protected procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override; public procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override; procedure SetValueVar(Instance: TObject; const Value: RawUTF8; wasString: boolean); override; procedure GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); override; function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override; function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override; end; /// information about a TSQLRawBlob published property TSQLPropInfoRTTIRawBlob = class(TSQLPropInfoRTTIAnsi) protected procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override; public procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override; procedure SetValueVar(Instance: TObject; const Value: RawUTF8; wasString: boolean); override; procedure GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); override; procedure GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar; var temp: RawByteString); override; function SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; override; function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override; function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override; procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override; procedure GetBlob(Instance: TObject; var Blob: RawByteString); procedure SetBlob(Instance: TObject; const Blob: RawByteString); function IsNull(Instance: TObject): Boolean; end; /// information about a WideString published property TSQLPropInfoRTTIWide = class(TSQLPropInfoRTTI) protected procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override; public procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override; procedure GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); override; procedure CopyValue(Source, Dest: TObject); override; procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override; function SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; override; procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override; function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override; function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override; end; {$ifdef HASVARUSTRING} /// information about a UnicodeString published property TSQLPropInfoRTTIUnicode = class(TSQLPropInfoRTTI) protected procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override; public procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override; procedure SetValueVar(Instance: TObject; const Value: RawUTF8; wasString: boolean); override; procedure GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); override; procedure CopyValue(Source, Dest: TObject); override; procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override; function SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; override; procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override; procedure GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar; var temp: RawByteString); override; function SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; override; function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override; function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override; end; {$endif HASVARUSTRING} /// information about a dynamic array published property TSQLPropInfoRTTIDynArray = class(TSQLPropInfoRTTI) protected fObjArray: PClassInstance; fWrapper: TDynArray; procedure GetDynArray(Instance: TObject; var result: TDynArray); {$ifdef HASINLINE}inline;{$endif} function GetDynArrayElemType: PTypeInfo; {$ifdef HASINLINE}inline;{$endif} /// will create TDynArray.SaveTo by default, or JSON if is T*ObjArray procedure Serialize(Instance: TObject; var data: RawByteString; ExtendedJson: boolean); virtual; procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override; public /// initialize the internal fields // - should not be called directly, but with dedicated class methods like // class function CreateFrom() constructor Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType; aOptions: TSQLPropInfoListOptions); override; procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override; procedure GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); override; procedure GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar; var temp: RawByteString); override; function SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; override; procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override; function SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; override; function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override; function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override; procedure NormalizeValue(var Value: RawUTF8); override; procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override; {$ifndef NOVARIANTS} procedure GetVariant(Instance: TObject; var Dest: Variant); override; procedure SetVariant(Instance: TObject; const Source: Variant); override; {$endif} /// optional index of the dynamic array published property // - used e.g. for fast lookup by TSQLRecord.DynArray(DynArrayFieldIndex) property DynArrayIndex: integer read fFieldWidth; /// read-only access to the low-level type information the array item type property DynArrayElemType: PTypeInfo read GetDynArrayElemType; /// dynamic array item information for a T*ObjArray // - equals nil if this dynamic array was not previously registered via // TJSONSerializer.RegisterObjArrayForJSON() // - note that if the field is a T*ObjArray, you could create a new item // by calling ObjArray^.CreateNew // - T*ObjArray database column will be stored as text property ObjArray: PClassInstance read fObjArray; end; TSQLPropInfoRTTIDynArrayObjArray = array of TSQLPropInfoRTTIDynArray; {$ifndef NOVARIANTS} /// information about a variant published property // - is also used for TNullable* properties TSQLPropInfoRTTIVariant = class(TSQLPropInfoRTTI) protected fDocVariantOptions: TDocVariantOptions; procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override; public /// initialize the internal fields constructor Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType; aOptions: TSQLPropInfoListOptions); override; procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override; procedure SetValueVar(Instance: TObject; const Value: RawUTF8; wasString: boolean); override; procedure SetValuePtr(Instance: TObject; Value: PUTF8Char; ValueLen: integer; wasString: boolean); procedure GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); override; procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override; function SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; override; function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override; function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override; procedure NormalizeValue(var Value: RawUTF8); override; procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override; procedure GetVariant(Instance: TObject; var Dest: Variant); override; procedure SetVariant(Instance: TObject; const Source: Variant); override; /// how this property will deal with its instances (including TDocVariant) // - by default, contains JSON_OPTIONS_FAST for best performance - i.e. // [dvoReturnNullForUnknownProperty,dvoValueCopiedByReference] // - set JSON_OPTIONS_FAST_EXTENDED (or include dvoSerializeAsExtendedJson) // so that any TDocVariant nested field names will not be double-quoted, // saving some chars in the stored TEXT column and in the JSON escaped // transmitted data over REST, by writing '{name:"John",age:123}' instead of // '{"name":"John","age":123}': be aware that this syntax is supported by // the ORM, SOA, TDocVariant, TBSONVariant, and our SynCrossPlatformJSON // unit, but not AJAX/JavaScript or most JSON libraries // - see also TSQLModel/TSQLRecordProperties.SetVariantFieldsDocVariantOptions property DocVariantOptions: TDocVariantOptions read fDocVariantOptions write fDocVariantOptions; end; {$endif NOVARIANTS} /// optional event handler used by TSQLPropInfoRecord to handle textual storage // - by default, TSQLPropInfoRecord content will be stored as sftBlobCustom; // specify such a callback event to allow storage as UTF-8 textual field and // use a sftUTF8Custom kind of column // - event implementation shall convert data/datalen binary value into Text TOnSQLPropInfoRecord2Text = procedure(Data: pointer; DataLen: integer; var Text: RawUTF8); /// optional event handler used by TSQLPropInfoRecord to handle textual storage // - by default, TSQLPropInfoRecord content will be stored as sftBlobCustom; // specify such a callback event to allow storage as UTF-8 textual field and // use a sftUTF8Custom kind of column // - event implementaiton shall convert Text into Data binary value TOnSQLPropInfoRecord2Data = procedure(Text: PUTF8Char; var Data: RawByteString); /// abstract information about a record-like property defined directly in code // - do not use this class, but TSQLPropInfoRecordRTTI and TSQLPropInfoRecordFixedSize // - will store the content as BLOB by default, and SQLFieldType as sftBlobCustom // - if aData2Text/aText2Data are defined, use TEXT storage and sftUTF8Custom type TSQLPropInfoCustom = class(TSQLPropInfo) protected fOffset: PtrUInt; fData2Text: TOnSQLPropInfoRecord2Text; fText2Data: TOnSQLPropInfoRecord2Data; procedure BinaryToText(var Value: RawUTF8; ToSQL: boolean; wasSQLString: PBoolean); override; procedure TextToBinary(Value: PUTF8Char; var result: RawByteString); override; public /// define a custom property in code // - do not call this constructor directly, but one of its inherited classes, // via a call to TSQLRecordProperties.RegisterCustom*() constructor Create(const aName: RawUTF8; aSQLFieldType: TSQLFieldType; aAttributes: TSQLPropInfoAttributes; aFieldWidth, aPropIndex: Integer; aProperty: pointer; aData2Text: TOnSQLPropInfoRecord2Text; aText2Data: TOnSQLPropInfoRecord2Data); reintroduce; public function GetFieldAddr(Instance: TObject): pointer; override; end; /// information about a record property defined directly in code using RTTI TSQLPropInfoRecordTyped = class(TSQLPropInfoCustom) protected fTypeInfo: PTypeInfo; public property TypeInfo: PTypeInfo read fTypeInfo; end; /// information about a record property defined directly in code // - Delphi does not publish RTTI for published record properties // - you can use this class to register a record property from its RTTI // - will store the content as BLOB by default, and SQLFieldType as sftBlobCustom // - if aData2Text/aText2Data are defined, use TEXT storage and sftUTF8Custom type // - this class will use only binary RecordLoad/RecordSave methods TSQLPropInfoRecordRTTI = class(TSQLPropInfoRecordTyped) protected function GetSQLFieldRTTITypeName: RawUTF8; override; procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override; public /// define a record property from its RTTI definition // - handle any kind of record with available generated TypeInfo() // - aPropertyPointer shall be filled with the offset to the private // field within a nil object, e.g for // ! class TMainObject = class(TSQLRecord) // ! (...) // ! fFieldName: TMyRecord; // ! public // ! (...) // ! property FieldName: TMyRecord read fFieldName write fFieldName; // ! end; // you will have to register it via a call to // TSQLRecordProperties.RegisterCustomRTTIRecordProperty() // - optional aIsNotUnique parametercanl be defined // - implementation will use internally RecordLoad/RecordSave functions // - you can specify optional aData2Text/aText2Data callbacks to store // the content as textual values, and not as BLOB constructor Create(aRecordInfo: PTypeInfo; const aName: RawUTF8; aPropertyIndex: integer; aPropertyPointer: pointer; aAttributes: TSQLPropInfoAttributes=[]; aFieldWidth: integer=0; aData2Text: TOnSQLPropInfoRecord2Text=nil; aText2Data: TOnSQLPropInfoRecord2Data=nil); reintroduce; overload; public procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override; procedure GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); override; procedure GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar; var temp: RawByteString); override; function SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; override; procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override; function SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; override; function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override; function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override; procedure NormalizeValue(var Value: RawUTF8); override; {$ifndef NOVARIANTS} procedure GetVariant(Instance: TObject; var Dest: Variant); override; procedure SetVariant(Instance: TObject; const Source: Variant); override; {$endif} end; /// information about a fixed-size record property defined directly in code // - Delphi does not publish RTTI for published record properties // - you can use this class to register a record property with no RTTI (i.e. // a record with no reference-counted types within) // - will store the content as BLOB by default, and SQLFieldType as sftBlobCustom // - if aData2Text/aText2Data are defined, use TEXT storage and sftUTF8Custom type TSQLPropInfoRecordFixedSize = class(TSQLPropInfoRecordTyped) protected fRecordSize: integer; function GetSQLFieldRTTITypeName: RawUTF8; override; procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override; public /// define an unmanaged fixed-size record property // - simple kind of records (i.e. those not containing reference-counted // members) do not have RTTI generated, at least in older versions of Delphi: // use this constructor to define a direct property access // - main parameter is the record size, in bytes constructor Create(aRecordSize: cardinal; const aName: RawUTF8; aPropertyIndex: integer; aPropertyPointer: pointer; aAttributes: TSQLPropInfoAttributes=[]; aFieldWidth: integer=0; aData2Text: TOnSQLPropInfoRecord2Text=nil; aText2Data: TOnSQLPropInfoRecord2Data=nil); reintroduce; overload; public procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override; procedure GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); override; procedure GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar; var temp: RawByteString); override; function SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; override; procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override; function SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; override; function CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; override; function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override; procedure NormalizeValue(var Value: RawUTF8); override; {$ifndef NOVARIANTS} procedure GetVariant(Instance: TObject; var Dest: Variant); override; procedure SetVariant(Instance: TObject; const Source: Variant); override; {$endif} end; /// information about a custom property defined directly in code // - you can define any kind of property, either a record or any type // - this class will use JSON serialization, by type name or TypeInfo() pointer // - will store the content as TEXT by default, and SQLFieldType as sftUTF8Custom TSQLPropInfoCustomJSON = class(TSQLPropInfoRecordTyped) protected fCustomParser: TJSONCustomParserRTTI; function GetSQLFieldRTTITypeName: RawUTF8; override; procedure SetCustomParser(aCustomParser: TJSONCustomParserRTTI); public /// initialize the internal fields // - should not be called directly constructor Create(aPropInfo: PPropInfo; aPropIndex: integer); reintroduce; overload; virtual; /// define a custom property from its RTTI definition // - handle any kind of property, e.g. from enhanced RTTI or a custom record // defined via TTextWriter.RegisterCustomJSONSerializer[FromText]() // - aPropertyPointer shall be filled with the offset to the private // field within a nil object, e.g for // ! class TMainObject = class(TSQLRecord) // ! (...) // ! fFieldName: TMyRecord; // ! public // ! (...) // ! property FieldName: TMyRecord read fFieldName write fFieldName; // ! end; // you will have to register it via a call to // TSQLRecordProperties.RegisterCustomPropertyFromRTTI() // - optional aIsNotUnique parameter can be defined // - implementation will use internally RecordLoadJSON/RecordSave functions // - you can specify optional aData2Text/aText2Data callbacks to store // the content as textual values, and not as BLOB constructor Create(aTypeInfo: PTypeInfo; const aName: RawUTF8; aPropertyIndex: integer; aPropertyPointer: pointer; aAttributes: TSQLPropInfoAttributes=[]; aFieldWidth: integer=0); reintroduce; overload; /// define a custom property from its RTTI definition // - handle any kind of property, e.g. from enhanced RTTI or a custom record // defined via TTextWriter.RegisterCustomJSONSerializer[FromText]() // - aPropertyPointer shall be filled with the offset to the private // field within a nil object, e.g for // ! class TMainObject = class(TSQLRecord) // ! (...) // ! fGUID: TGUID; // ! public // ! (...) // ! property GUID: TGUID read fGUID write fGUID; // ! end; // you will have to register it via a call to // TSQLRecordProperties.RegisterCustomPropertyFromTypeName() // - optional aIsNotUnique parameter can be defined // - implementation will use internally RecordLoadJSON/RecordSave functions // - you can specify optional aData2Text/aText2Data callbacks to store // the content as textual values, and not as BLOB constructor Create(const aTypeName, aName: RawUTF8; aPropertyIndex: integer; aPropertyPointer: pointer; aAttributes: TSQLPropInfoAttributes=[]; aFieldWidth: integer=0); reintroduce; overload; /// finalize the instance destructor Destroy; override; /// the corresponding custom JSON parser property CustomParser: TJSONCustomParserRTTI read fCustomParser; public procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override; procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override; procedure GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); override; procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override; function SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; override; procedure NormalizeValue(var Value: RawUTF8); override; end; /// dynamic array of ORM fields information for published properties TSQLPropInfoObjArray = array of TSQLPropInfo; /// handle a read-only list of fields information for published properties // - is mainly used by our ORM for TSQLRecord RTTI, but may be used for // any TPersistent TSQLPropInfoList = class protected fList: TSQLPropInfoObjArray; fCount: integer; fTable: TClass; fOptions: TSQLPropInfoListOptions; fOrderedByName: TIntegerDynArray; function GetItem(aIndex: integer): TSQLPropInfo; procedure QuickSortByName(L,R: PtrInt); procedure InternalAddParentsFirst(aClassType: TClass); overload; procedure InternalAddParentsFirst(aClassType: TClass; aFlattenedProps: PPropInfoDynArray); overload; public /// initialize the list from a given class RTTI constructor Create(aTable: TClass; aOptions: TSQLPropInfoListOptions); /// release internal list items destructor Destroy; override; /// add a TSQLPropInfo to the list function Add(aItem: TSQLPropInfo): integer; /// find an item in the list // - returns nil if not found function ByRawUTF8Name(const aName: RawUTF8): TSQLPropInfo; overload; {$ifdef HASINLINE}inline;{$endif} /// find an item in the list // - returns nil if not found function ByName(aName: PUTF8Char): TSQLPropInfo; overload; {$ifdef HASINLINE}inline;{$endif} /// find an item in the list // - returns -1 if not found function IndexByName(const aName: RawUTF8): integer; overload; {$ifdef HASINLINE}inline;{$endif} /// find an item in the list // - returns -1 if not found function IndexByName(aName: PUTF8Char): integer; overload; /// find an item by name in the list, including RowID/ID // - will identify 'ID' / 'RowID' field name as -1 // - raise an EORMException if not found in the internal list function IndexByNameOrExcept(const aName: RawUTF8): integer; /// find one or several items by name in the list, including RowID/ID // - will identify 'ID' / 'RowID' field name as -1 // - raise an EORMException if not found in the internal list procedure IndexesByNamesOrExcept(const aNames: array of RawUTF8; const aIndexes: array of PInteger); /// find an item in the list, searching by unflattened name // - for a flattened property, you may for instance call // IndexByNameUnflattenedOrExcept('Address.Country.Iso') // instead of IndexByNameOrExcept('Address_Country') // - won't identify 'ID' / 'RowID' field names, just List[]. // - raise an EORMException if not found in the internal list function IndexByNameUnflattenedOrExcept(const aName: RawUTF8): integer; /// fill a TRawUTF8DynArray instance from the field names // - excluding ID procedure NamesToRawUTF8DynArray(var Names: TRawUTF8DynArray); /// returns the number of TSQLPropInfo in the list property Count: integer read fCount; /// quick access to the TSQLPropInfo list // - note that length(List) may not equal Count, since is its capacity property List: TSQLPropInfoObjArray read fList; /// read-only retrieval of a TSQLPropInfo item // - will raise an exception if out of range property Items[aIndex: integer]: TSQLPropInfo read GetItem; end; /// simple writer to a Stream, specialized for writing an object as INI // - resulting content will be UTF-8 encoded // - use an internal buffer, faster than string+string TINIWriter = class(TTextWriter) /// write the published properties of the object in INI text format // - i.e. append PropertyName=PropertyValue lines // - add a new INI-like section with [Value.ClassName] if WithSection is true // - use internally TPropInfo.GetToText for the conversion to text // - Value object must have been compiled with the $M+ define, i.e. must // inherit from TPersistent, TSynPersistent or TSQLRecord // - the enumerates properties are stored with their integer index value // - dynamic arrays will be serialized as JSON, unless RawUTF8DynArrayAsCSV // is set, and a TRawUTF8DynArray property will be stored as CSV // - content can be read back using overloaded procedures ReadObject() procedure WriteObject(Value: TObject; const SubCompName: RawUTF8=''; WithSection: boolean=true; RawUTF8DynArrayAsCSV: boolean=false); reintroduce; end; /// method prototype to be used for custom serialization of a class // - to be used with TJSONSerializer.RegisterCustomSerializer() method // - note that the generated JSON content is not required to start with '{', // as a normal JSON object (you may e.g. write a JSON string for some class) - // as a consequence, custom code could explicitely start with Add('{') // - implementation code shall follow function TJSONSerializer.WriteObject() // patterns, i.e. aSerializer.Add/AddInstanceName/AddJSONEscapeString... // - implementation code shall follow the same exact format for the // associated TJSONSerializerCustomReader callback TJSONSerializerCustomWriter = procedure(const aSerializer: TJSONSerializer; aValue: TObject; aOptions: TTextWriterWriteObjectOptions) of object; /// method prototype to be used for custom un-serialization of a class // - to be used with TJSONSerializer.RegisterCustomSerializer() method // - note that the read JSON content is not required to start with '{', // as a normal JSON object (you may e.g. read a JSON string for some class) - // as a consequence, custom code could explicitely start with "if aFrom^='{'..." // - implementation code shall follow function JSONToObject() patterns, i.e. // calling low-level GetJSONField() function to decode the JSON content // - implementation code shall follow the same exact format for the // associated TJSONSerializerCustomWriter callback TJSONSerializerCustomReader = function(const aValue: TObject; aFrom: PUTF8Char; var aValid: Boolean; aOptions: TJSONToObjectOptions): PUTF8Char of object; /// several options to customize how TSQLRecord will be serialized // - e.g. if properties storing JSON should be serialized as an object, and not // escaped as a string (which is the default, matching ORM column storage) // - if an additional "ID_str":"12345" field should be added to the standard // "ID":12345 field, which may exceed 53-bit integer precision of JavsCript TJSONSerializerSQLRecordOption = ( jwoAsJsonNotAsString, jwoID_str); /// options to customize how TSQLRecord will be written by TJSONSerializer TJSONSerializerSQLRecordOptions = set of TJSONSerializerSQLRecordOption; /// simple writer to a Stream, specialized for writing an object as JSON // - override WriteObject() to use class RTTI process of this unit, and // allow custom JSON serialization // - this is the full-feature JSON serialization class TJSONSerializer = class(TJSONWriter) protected fSQLRecordOptions: TJSONSerializerSQLRecordOptions; procedure SetSQLRecordOptions(Value: TJSONSerializerSQLRecordOptions); public /// serialize as JSON the published integer, Int64, floating point values, // TDateTime (stored as ISO 8601 text), string and enumerate (e.g. boolean) // properties of the object // - won't handle shortstring properties // - the object must have been compiled with the $M+ define, i.e. must // inherit from TPersistent or TSQLRecord, or has been defined with a // custom serializer via RegisterCustomSerializer() // - will write also the properties published in the parent classes // - the enumerates properties are stored with their integer index value by // default, but will be written as text if woFullExpand option is set // - TList objects are not handled by default - they will be written only // if FullExpand is set to true (and JSONToObject won't be able to read it) // - nested properties are serialized as nested JSON objects // - any TCollection property will also be serialized as JSON array // - any TStrings or TRawUTF8List property will also be serialized as // JSON string array // - function ObjectToJSON() is just a wrapper over this method procedure WriteObject(Value: TObject; Options: TTextWriterWriteObjectOptions=[woDontStoreDefault]); override; /// customize TSQLRecord.GetJSONValues serialization process // - jwoAsJsonNotAsString will force TSQLRecord.GetJSONValues to serialize // nested property instances as a JSON object/array, not a JSON string: // i.e. root/table/id REST will be ready-to-be-consumed from AJAX clients // (e.g. TSQLPropInfoRTTIObject.GetJSONValues as a JSON object, and // TSQLPropInfoRTTIDynArray.GetJSONValues as a JSON array) // - jwoID_str will add an "ID_str":"12345" property to the default // "ID":12345 field to circumvent JavaScript's limitation of 53-bit for // integer numbers, which is easily reached with our 64-bit TID values, e.g. // if TSynUniqueIdentifier are used to generate the IDs: AJAX clients should // better use this "ID_str" string value to identify each record, and ignore // the "id" fields property SQLRecordOptions: TJSONSerializerSQLRecordOptions read fSQLRecordOptions write SetSQLRecordOptions; /// define a custom serialization for a given class // - by default, TSQLRecord, TPersistent, TStrings, TCollection classes // are processed: but you can specify here some callbacks to perform // the serialization process for any class // - any previous registration is overridden // - setting both aReader=aWriter=nil will return back to the default class // serialization (i.e. published properties serialization) // - note that any inherited classes will be serialized as the parent class // - this method is thread-safe, but should be called before any serialization class procedure RegisterCustomSerializer(aClass: TClass; aReader: TJSONSerializerCustomReader; aWriter: TJSONSerializerCustomWriter); /// define custom serialization of field names for a given class // - any aClassField[] property name will be serialized using aJsonFields[] // - if any aJsonFields[] equals '', this published property will be // excluded from the serialization object // - aJsonFields[] is expected to be only plain pascal identifier, i.e. // A-Z a-z 0-9 and _ characters, up to 63 in length // - setting both aClassField=aJsonFields=[] will return back to the default // class serialization (i.e. serialization with published properties names) // - by design, this customization excludes RegisterCustomSerializer() with // custom reader/writer callbacks // - note that any inherited classes will be serialized as the parent class // - this method is thread-safe, but should be called before any serialization class procedure RegisterCustomSerializerFieldNames(aClass: TClass; const aClassFields, aJsonFields: array of ShortString); /// let a given class be recognized by JSONToObject() from "ClassName":".." // - TObjectList item instances will be created corresponding to the // serialized class name field specified, and JSONToNewObject() can create a // new instance using the "ClassName":"..." field to identify the class type // - by default, all referenced TSQLRecord classes will be globally // registered when TSQLRecordProperties information is retrieved // - this method is thread-safe, but should be called before any serialization class procedure RegisterClassForJSON(aItemClass: TClass); overload; /// let a given class be recognized by JSONToObject() from "ClassName":".." // - TObjectList item instances will be created corresponding to the // serialized class name field specified, and JSONToNewObject() can create a // new instance using the "ClassName":"..." field to identify the class type // - by default, all referenced TSQLRecord classes will be globally // registered when TSQLRecordProperties information is retrieved // - this method is thread-safe, but should be called before any serialization class procedure RegisterClassForJSON(const aItemClass: array of TClass); overload; {$ifndef LVCL} /// let a given TCollection be recognized during JSON serialization // - due to how TCollection instances are created, you can not create a // server-side instance of TCollection directly // - first workaround is to inherit from TInterfacedCollection // - this method allows to recognize the needed TCollectionItem class for // a given TCollection class, so allow to (un)serialize any TCollection, // without defining a new method and inherits from TInterfacedCollection // - note that both supplied classes will be registered for the internal // "ClassName":"..." RegisterClassForJSON() process // - this method is thread-safe, but should be called before any serialization class procedure RegisterCollectionForJSON(aCollection: TCollectionClass; aItem: TCollectionItemClass); {$endif} /// let a T*ObjArray dynamic array be used for storage of class instances // - will allow JSON serialization and unserialization of the registered // dynamic array property defined in any TPersistent or TSQLRecord // - could be used as such (note the T*ObjArray type naming convention): // ! TUserObjArray = array of TUser; // ! ... // ! TJSONSerializer.RegisterObjArrayForJSON(TypeInfo(TUserObjArray),TUser); // - then you can use ObjArrayAdd/ObjArrayFind/ObjArrayDelete to manage // the stored items, and never forget to call ObjArrayClear to release // the memory // - will use the default published properties serializer, unless you specify // your custom Reader/Write callbacks class procedure RegisterObjArrayForJSON(aDynArray: PTypeInfo; aItem: TClass; aReader: TDynArrayJSONCustomReader=nil; aWriter: TDynArrayJSONCustomWriter=nil);overload; /// let T*ObjArray dynamic arrays be used for storage of class instances // - will allow JSON serialization and unserialization of the registered // dynamic array property defined in any TPersistent or TSQLRecord // - will call the overloaded RegisterObjArrayForJSON() class method by pair: // ! TJSONSerializer.RegisterObjArrayForJSON([ // ! TypeInfo(TAddressObjArray),TAddress, TypeInfo(TUserObjArray),TUser]); class procedure RegisterObjArrayForJSON(const aDynArrayClassPairs: array of const); overload; /// retrieve TClassInstance information for a T*ObjArray dynamic array type // - the T*ObjArray dynamic array should have been previously registered // via TJSONSerializer.RegisterObjArrayForJSON() overloaded methods // - returns nil if the supplied type is not a registered T*ObjArray class function RegisterObjArrayFindType(aDynArray: PTypeInfo): PClassInstance; /// retrieve the T*ObjArray dynamic array type RTTI for a given item class // - the T*ObjArray dynamic array should have been previously registered // via TJSONSerializer.RegisterObjArrayForJSON() overloaded methods // - returns nil if the supplied type is not a registered T*ObjArray class function RegisterObjArrayFindTypeInfo(aClass: TClass): PTypeInfo; end; const /// fake TTypeInfo RTTI used for TGUID on older versions of the compiler GUID_FAKETYPEINFO: packed record Kind: TTypeKind; Name: string[5]; Size: cardinal; Count: integer; end = ( Kind: tkRecord; Name: 'TGUID'; Size: SizeOf(TGUID); Count: 0); /// returns the interface name of a registered GUID, or its hexadecimal value function ToText({$IFDEF FPC_HAS_CONSTREF}constref{$ELSE}const{$ENDIF} aGUID: TGUID): TGUIDShortString; overload; /// retrieve a Field property RTTI information from a Property Name function ClassFieldProp(ClassType: TClass; const PropName: shortstring): PPropInfo; /// retrieve a Field property RTTI information from a Property Name // - this special version also search into parent properties (default is only current) function ClassFieldPropWithParents(aClassType: TClass; const aPropName: shortstring; aCaseSensitive: boolean=false): PPropInfo; /// retrieve an integer/Int64 Field propery value from a Property Name // - this version also search into parent properties // - returns TRUE and set PropValue if a matching property was found function ClassFieldInt64(Instance: TObject; const PropName: ShortString; out PropValue: Int64): boolean; /// retrieve a class Field property instance from a Property Name // - this version also search into parent properties // - returns TRUE and set PropInstance if a matching property was found function ClassFieldInstance(Instance: TObject; const PropName: shortstring; PropClassType: TClass; out PropInstance): boolean; overload; /// retrieve a Field property RTTI information from a Property Name // - this special version also search into parent properties (default is only current) function ClassFieldPropWithParentsFromUTF8(aClassType: TClass; PropName: PUTF8Char; PropNameLen: integer; aCaseSensitive: boolean=false): PPropInfo; /// retrieve a Field property RTTI information searching for an exact Property class type // - this special version also search into parent properties function ClassFieldPropWithParentsFromClassType(aClassType,aSearchedClassType: TClass): PPropInfo; /// retrieve a Field property RTTI information searching for an inherited Property class type // - this special version also search into parent properties function ClassFieldPropWithParentsInheritsFromClassType(aClassType,aSearchedClassType: TClass): PPropInfo; /// retrieve a Field property RTTI information searching for an exact Property offset address // - this special version also search into parent properties function ClassFieldPropWithParentsFromClassOffset(aClassType: TClass; aSearchedOffset: pointer): PPropInfo; /// retrieve a class Field property instance from a Property class type // - this version also search into parent properties // - returns TRUE and set PropInstance if a matching property was found function ClassFieldInstance(Instance: TObject; PropClassType: TClass; out PropInstance): boolean; overload; /// retrieve all class Field property instances from a Property class type // - this version also search into parent properties // - returns all matching property instances found function ClassFieldInstances(Instance: TObject; PropClassType: TClass): TObjectDynArray; /// retrieve a class instance property value matching a class type // - if aSearchedInstance is aSearchedClassType, will return aSearchedInstance // - if aSearchedInstance is not aSearchedClassType, it will try all nested // properties of aSearchedInstance for a matching aSearchedClassType: if no // exact match is found, will return aSearchedInstance function ClassFieldPropInstanceMatchingClass(aSearchedInstance: TObject; aSearchedClassType: TClass): TObject; /// retrieve the total number of properties for a class, including its parents function ClassFieldCountWithParents(ClassType: TClass; onlyWithoutGetter: boolean=false): integer; /// returns TRUE if the class has some published fields, including its parents function ClassHasPublishedFields(ClassType: TClass): boolean; /// retrieve all class hierachy types which have some published properties function ClassHierarchyWithField(ClassType: TClass): TClassDynArray; /// retrieve the PPropInfo values of all published properties of a class // - you could select which property types should be included in the list function ClassFieldAllProps(ClassType: TClass; Types: TTypeKinds=[low(TTypeKind)..high(TTypeKind)]): PPropInfoDynArray; /// retrieve the field names of all published properties of a class // - will optionally append the property type to the name, e.g 'Age: integer' // - you could select which property types should be included in the list function ClassFieldNamesAllProps(ClassType: TClass; IncludePropType: boolean=false; Types: TTypeKinds=[low(TTypeKind)..high(TTypeKind)]): TRawUTF8DynArray; /// retrieve the field names of all published properties of a class // - will optionally append the property type to the name, e.g 'Age: integer' // - you could select which property types should be included in the list function ClassFieldNamesAllPropsAsText(ClassType: TClass; IncludePropType: boolean=false; Types: TTypeKinds=[low(TTypeKind)..high(TTypeKind)]): RawUTF8; /// retrieve an object's component from its property name and class // - useful to set User Interface component, e.g. function GetObjectComponent(Obj: TPersistent; const ComponentName: shortstring; ComponentClass: TClass): pointer; /// retrieve the class property RTTI information for a specific class function InternalClassProp(ClassType: TClass): PClassProp; {$ifdef FPC}inline;{$else}{$ifdef HASINLINENOTX86}inline;{$endif}{$endif} /// retrieve the class property RTTI information for a specific class // - will return the number of published properties // - and set the PropInfo variable to point to the first property // - typical use to enumerate all published properties could be: // ! var i: integer; // ! CT: TClass; // ! P: PPropInfo; // ! begin // ! CT := ..; // ! repeat // ! for i := 1 to InternalClassPropInfo(CT,P) do begin // ! // use P^ // ! P := P^.Next; // ! end; // ! CT := GetClassParent(CT); // ! until CT=nil; // ! end; // such a loop is much faster than using the RTL's TypeInfo or RTTI units function InternalClassPropInfo(ClassType: TClass; out PropInfo: PPropInfo): integer; /// retrieve a method RTTI information for a specific class function InternalMethodInfo(aClassType: TClass; const aMethodName: ShortString): PMethodInfo; /// execute an instance method from its RTTI per-interface information // - calling this function with a pre-computed PInterfaceEntry value is faster // than calling the TObject.GetInterface() method, especially when the class // implements several interfaces, since it avoid a slow GUID lookup function GetInterfaceFromEntry(Instance: TObject; Entry: PInterfaceEntry; out Obj): boolean; /// retrieve the ready to be displayed text of an enumeration // - will "uncamel" then translate into a generic VCL string // - aIndex will be converted to the matching ordinal value (byte or word) function GetEnumCaption(aTypeInfo: PTypeInfo; const aIndex): string; /// get the corresponding enumeration name, without the first lowercase chars // (otDone -> 'Done') // - aIndex will be converted to the matching ordinal value (byte or word) // - this will return the code-based English text; use GetEnumCaption() to // retrieve the enumeration display text function GetEnumNameTrimed(aTypeInfo: PTypeInfo; const aIndex): RawUTF8; /// get all included values of an enumeration set, as CSV names function GetSetNameCSV(aTypeInfo: PTypeInfo; const aValue): RawUTF8; var /// a shared list of T*ObjArray registered serializers // - you should not access this variable, but via inline methods ObjArraySerializers: TPointerClassHash; {$ifndef NOVARIANTS} /// fill a class instance from a TDocVariant object document properties // - returns FALSE if the variant is not a dvObject, TRUE otherwise function DocVariantToObject(var doc: TDocVariantData; obj: TObject): boolean; /// fill a T*ObjArray variable from a TDocVariant array document values // - will always erase the T*ObjArray instance, and fill it from arr values procedure DocVariantToObjArray(var arr: TDocVariantData; var objArray; objClass: TClass); overload; /// fill a T*ObjArray variable from a TDocVariant array document values // - will always erase the T*ObjArray instance, and fill it from arr values procedure DocVariantToObjArray(var arr: TDocVariantData; var objArray; objClass: PClassInstance); overload; /// will convert a blank TObject into a TDocVariant document instance function ObjectDefaultToVariant(aClass: TClass; aOptions: TDocVariantOptions): variant; overload; {$endif} { ************ cross-cutting classes and types } type {$ifndef LVCL} /// any TCollection used between client and server shall inherit from this class // - you should override the GetClass virtual method to provide the // expected collection item class to be used on server side // - another possibility is to register a TCollection/TCollectionItem pair // via a call to TJSONSerializer.RegisterCollectionForJSON() TInterfacedCollection = class(TCollection) protected /// you shall override this abstract method class function GetClass: TCollectionItemClass; virtual; abstract; public /// this constructor which will call GetClass to initialize the collection constructor Create; reintroduce; virtual; end; /// class-reference type (metaclass) of a TInterfacedCollection kind TInterfacedCollectionClass = class of TInterfacedCollection; /// abstract TCollectionItem class, which will instantiate all its nested class // published properties, then release them (and any T*ObjArray) when freed // - could be used for gathering of TCollectionItem properties, e.g. for // Domain objects in DDD, especially for list of value objects // - consider using T*ObjArray dynamic array published properties in your // value types instead of TCollection storage: T*ObjArray have a lower overhead // and are easier to work with, once TJSONSerializer.RegisterObjArrayForJSON // is called to register the T*ObjArray type // - note that non published (e.g. public) properties won't be instantiated, // serialized, nor released - but may contain weak references to other classes // - please take care that you will not create any endless recursion: you should // ensure that at one level, nested published properties won't have any class // instance refering to its owner (there is no weak reference - remember!) // - since the destructor will release all nested properties, you should // never store a reference to any of those nested instances if this owner // may be freed before TCollectionItemAutoCreateFields = class(TCollectionItem) public /// this overriden constructor will instantiate all its nested // TPersistent/TSynPersistent/TSynAutoCreateFields published properties constructor Create(Collection: TCollection); override; /// finalize the instance, and release its published properties destructor Destroy; override; end; {$endif LVCL} /// abstract TPersistent class, which will instantiate all its nested TPersistent // class published properties, then release them (and any T*ObjArray) when freed // - TSynAutoCreateFields is to be preferred in most cases, thanks to its // lower overhead // - note that non published (e.g. public) properties won't be instantiated, // serialized, nor released - but may contain weak references to other classes // - please take care that you will not create any endless recursion: you should // ensure that at one level, nested published properties won't have any class // instance refering to its owner (there is no weak reference - remember!) // - since the destructor will release all nested properties, you should // never store a reference to any of those nested instances if this owner // may be freed before TPersistentAutoCreateFields = class(TPersistentWithCustomCreate) public /// this overriden constructor will instantiate all its nested // TPersistent/TSynPersistent/TSynAutoCreateFields published properties constructor Create; override; /// finalize the instance, and release its published properties destructor Destroy; override; end; /// our own empowered TPersistentAutoCreateFields-like parent class // - this class is a perfect parent to store any data by value, e.g. DDD Value // Objects, Entities or Aggregates // - is defined as an abstract class able with a virtual constructor, RTTI // for published properties, and automatic memory management of all nested // class published properties: any class defined as a published property will // be owned by this instance - i.e. with strong reference // - will also release any T*ObjArray dynamic array storage of persistents, // previously registered via TJSONSerializer.RegisterObjArrayForJSON() // - nested published classes (or T*ObjArray) don't need to inherit from // TSynAutoCreateFields: they may be from any TPersistent/TSynPersistent type // - note that non published (e.g. public) properties won't be instantiated, // serialized, nor released - but may contain weak references to other classes // - please take care that you will not create any endless recursion: you should // ensure that at one level, nested published properties won't have any class // instance refering to its owner (there is no weak reference - remember!) // - since the destructor will release all nested properties, you should // never store a reference to any of those nested instances if this owner // may be freed before // - TPersistent/TPersistentAutoCreateFields have an unexpected speed overhead // due a giant lock introduced to manage property name fixup resolution // (which we won't use outside the VCL) - this class is definitively faster TSynAutoCreateFields = class(TSynPersistent) public /// this overriden constructor will instantiate all its nested // TPersistent/TSynPersistent/TSynAutoCreateFields published properties {$ifdef FPC_OR_PUREPASCAL} constructor Create; override; {$else} class function NewInstance: TObject; override; {$endif} /// finalize the instance, and release its published properties destructor Destroy; override; /// virtual method allowing instance customization after initialization // - called e.g. by JsonToObject, but may be executed after manual fields // assignment // - do nothing by default // - may be overriden for string interning or content customization procedure AfterLoad; virtual; end; /// adding locking methods to a TSynAutoCreateFields with virtual constructor TSynAutoCreateFieldsLocked = class(TSynAutoCreateFields) protected fSafe: TSynLocker; public /// initialize the object instance, and its associated lock constructor Create; override; /// release the instance (including the locking resource) destructor Destroy; override; /// access to the locking methods of this instance // - use Safe.Lock/TryLock with a try ... finally Safe.Unlock block property Safe: TSynLocker read fSafe; /// could be used as a short-cut to Safe.Lock procedure Lock; {$ifdef HASINLINE}inline;{$endif} /// could be used as a short-cut to Safe.UnLock procedure Unlock; {$ifdef HASINLINE}inline;{$endif} end; /// abstract TInterfacedObject class, which will instantiate all its nested // TPersistent/TSynPersistent published properties, then release them when freed // - will handle automatic memory management of all nested class and T*ObjArray // published properties: any class or T*ObjArray defined as a published // property will be owned by this instance - i.e. with strong reference // - non published properties (e.g. public) won't be instantiated, so may // store weak class references // - could be used for gathering of TCollectionItem properties, e.g. for // Domain objects in DDD, especially for list of value objects, with some // additional methods defined by an Interface // - since the destructor will release all nested properties, you should // never store a reference to any of those nested instances if this owner // may be freed before TInterfacedObjectAutoCreateFields = class(TInterfacedObjectWithCustomCreate) public /// this overriden constructor will instantiate all its nested // TPersistent/TSynPersistent/TSynAutoCreateFields class and T*ObjArray // published properties constructor Create; override; /// finalize the instance, and release its published properties destructor Destroy; override; end; /// abstract parent class able to store settings as JSON file TSynJsonFileSettings = class(TSynAutoCreateFields) protected fInitialJsonContent: RawUTF8; fFileName: TFileName; public /// read existing settings from a JSON content function LoadFromJson(var aJson: RawUTF8): boolean; /// read existing settings from a JSON file function LoadFromFile(const aFileName: TFileName): boolean; virtual; /// persist the settings as a JSON file, named from LoadFromFile() parameter procedure SaveIfNeeded; virtual; /// optional persistence file name, as set by LoadFromFile() property FileName: TFileName read fFileName; end; /// used by TRawUTF8ObjectCacheList to manage a list of information cache TRawUTF8ObjectCacheSettings = class(TSynPersistent) protected fTimeOutMS: integer; fPurgePeriodMS: integer; public /// will set default values to settings constructor Create; override; published /// period after which the cache information should be flushed // - use -1 to disable time out; any big value will be limited to 10 minutes // - default is 120000, i.e. 2 minutes property TimeOutMS: integer read fTimeOutMS write fTimeOutMS; // period after which TRawUTF8ObjectCacheList will search for expired entries // - use -1 to disable purge (not adviced, since may break process) // - default is 1000, i.e. 1 second property PurgePeriodMS: integer read fPurgePeriodMS write fPurgePeriodMS; end; TRawUTF8ObjectCacheList = class; /// maintain information cache for a given key // - after a given period of time, the entry is not deleted, but CacheClear // virtual method is called to release the associated data or services // - inherit from this abstract class to store your own key-defined information // or you own interface-based services TRawUTF8ObjectCache = class(TSynAutoCreateFieldsLocked) protected fKey: RawUTF8; // inherited class could publish fKey with a custom name fOwner: TRawUTF8ObjectCacheList; fTimeoutMS: integer; fTimeoutTix: Int64; /// should be called by inherited classes when information or services are set // - set fTimeoutTix according to fTimeoutMS, to enable timeout mechanism // - could be used when the content is refreshed, to increase the entry TTL // - caller should do Safe.Lock to ensure thread-safety procedure CacheSet; virtual; /// called by Destroy and TRawUTF8ObjectCacheList.DoPurge // - set fTimeoutTix := 0 (inherited should also release services interfaces) // - protected by Safe.Lock from TRawUTF8ObjectCacheList.DoPurge procedure CacheClear; virtual; public /// initialize the information cache entry // - should not be called directly, but by TRawUTF8ObjectCacheList.GetLocked constructor Create(aOwner: TRawUTF8ObjectCacheList; const aKey: RawUTF8); reintroduce; virtual; /// finalize the information cache entry // - will also call the virtual CacheClear method destructor Destroy; override; /// Dependency Injection using fOwner.OnKeyResolve, for the current Key function Resolve(const aInterface: TGUID; out Obj): boolean; /// access to the associated storage list property Owner: TRawUTF8ObjectCacheList read fOwner; end; /// class-reference type (metaclass) of a TRawUTF8ObjectCache // - used e.g. by TRawUTF8ObjectCacheClass.Create to generate the // expected cache instances TRawUTF8ObjectCacheClass = class of TRawUTF8ObjectCache; /// manage a list of information cache, identified by a hashed key // - you should better inherit from this class, to give a custom name and // constructor, or alter the default behavior // - will maintain a list of TRawUTF8ObjectCache instances TRawUTF8ObjectCacheList = class(TRawUTF8List) protected fSettings: TRawUTF8ObjectCacheSettings; fLog: TSynLogFamily; fLogEvent: TSynLogInfo; fClass: TRawUTF8ObjectCacheClass; fNextPurgeTix: Int64; fPurgeForceList: TRawUTF8List; fOnKeyResolve: TOnKeyResolve; procedure DoPurge; virtual; // returns fClass.Create by default: inherited classes may add custom check // or return nil if Key is invalid function NewObjectCache(const Key: RawUTF8): TRawUTF8ObjectCache; virtual; public /// initialize the cache-information for a given class // - inherited classes may reintroduce a new constructor, for ease of use constructor Create(aClass: TRawUTF8ObjectCacheClass; aSettings: TRawUTF8ObjectCacheSettings; aLog: TSynLogFamily; aLogEvent: TSynLogInfo; const aOnKeyResolve: TOnKeyResolve); reintroduce; /// finalize the cache information destructor Destroy; override; /// fill TRawUTF8ObjectCache with the matching key information // - an unknown key, but with a successful NewObjectCache() call, will // create and append a new fClass instance to the list (if onlyexisting // is left to its default FALSE) // - global or key-specific purge will be performed, if needed // - on success (true), output cache instance will be locked function GetLocked(const Key: RawUTF8; out cache: TRawUTF8ObjectCache; onlyexisting: boolean=false): boolean; virtual; /// you may call this method regularly to check for a needed purge // - if Settings.PurgePeriodMS is reached, each TRawUTF8ObjectCache instance // will check for its TimeOutMS and call CacheClear if information is outdated procedure TryPurge; /// register a key identifier so that next TryPurge will flush the entry // - a direct CacheClear may trigger a race condition in NewObjectCache: // so you may use this function e.g. from a SOA callback procedure AddToPurge(const Key: RawUTF8); virtual; /// this method will clear all associated information // - a regular Clear will destroy all TRawUTF8ObjectCache instances, // whereas this method will call CacheClear on each entry, so will // be more thread-safe and efficient in pratice procedure ForceCacheClear; /// access to the associated logging instance procedure Log(const TextFmt: RawUTF8; const TextArgs: array of const; Level: TSynLogInfo = sllNone); /// optional service locator for by-key Dependency Injection property OnKeyResolve: TOnKeyResolve read fOnKeyResolve write fOnKeyResolve; end; var /// acccess to Zip Deflate compression in level 6 as a TSynCompress class AlgoDeflate: TAlgoCompress; /// acccess to Zip Deflate compression in level 1 as a TAlgoCompress class AlgoDeflateFast: TAlgoCompress; const /// void HTTP Status Code (not a standard value, for internal use only) HTTP_NONE = 0; /// HTTP Status Code for "Continue" HTTP_CONTINUE = 100; /// HTTP Status Code for "Switching Protocols" HTTP_SWITCHINGPROTOCOLS = 101; /// HTTP Status Code for "Success" HTTP_SUCCESS = 200; /// HTTP Status Code for "Created" HTTP_CREATED = 201; /// HTTP Status Code for "Accepted" HTTP_ACCEPTED = 202; /// HTTP Status Code for "Non-Authoritative Information" HTTP_NONAUTHORIZEDINFO = 203; /// HTTP Status Code for "No Content" HTTP_NOCONTENT = 204; /// HTTP Status Code for "Reset Content" HTTP_RESETCONTENT = 205; /// HTTP Status Code for "Partial Content" HTTP_PARTIALCONTENT = 206; /// HTTP Status Code for "Multiple Choices" HTTP_MULTIPLECHOICES = 300; /// HTTP Status Code for "Moved Permanently" HTTP_MOVEDPERMANENTLY = 301; /// HTTP Status Code for "Found" HTTP_FOUND = 302; /// HTTP Status Code for "See Other" HTTP_SEEOTHER = 303; /// HTTP Status Code for "Not Modified" HTTP_NOTMODIFIED = 304; /// HTTP Status Code for "Use Proxy" HTTP_USEPROXY = 305; /// HTTP Status Code for "Temporary Redirect" HTTP_TEMPORARYREDIRECT = 307; /// HTTP Status Code for "Bad Request" HTTP_BADREQUEST = 400; /// HTTP Status Code for "Unauthorized" HTTP_UNAUTHORIZED = 401; /// HTTP Status Code for "Forbidden" HTTP_FORBIDDEN = 403; /// HTTP Status Code for "Not Found" HTTP_NOTFOUND = 404; // HTTP Status Code for "Method Not Allowed" HTTP_NOTALLOWED = 405; // HTTP Status Code for "Not Acceptable" HTTP_NOTACCEPTABLE = 406; // HTTP Status Code for "Proxy Authentication Required" HTTP_PROXYAUTHREQUIRED = 407; /// HTTP Status Code for "Request Time-out" HTTP_TIMEOUT = 408; /// HTTP Status Code for "Conflict" HTTP_CONFLICT = 409; /// HTTP Status Code for "Payload Too Large" HTTP_PAYLOADTOOLARGE = 413; /// HTTP Status Code for "Internal Server Error" HTTP_SERVERERROR = 500; /// HTTP Status Code for "Not Implemented" HTTP_NOTIMPLEMENTED = 501; /// HTTP Status Code for "Bad Gateway" HTTP_BADGATEWAY = 502; /// HTTP Status Code for "Service Unavailable" HTTP_UNAVAILABLE = 503; /// HTTP Status Code for "Gateway Timeout" HTTP_GATEWAYTIMEOUT = 504; /// HTTP Status Code for "HTTP Version Not Supported" HTTP_HTTPVERSIONNONSUPPORTED = 505; /// you can use this cookie value to delete a cookie on the browser side COOKIE_EXPIRED = '; Expires=Sat, 01 Jan 2010 00:00:01 GMT'; /// internal HTTP content-type for efficient static file sending // - detected e.g. by http.sys' THttpApiServer.Request or via the NGINX // X-Accel-Redirect header's THttpServer.Process for direct sending // - the OutCustomHeader should contain the proper 'Content-type: ....' // corresponding to the file (e.g. by calling GetMimeContentType() function // from SynCommons supplyings the file name) // - should match HTTP_RESP_STATICFILE constant defined in SynCrtSock.pas unit STATICFILE_CONTENT_TYPE = '!STATICFILE'; /// used to notify e.g. the THttpServerRequest not to wait for any response // from the client // - is not to be used in normal HTTP process, but may be used e.g. by // TWebSocketProtocolRest.ProcessFrame() to avoid to wait for an incoming // response from the other endpoint // - should match HTTP_RESP_NORESPONSE constant defined in SynCrtSock.pas unit NORESPONSE_CONTENT_TYPE = '!NORESPONSE'; /// internal HTTP content-type Header for efficient static file sending STATICFILE_CONTENT_TYPE_HEADER = HEADER_CONTENT_TYPE+STATICFILE_CONTENT_TYPE; /// uppercase version of HTTP header for static file content serving STATICFILE_CONTENT_TYPE_HEADER_UPPPER = HEADER_CONTENT_TYPE_UPPER+STATICFILE_CONTENT_TYPE; var /// convert any HTTP_* constant to a short English text // - see @http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html // - if SynCrtSock is linked (i.e. set mORMotHttpClient or mORMotHttpServer), // SynCrtSock StatusCodeToReason() function will be assigned, which is // somewhat faster, and more complete StatusCodeToErrorMessage: procedure(Code: integer; var result: RawUTF8); /// convert any HTTP_* constant to an integer error code and its English text // - see @http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html // - will call StatusCodeToErrorMessage() function StatusCodeToErrorMsg(Code: integer): shortstring; /// returns true for successful HTTP status codes, i.e. in 200..399 range // - will map mainly SUCCESS (200), CREATED (201), NOCONTENT (204), // PARTIALCONTENT (206), NOTMODIFIED (304) or TEMPORARYREDIRECT (307) codes // - any HTTP status not part of this range will be identified as erronous // request in the internal server statistics function StatusCodeIsSuccess(Code: integer): boolean; {$ifdef HASINLINE}inline;{$endif} /// check the supplied HTTP header to not contain more than one EOL // - to avoid unexpected HTTP body injection, e.g. from unsafe business code function IsInvalidHttpHeader(head: PUTF8Char; headlen: PtrInt): boolean; /// computes an URI with optional jwt authentication parameter // - if AuthenticationBearer is set, will add its values as additional parameter: // $ URI?authenticationbearer=URIAuthenticationBearer function AuthURI(const URI, URIAuthenticationBearer: RawUTF8): RawUTF8; type /// the available HTTP methods transmitted between client and server // - some custom verbs are available in addition to standard REST commands // - most of iana verbs are available // see http://www.iana.org/assignments/http-methods/http-methods.xhtml // - for basic CRUD operations, we consider Create=mPOST, Read=mGET, // Update=mPUT and Delete=mDELETE - even if it is not fully RESTful TSQLURIMethod = ( mNone, mGET, mPOST, mPUT, mDELETE, mHEAD, mBEGIN, mEND, mABORT, mLOCK, mUNLOCK, mSTATE, mOPTIONS, mPROPFIND, mPROPPATCH, mTRACE, mCOPY, mMKCOL, mMOVE, mPURGE, mREPORT, mMKACTIVITY, mMKCALENDAR,mCHECKOUT, mMERGE, mNOTIFY, mPATCH, mSEARCH, mCONNECT); /// set of available HTTP methods transmitted between client and server TSQLURIMethods = set of TSQLURIMethod; /// convert a string HTTP verb into its TSQLURIMethod enumerate function ToMethod(const method: RawUTF8): TSQLURIMethod; var /// the options used by TSynJsonFileSettings.SaveIfNeeded // - you can modify this global variable to customize the whole process SETTINGS_WRITEOPTIONS: TTextWriterWriteObjectOptions = [woHumanReadable, woStoreStoredFalse, woHumanReadableFullSetsAsStar, woHumanReadableEnumSetAsComment, woInt64AsHex]; /// the options used by TServiceFactoryServer.OnLogRestExecuteMethod // - you can modify this global variable to customize the whole process SERVICELOG_WRITEOPTIONS: TTextWriterWriteObjectOptions = [woDontStoreDefault, woDontStoreEmptyString, woDontStore0, woHideSynPersistentPassword]; /// the options used by TObjArraySerializer, TInterfacedObjectFake and // TServiceMethodExecute when serializing values as JSON // - used as DEFAULT_WRITEOPTIONS[DontStoreVoidJSON] // - you can modify this global variable to customize the whole process DEFAULT_WRITEOPTIONS: array[boolean] of TTextWriterWriteObjectOptions = ( [woDontStoreDefault, woSQLRawBlobAsBase64], [woDontStoreDefault, woDontStoreEmptyString, woDontStore0, woSQLRawBlobAsBase64]); {$ifdef MSWINDOWS} {$ifdef ISDELPHIXE} // fix Delphi XE imcompatilibility type TSecurityAttributes = packed record nLength: DWORD; lpSecurityDescriptor: Pointer; bInheritHandle: BOOL; end; const SECURITY_DESCRIPTOR_REVISION = 1; SECURITY_DESCRIPTOR_MIN_LENGTH = 20; {$endif ISDELPHIXE} {$endif MSWINDOWS} { ******************* process monitoring / statistics } type /// the time periods covered by TSynMonitorUsage process // - defines the resolution of information computed and stored TSynMonitorUsageGranularity = ( mugUndefined, mugMinute, mugHour, mugDay, mugMonth, mugYear); /// defines one or several time periods for TSynMonitorUsage process TSynMonitorUsageGranularities = set of TSynMonitorUsageGranularity; /// how the TSynMonitorUsage storage IDs are computed // - stored e.g. in TSQLMonitorUsage.ID primary key (after a shift) // - it follows a 23 bit pattern of hour (5 bit), day (5 bit), month (4 bit), // year (9 bit - starting at 2016) so that it is monotonic over time // - by default, will store the information using mugHour granularity (i.e. // values for the 60 minutes in a record), and pseudo-hours of 29, 30 and 31 // (see USAGE_ID_HOURMARKER[]) will identify mugDay, mugMonth and mugYear // consolidated statistics // - it will therefore store up to 24*365+365+12+1 = 9138 records per year // in the associated storage engine (so there is no actual need to purge it) {$ifdef USERECORDWITHMETHODS}TSynMonitorUsageID = record {$else}TSynMonitorUsageID = object{$endif} public /// the TID, as computed from time and granularity Value: integer; /// computes an ID corresponding to mugHour granularity of a given time // - minutes and seconds will be ignored // - mugHour granularity will store 0..59 information about each minute procedure From(Y,M,D,H: integer); overload; /// computes an ID corresponding to mugDay granularity of a given time // - hours, minutes and seconds will be merged // - mugDay granularity will store 0..23 information about each hour // - a pseudo hour of 29 (i.e. USAGE_ID_HOURMARKER[mugDay]) is used procedure From(Y,M,D: integer); overload; /// computes an ID corresponding to mugMonth granularity of a given time // - days, hours, minutes and seconds will be merged // - mugMonth granularity will store 0..31 information about each day // - a pseudo hour of 30 (i.e. USAGE_ID_HOURMARKER[mugMonth]) is used procedure From(Y,M: integer); overload; /// computes an ID corresponding to mugYear granularity of a given time // - months, days, hours, minutes and seconds will be merged // - mugYear granularity will store 0..11 information about each month // - a pseudo hour of 31 (i.e. USAGE_ID_HOURMARKER[mugYear]) is used procedure From(Y: integer); overload; /// computes an ID corresponding to a given time // - will set the ID with mugHour granularity, i.e. the information about // the given hour, stored as per minute 0..59 values // - minutes and seconds in supplied TimeLog value will therefore be ignored procedure FromTimeLog(const TimeLog: TTimeLog); /// computes an ID corresponding to the current UTC date/time // - minutes and seconds will be ignored procedure FromNowUTC; /// returns the date/time // - minutes and seconds will set to 0 function ToTimeLog: TTimeLog; /// convert to Iso-8601 encoded text function Text(Expanded: boolean; FirstTimeChar: AnsiChar = 'T'): RawUTF8; /// retrieve the resolution of the stored information // - i.e. either mugHour, mugDay, mugMonth or mugYear, which will store // a true 0..23 hour value (for mugHour), or 29/30/31 pseudo-hour (i.e. // USAGE_ID_HOURMARKER[mugDay/mugMonth/mugYear]) function Granularity: TSynMonitorUsageGranularity; /// change the resolution of the stored information procedure Truncate(gran: TSynMonitorUsageGranularity); /// low-level read of a time field stored in this ID, per granularity function GetTime(gran: TSynMonitorUsageGranularity; monthdaystartat0: boolean=false): integer; {$ifdef HASINLINE}inline;{$endif} /// low-level modification of a time field stored in this ID, per granularity procedure SetTime(gran: TSynMonitorUsageGranularity; aValue: integer); end; TSynMonitorUsageTrackProp = record Info: PPropInfo; /// property type, as recognized by MonitorPropUsageValue() Kind: TSynMonitorType; Name: RawUTF8; Values: array[mugHour..mugYear] of TInt64DynArray; ValueLast: Int64; end; TSynMonitorUsageTrackPropDynArray = array of TSynMonitorUsageTrackProp; TSynMonitorUsageTrack = record Instance: TObject; Name: RawUTF8; Props: TSynMonitorUsageTrackPropDynArray; end; PSynMonitorUsageTrackProp = ^TSynMonitorUsageTrackProp; PSynMonitorUsageTrack = ^TSynMonitorUsageTrack; /// abstract class to track, compute and store TSynMonitor detailed statistics // - you should inherit from this class to implement proper data persistence, // e.g. using TSynMonitorUsageRest for ORM-based storage TSynMonitorUsage = class(TSynPersistentLock) protected fLog: TSynLogFamily; fTracked: array of TSynMonitorUsageTrack; fValues: array[mugHour..mugYear] of Variant; fCustomWritePropGranularity: TSynMonitorUsageGranularity; fLastInstance: TObject; fLastTrack: PSynMonitorUsageTrack; fPrevious: TTimeLogBits; fComment: RawUTF8; function TrackPropLock(Instance: TObject; Info: PPropInfo): PSynMonitorUsageTrackProp; // those methods will be protected (e.g. in Modified) by fSafe.Lock: procedure SavePrevious(Scope: TSynMonitorUsageGranularity); procedure Save(ID: TSynMonitorUsageID; Gran, Scope: TSynMonitorUsageGranularity); function Load(const Time: TTimeLogBits): boolean; procedure LoadTrack(var Track: TSynMonitorUsageTrack); // should be overriden with proper persistence storage: function SaveDB(ID: integer; const Track: variant; Gran: TSynMonitorUsageGranularity): boolean; virtual; abstract; function LoadDB(ID: integer; Gran: TSynMonitorUsageGranularity; out Track: variant): boolean; virtual; abstract; // may be overriden for testing purposes procedure SetCurrentUTCTime(out minutes: TTimeLogBits); virtual; public /// finalize the statistics, saving any pending information destructor Destroy; override; /// track the values of one named object instance // - will recognize the TSynMonitor* properties as TSynMonitorType from // RTTI, using MonitorPropUsageValue(), within any (nested) object // - the instance will be stored in fTracked[].Instance: ensure it will // stay available during the whole TSynMonitorUsage process function Track(Instance: TObject; const Name: RawUTF8=''): integer; overload; virtual; /// track the values of the given object instances // - will recognize the TSynMonitor* properties as TSynMonitorType from // RTTI, using MonitorPropUsageValue(), within any (nested) object // - instances will be stored in fTracked[].Instance: ensure they will // stay available during the whole TSynMonitorUsage process procedure Track(const Instances: array of TSynMonitor); overload; /// to be called when tracked properties changed on a tracked class instance function Modified(Instance: TObject): integer; overload; /// to be called when tracked properties changed on a tracked class instance function Modified(Instance: TObject; const PropNames: array of RawUTF8; ModificationTime: TTimeLog=0): integer; overload; virtual; /// some custom text, associated with the current stored state // - will be persistented by Save() methods property Comment: RawUTF8 read fComment write fComment; end; const USAGE_VALUE_LEN: array[mugHour..mugYear] of integer = (60,24,31,12); USAGE_ID_SHIFT: array[mugHour..mugYear] of byte = (0,5,10,14); USAGE_ID_BITS: array[mugHour..mugYear] of byte = (5,5,4,9); USAGE_ID_MASK: array[mugHour..mugYear] of integer = (31,31,15,511); USAGE_ID_MAX: array[mugHour..mugYear] of cardinal = (23,30,11,127); USAGE_ID_HOURMARKER: array[mugDay..mugYear] of integer = (29,30,31); USAGE_ID_YEAROFFSET = 2016; /// kind of "cumulative" TSynMonitorType stored in TSynMonitor / TSynMonitorUsage // - those properties will have their values reset for each granularity level // - will recognize TSynMonitorTotalMicroSec, TSynMonitorTotalBytes, // TSynMonitorOneBytes, TSynMonitorBytesPerSec, TSynMonitorCount and // TSynMonitorCount64 types SYNMONITORVALUE_CUMULATIVE = [smvMicroSec,smvBytes,smvCount,smvCount64]; function ToText(gran: TSynMonitorUsageGranularity): PShortString; overload; /// guess the kind of value stored in a TSynMonitor / TSynMonitorUsage property // - will recognize TSynMonitorTotalMicroSec, TSynMonitorOneMicroSec, // TSynMonitorTotalBytes, TSynMonitorOneBytes, TSynMonitorBytesPerSec, // TSynMonitorCount and TSynMonitorCount64 types from supplied RTTI function MonitorPropUsageValue(info: PPropInfo): TSynMonitorType; { ************ main ORM / SOA classes and types } const /// the used TAuthSession.IDCardinal value if the session not started yet // - i.e. if the session handling is still in its handshaking phase CONST_AUTHENTICATION_SESSION_NOT_STARTED = 0; /// the used TAuthSession.IDCardinal value if authentication mode is not set // - i.e. if TSQLRest.HandleAuthentication equals FALSE CONST_AUTHENTICATION_NOT_USED = 1; /// maximum handled dimension for TSQLRecordRTree // - this value is the one used by SQLite3 R-Tree virtual table RTREE_MAX_DIMENSION = 5; /// used as "stored AS_UNIQUE" published property definition in TSQLRecord AS_UNIQUE = false; /// custom contract value to ignore contract validation from client side // - you could set the aContractExpected parameter to this value for // TSQLRestClientURI.ServiceDefine or TSQLRestClientURI.ServiceRegister // so that the contract won't be checked with the server // - it will be used e.g. if the remote server is not a mORMot server, // but a plain REST/HTTP server - e.g. for public API notifications SERVICE_CONTRACT_NONE_EXPECTED = '*'; /// maximum number of methods handled by interfaces // - if you think this constant is too low, you are about to break // the "Interface Segregation" SOLID principle: so don't ask to increase // this value, we won't allow to write un-SOLID code! :) MAX_METHOD_COUNT = 128; /// maximum number of method arguments handled by interfaces // - if you consider this as a low value, you should better define some // records/classes as DTOs instead of multiplicating parameters: so don't // ask to increase this value, we rather encourage writing clean code // - used e.g. to avoid creating dynamic arrays if not needed, and // ease method calls MAX_METHOD_ARGS = 32; type TSQLTable = class; {$M+} { we need the RTTI information to be compiled for the published properties of these classes and their children (like TPersistent), to enable ORM - must be defined at the forward definition level } TSQLRecordProperties = class; TSQLModel = class; TSQLModelRecordProperties = class; TSQLRecord = class; // published properties = ORM fields/columns TSQLRecordMany = class; TSQLAuthUser = class; TSQLRest = class; TSQLRestClient = class; {.$METHODINFO ON} // this will include public methods as RESTful callbacks :( TSQLRestServer = class; {.$METHODINFO OFF} TSQLRestStorage = class; TSQLRestStorageRemote = class; TSQLRestClientURI = class; TInterfaceFactory = class; TSQLRestBatch = class; TSQLRestBatchLocked = class; {$M-} /// class-reference type (metaclass) of TSQLRecord TSQLRecordClass = class of TSQLRecord; PClass = ^TClass; PSQLRecordClass = ^TSQLRecordClass; /// a dynamic array storing TSQLRecord instances // - not used direcly, but as specialized T*ObjArray types TSQLRecordObjArray = array of TSQLRecord; /// a dynamic array used to store the TSQLRecord classes in a Database Model TSQLRecordClassDynArray = array of TSQLRecordClass; /// exception raised in case of incorrect TSQLTable.Step / Field*() use ESQLTableException = class(ESynException); /// generic parent class of all custom Exception types of this unit EORMException = class(ESynException); /// exception raised in case of TSQLRestBatch problem EORMBatchException = class(EORMException); /// exception raised in case of wrong Model definition EModelException = class(EORMException); /// exception raised in case of unexpected parsing error EParsingException = class(EORMException); /// exception raised in case of a Client-Server communication error ECommunicationException = class(EORMException); /// exception raised in case of an error in project implementation logic EBusinessLayerException = class(EORMException); /// exception raised in case of any authentication error ESecurityException = class(EORMException); /// exception dedicated to interface factory, e.g. services and mock/stubs EInterfaceFactoryException = class(ESynException); /// exception raised in case of Dependency Injection (aka IoC) issue EInterfaceResolverException = class(ESynException); /// exception dedicated to interface based service implementation EServiceException = class(EORMException); /// information about a TSQLRecord class property // - sftID for TSQLRecord properties, which are pointer(RecordID), not // any true class instance // - sftMany for TSQLRecordMany properties, for which no data is // stored in the table itself, but in a pivot table // - sftObject for e.g. TStrings TRawUTF8List TCollection instances TSQLPropInfoRTTIInstance = class(TSQLPropInfoRTTIPtrInt) protected fObjectClass: TClass; public /// will setup the corresponding ObjectClass property constructor Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType; aOptions: TSQLPropInfoListOptions); override; /// direct access to the property class instance function GetInstance(Instance: TObject): TObject; {$ifdef HASINLINE}inline;{$endif} /// direct access to the property class instance procedure SetInstance(Instance, Value: TObject); {$ifdef HASINLINE}inline;{$endif} /// direct access to the property class // - can be used e.g. for TSQLRecordMany properties property ObjectClass: TClass read fObjectClass; end; /// information about a TRecordReference/TRecordReferenceToBeDeleted // published property // - identified as a sftRecord kind of property TSQLPropInfoRTTIRecordReference = class(TSQLPropInfoRTTIInt64) protected fCascadeDelete: boolean; public /// will identify TRecordReferenceToBeDeleted kind of field, and // setup the corresponding CascadeDelete property constructor Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType; aOptions: TSQLPropInfoListOptions); override; /// TRUE if this sftRecord is a TRecordReferenceToBeDeleted property CascadeDelete: boolean read fCascadeDelete; end; /// information about a TID published property // - identified as a sftTID kind of property, optionally tied to a TSQLRecord // class, via its custom type name, e.g. // ! TSQLRecordClientID = type TID; -> TSQLRecordClient class TSQLPropInfoRTTITID = class(TSQLPropInfoRTTIRecordReference) protected fRecordClass: TSQLRecordClass; public /// will setup the corresponding RecordClass property from the TID type name // - the TSQLRecord type should have previously been registered to the // TJSONSerializer.RegisterClassForJSON list, e.g. in TSQLModel.Create, so // that e.g. 'TSQLRecordClientID' type name will match TSQLRecordClient // - in addition, the '...ToBeDeletedID' name pattern will set CascadeDelete constructor Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType; aOptions: TSQLPropInfoListOptions); override; /// the TSQLRecord class associated to this TID // - is computed from its type name - for instance, if you define: // ! type // ! TSQLRecordClientID = type TID; // ! TSQLOrder = class(TSQLRecord) // ! ... // ! published OrderedBy: TSQLRecordClientID read fOrderedBy write fOrderedBy; // ! ... // then this OrderedBy property will be tied to the TSQLRecordClient class // of the corresponding model, and the field value will be reset to 0 when // the targetting record is deleted (emulating a ON DELETE SET DEFAULT) // - equals TSQLRecord for plain TID field // - equals nil if T*ID type name doesn't match any registered class property RecordClass: TSQLRecordClass read fRecordClass; /// TRUE if this sftTID type name follows the '...ToBeDeletedID' pattern // - e.g. 'TSQLRecordClientToBeDeletedID' type name will match // TSQLRecordClient and set CascadeDelete // - is computed from its type name - for instance, if you define: // ! type // ! TSQLRecordClientToBeDeletedID = type TID; // ! TSQLOrder = class(TSQLRecord) // ! ... // ! published OrderedBy: TSQLRecordClientToBeDeletedID read fOrderedBy write fOrderedBy; // ! ... // then this OrderedBy property will be tied to the TSQLRecordClient class // of the corresponding model, and the whole record will be deleted when // the targetting record is deleted (emulating a ON DELETE CASCADE) property CascadeDelete: boolean read fCascadeDelete; end; /// information about a TRecordVersion published property // - identified as a sftRecordVersion kind of property, to track changes TSQLPropInfoRTTIRecordVersion = class(TSQLPropInfoRTTIInt64); /// information about a TSQLRecord class TSQLRecord property // - kind sftID, which are pointer(RecordID), not any true class instance // - will store the content just as an integer value // - will recognize any instance pre-allocated via Create*Joined() constructor TSQLPropInfoRTTIID = class(TSQLPropInfoRTTIInstance) public /// raise an exception if was created by Create*Joined() constructor procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override; /// this method will recognize if the TSQLRecord was allocated by // a Create*Joined() constructor: in this case, it will write the ID // of the nested property, and not the PtrInt() transtyped value procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override; end; TSQLPropInfoRTTIIDObjArray = array of TSQLPropInfoRTTIID; /// information about a TSQLRecord class TStrings/TRawUTF8List/TCollection // property // - kind sftObject e.g. for TStrings TRawUTF8List TCollection TObjectList instances // - binary serialization will store textual JSON serialization of the // object, including custom serialization TSQLPropInfoRTTIObject = class(TSQLPropInfoRTTIInstance) protected procedure CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); override; public procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override; procedure GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); override; procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override; function SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; override; function GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; override; procedure NormalizeValue(var Value: RawUTF8); override; procedure GetJSONValues(Instance: TObject; W: TJSONSerializer); override; end; /// information about a TSQLRecord class TSQLRecordMany property // - kind sftMany, for which no data is stored in the table itself, but in // a separated pivot table TSQLPropInfoRTTIMany = class(TSQLPropInfoRTTIInstance) public procedure SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); override; procedure GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); override; procedure GetBinary(Instance: TObject; W: TFileBufferWriter); override; function SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; override; end; TSQLPropInfoRTTIManyObjArray = array of TSQLPropInfoRTTIMany; /// the kind of SQlite3 (virtual) table // - TSQLRecordFTS3/4/5 will be associated with vFTS3/vFTS4/vFTS5 values, // TSQLRecordRTree/TSQLRecordRTreeInteger with rRTree/rRTreeInteger, any native // SQlite3 table as vSQLite3, and a TSQLRecordVirtualTable*ID as // rCustomForcedID/rCustomAutoID // - a plain TSQLRecord class can be defined as rCustomForcedID (e.g. for // TSQLRecordMany) after registration for an external DB via a call to // VirtualTableExternalRegister() from mORMotDB unit TSQLRecordVirtualKind = ( rSQLite3, rFTS3, rFTS4, rFTS5, rRTree, rRTreeInteger, rCustomForcedID, rCustomAutoID); /// kind of (static) database server implementation available // - sMainEngine will identify the default main SQlite3 engine // - sStaticDataTable will identify a TSQLRestStorageInMemory - i.e. // TSQLRestServer.fStaticData[] which can work without SQLite3 // - sVirtualTable will identify virtual TSQLRestStorage classes - i.e. // TSQLRestServer.fStaticVirtualTable[] which points to SQLite3 virtual tables // (e.g. TObjectList or external databases) TSQLRestServerKind = (sMainEngine, sStaticDataTable, sVirtualTable); /// pointer to the kind of (static) database server implementation PSQLRestServerKind = ^TSQLRestServerKind; /// some information about a given TSQLRecord class properties // - used internaly by TSQLRecord, via a global cache handled by this unit: // you can access to each record's properties via TSQLRecord.RecordProps class // - such a global cache saves some memory for each TSQLRecord instance, // and allows faster access to most wanted RTTI properties TSQLRecordProperties = class protected fTable: TSQLRecordClass; fClassType: PClassType; fClassProp: PClassProp; fHasNotSimpleFields: boolean; fHasTypeFields: TSQLFieldTypes; fFields: TSQLPropInfoList; fSimpleFields: TSQLPropInfoObjArray; fSQLTableName: RawUTF8; fCopiableFields: TSQLPropInfoObjArray; fManyFields: TSQLPropInfoRTTIManyObjArray; fJoinedFields: TSQLPropInfoRTTIIDObjArray; fJoinedFieldsTable: TSQLRecordClassDynArray; fDynArrayFields: TSQLPropInfoRTTIDynArrayObjArray; fDynArrayFieldsHasObjArray: boolean; fBlobCustomFields: TSQLPropInfoObjArray; fBlobFields: TSQLPropInfoRTTIObjArray; fFilters: TSynFilterOrValidateObjArrayArray; fRecordManySourceProp: TSQLPropInfoRTTIInstance; fRecordManyDestProp: TSQLPropInfoRTTIInstance; fSQLTableNameUpperWithDot: RawUTF8; fSQLFillPrepareMany: RawUTF8; fSQLTableSimpleFieldsNoRowID: RawUTF8; fSQLTableUpdateBlobFields: RawUTF8; fSQLTableRetrieveBlobFields: RawUTF8; fSQLTableRetrieveAllFields: RawUTF8; fRecordVersionField: TSQLPropInfoRTTIRecordVersion; fWeakZeroClass: TObject; /// the associated TSQLModel instances // - e.g. allow O(1) search of a TSQLRecordClass in a model fModel: array of record /// one associated model Model: TSQLModel; /// the index in the Model.Tables[] array TableIndex: PtrInt; /// associated ORM parameters Properties: TSQLModelRecordProperties; end; fLock: TRTLCriticalSection; fModelMax: integer; fCustomCollation: TRawUTF8DynArray; /// add an entry in fModel[] / fModelMax procedure InternalRegisterModel(aModel: TSQLModel; aTableIndex: integer; aProperties: TSQLModelRecordProperties); public /// initialize the properties content constructor Create(aTable: TSQLRecordClass); /// release associated used memory destructor Destroy; override; /// return TRUE if the given name is either ID/RowID, either a property name function IsFieldName(const PropName: RawUTF8): boolean; /// return TRUE if the given name is either ID/RowID, either a property name, // or an aggregate function (MAX/MIN/AVG/SUM) on a valid property name function IsFieldNameOrFunction(const PropName: RawUTF8): boolean; /// set all bits corresponding to the supplied field names // - returns TRUE on success, FALSE if any field name is not existing function FieldBitsFromRawUTF8(const aFields: array of RawUTF8; var Bits: TSQLFieldBits): boolean; overload; /// set all bits corresponding to the supplied field names // - returns the matching fields set function FieldBitsFromRawUTF8(const aFields: array of RawUTF8): TSQLFieldBits; overload; /// set all bits corresponding to the supplied CSV field names // - returns TRUE on success, FALSE if any field name is not existing function FieldBitsFromCSV(const aFieldsCSV: RawUTF8; var Bits: TSQLFieldBits): boolean; overload; /// set all bits corresponding to the supplied CSV field names, including ID // - returns TRUE on success, FALSE if any field name is not existing // - this overloaded method will identify ID/RowID field name, and set // withID output parameter according to its presence // - if aFieldsCSV='*', Bits will contain all simple fields, and withID=true function FieldBitsFromCSV(const aFieldsCSV: RawUTF8; var Bits: TSQLFieldBits; out withID: boolean): boolean; overload; /// set all bits corresponding to the supplied CSV field names // - returns the matching fields set function FieldBitsFromCSV(const aFieldsCSV: RawUTF8): TSQLFieldBits; overload; /// set all simple bits corresponding to the simple fields, excluding some // - could be a convenient alternative to FieldBitsFromCSV() if only some // fields are to be excluded // - returns the matching fields set function FieldBitsFromExcludingCSV(const aFieldsCSV: RawUTF8; aOccasion: TSQLOccasion=soSelect): TSQLFieldBits; /// set all bits corresponding to the supplied BLOB field type information // - returns TRUE on success, FALSE if blob field is not recognized function FieldBitsFromBlobField(aBlobField: PPropInfo; var Bits: TSQLFieldBits): boolean; /// compute the CSV field names text from a set of bits function CSVFromFieldBits(const Bits: TSQLFieldBits): RawUTF8; /// set all field indexes corresponding to the supplied field names // - returns TRUE on success, FALSE if any field name is not existing function FieldIndexDynArrayFromRawUTF8(const aFields: array of RawUTF8; var Indexes: TSQLFieldIndexDynArray): boolean; overload; /// set all field indexes corresponding to the supplied field names // - returns the matching fields set function FieldIndexDynArrayFromRawUTF8(const aFields: array of RawUTF8): TSQLFieldIndexDynArray; overload; /// set all field indexes corresponding to the supplied CSV field names // - returns TRUE on success, FALSE if any field name is not existing function FieldIndexDynArrayFromCSV(const aFieldsCSV: RawUTF8; var Indexes: TSQLFieldIndexDynArray): boolean; overload; /// set all field indexes corresponding to the supplied CSV field names // - returns the matching fields set function FieldIndexDynArrayFromCSV(const aFieldsCSV: RawUTF8): TSQLFieldIndexDynArray; overload; /// set all field indexes corresponding to the supplied BLOB field type information // - returns TRUE on success, FALSE if blob field is not recognized function FieldIndexDynArrayFromBlobField(aBlobField: PPropInfo; var Indexes: TSQLFieldIndexDynArray): boolean; /// retrieve a Field property RTTI information from a Property Name // - this version returns nil if the property is not a BLOB field function BlobFieldPropFromRawUTF8(const PropName: RawUTF8): PPropInfo; /// retrieve a Field property RTTI information from a Property Name // - this version returns nil if the property is not a BLOB field function BlobFieldPropFromUTF8(PropName: PUTF8Char; PropNameLen: integer): PPropInfo; /// append a field name to a RawUTF8 Text buffer // - if FieldIndex=VIRTUAL_TABLE_ROWID_COLUMN (-1), appends 'RowID' or // 'ID' (if ForceNoRowID=TRUE) to Text // - on error (i.e. if FieldIndex is out of range) will return TRUE // - otherwise, will return FALSE and append the field name to Text function AppendFieldName(FieldIndex: Integer; var Text: RawUTF8; ForceNoRowID: boolean): boolean; /// return the first unique property of kind RawUTF8 // - this property is mainly the "Name" property, i.e. the one with // "stored AS_UNIQUE" (i.e. "stored false") definition on most TSQLRecord // - if ReturnFirstIfNoUnique is TRUE and no unique property is found, // the first RawUTF8 property is returned anyway // - returns '' if no matching field was found function MainFieldName(ReturnFirstIfNoUnique: boolean=false): RawUTF8; /// return the SQLite3 field datatype for each specified field // - set to '' for fields with no column created in the database (e.g. sftMany) // - returns e.g. ' INTEGER, ' or ' TEXT COLLATE SYSTEMNOCASE, ' function SQLFieldTypeToSQL(FieldIndex: integer): RawUTF8; /// set a custom SQlite3 text column collation for a specified field // - can be used e.g. to override the default COLLATE SYSTEMNOCASE of RawUTF8 // - collations defined within our SynSQLite3 unit are named BINARY, NOCASE, // RTRIM and our custom SYSTEMNOCASE, ISO8601, WIN32CASE, WIN32NOCASE // - do nothing if FieldIndex is not valid, and returns false // - could be set in overridden class procedure TSQLRecord.InternalDefineModel // so that it will be common to all database models, for both client and server function SetCustomCollation(FieldIndex: integer; const aCollationName: RawUTF8): boolean; overload; /// set a custom SQlite3 text column collation for a specified field // - overloaded method which expects the field to be named function SetCustomCollation(const aFieldName, aCollationName: RawUTF8): boolean; overload; /// set a custom SQlite3 text column collation for a given field type // - can be used e.g. to override ALL default COLLATE SYSTEMNOCASE of RawUTF8, // or the default COLLATE ISO8601 of TDateTime, and let the generated SQLite3 // file be available outside the scope of mORMot's SQLite3 engine // - collations defined within our SynSQLite3 unit are named BINARY, NOCASE, // RTRIM and our custom SYSTEMNOCASE, ISO8601, WIN32CASE, WIN32NOCASE // - could be set in overridden class procedure TSQLRecord.InternalDefineModel // so that it will be common to all database models, for both client and server // - note that you may inherit from TSQLRecordNoCase to use the NOCASE // standard SQLite3 collation for all descendant ORM objects procedure SetCustomCollationForAll(aFieldType: TSQLFieldType; const aCollationName: RawUTF8); /// allow to validate length of all text published properties of this table // - the "index" attribute of the RawUTF8/string published properties could // be used to specify a maximum length for external VARCHAR() columns // - SQLite3 will just ignore this "index" information, but it could be // handy to be able to validate the value length before sending to the DB // - this method will create TSynValidateText corresponding to the maximum // field size specified by the "index" attribute, to validate before write // - will expect the "index" value to be in UTF-16 codepoints, unless // IndexIsUTF8Length is set to TRUE, indicating UTF-8 length in "index" procedure SetMaxLengthValidatorForTextFields(IndexIsUTF8Length: boolean=false); /// allow to filter the length of all text published properties of this table // - the "index" attribute of the RawUTF8/string published properties could // be used to specify a maximum length for external VARCHAR() columns // - SQLite3 will just ignore this "index" information, but it could be // handy to be able to filter the value length before sending to the DB // - this method will create TSynFilterTruncate corresponding to the maximum // field size specified by the "index" attribute, to filter before write // - will expect the "index" value to be in UTF-16 codepoints, unless // IndexIsUTF8Length is set to TRUE, indicating UTF-8 length in "index" procedure SetMaxLengthFilterForTextFields(IndexIsUTF8Length: boolean=false); {$ifndef NOVARIANTS} /// customize the TDocVariant options for all variant published properties // - will change the TSQLPropInfoRTTIVariant.DocVariantOptions value // - use e.g. as SetVariantFieldDocVariantOptions(JSON_OPTIONS_FAST_EXTENDED) // - see also TSQLRecordNoCaseExtended root class procedure SetVariantFieldsDocVariantOptions(const Options: TDocVariantOptions); {$endif} /// return the UTF-8 encoded SQL statement source to alter the table for // adding the specified field function SQLAddField(FieldIndex: integer): RawUTF8; /// create a TJSONWriter, ready to be filled with TSQLRecord.GetJSONValues // - you can use TSQLRecordProperties.FieldBitsFromCSV() or // TSQLRecordProperties.FieldBitsFromRawUTF8() to compute aFields function CreateJSONWriter(JSON: TStream; Expand, withID: boolean; const aFields: TSQLFieldBits; KnownRowsCount: integer; aBufSize: integer=8192): TJSONSerializer; overload; /// create a TJSONWriter, ready to be filled with TSQLRecord.GetJSONValues(W) // - you can use TSQLRecordProperties.FieldBitsFromCSV() or // TSQLRecordProperties.FieldBitsFromRawUTF8() to compute aFields function CreateJSONWriter(JSON: TStream; Expand, withID: boolean; const aFields: TSQLFieldIndexDynArray; KnownRowsCount: integer; aBufSize: integer=8192): TJSONSerializer; overload; /// create a TJSONWriter, ready to be filled with TSQLRecord.GetJSONValues(W) // - this overloaded method will call FieldBitsFromCSV(aFieldsCSV,bits,withID) // to retrieve the bits just like a SELECT (i.e. '*' for simple fields) function CreateJSONWriter(JSON: TStream; Expand: boolean; const aFieldsCSV: RawUTF8; KnownRowsCount: integer; aBufSize: integer=8192): TJSONSerializer; overload; /// set the W.ColNames[] array content + W.AddColumns procedure SetJSONWriterColumnNames(W: TJSONSerializer; KnownRowsCount: integer); /// save the TSQLRecord RTTI into a binary header // - used e.g. by TSQLRestStorageInMemory.SaveToBinary() procedure SaveBinaryHeader(W: TFileBufferWriter); /// ensure that the TSQLRecord RTTI matches the supplied binary header // - used e.g. by TSQLRestStorageInMemory.LoadFromBinary() function CheckBinaryHeader(var R: TFileBufferReader): boolean; /// convert a JSON array of simple field values into a matching JSON object function SaveSimpleFieldsFromJsonArray(var P: PUTF8Char; var EndOfObject: AnsiChar; ExtendedJSON: boolean): RawUTF8; /// register a custom filter (transformation) or validation rule to // the TSQMRecord class for a specified field // - this will be used by TSQLRecord.Filter and TSQLRecord.Validate // methods (in default implementation) // - will return FALSE in case of an invalid field index function AddFilterOrValidate(aFieldIndex: integer; aFilter: TSynFilterOrValidate): boolean; overload; /// register a custom filter (transformation) or validatation to the // TSQLRecord class for a specified field // - this will be used by TSQLRecord.Filter and TSQLRecord.Validate // methods (in default implementation) // - will raise an EModelException if the field name does not exist procedure AddFilterOrValidate(const aFieldName: RawUTF8; aFilter: TSynFilterOrValidate); overload; /// add a custom unmanaged fixed-size record property // - simple kind of records (i.e. those not containing reference-counted // members) do not have RTTI generated, at least in older versions of Delphi // - use this method within TSQLRecord.InternalRegisterCustomProperties // overridden method to define a custom record property with no // reference-counted types within (like strings) - typical use may be TGUID // - main parameters are the record size, in bytes, and the property pointer // - add an TSQLPropInfoRecordFixedSize instance to the internal list // - if aData2Text/aText2Data parameters are not defined, it will fallback // to TSQLPropInfo.BinaryToText() simple text Base64 encoding // - can be used to override the default TSQLRecord corresponding method: // !class procedure TSQLMyRecord.InternalRegisterCustomProperties( // ! Props: TSQLRecordProperties); // !begin // ! Props.RegisterCustomFixedSizeRecordProperty(self,SizeOf(TMyRec),'RecField', // ! @TSQLMyRecord(nil).fRecField, [], SizeOf(TMyRec)); // !end; procedure RegisterCustomFixedSizeRecordProperty(aTable: TClass; aRecordSize: cardinal; const aName: RawUTF8; aPropertyPointer: pointer; aAttributes: TSQLPropInfoAttributes; aFieldWidth: integer; aData2Text: TOnSQLPropInfoRecord2Text=nil; aText2Data: TOnSQLPropInfoRecord2Data=nil); /// add a custom record property from its RTTI definition // - handle any kind of record with TypeInfo() generated // - use this method within InternalRegisterCustomProperties overridden method // to define a custom record property containing reference-counted types // - main parameters are the record RTTI information, and the property pointer // - add an TSQLPropInfoRecordRTTI instance to the internal list // - can be used as such: // !class procedure TSQLMyRecord.InternalRegisterCustomProperties( // ! Props: TSQLRecordProperties); // !begin // ! Props.RegisterCustomRTTIRecordProperty(self,TypeInfo(TMyRec),'RecField', // ! @TSQLMyRecord(nil).fRecField); // !end; procedure RegisterCustomRTTIRecordProperty(aTable: TClass; aRecordInfo: PTypeInfo; const aName: RawUTF8; aPropertyPointer: pointer; aAttributes: TSQLPropInfoAttributes=[]; aFieldWidth: integer=0; aData2Text: TOnSQLPropInfoRecord2Text=nil; aText2Data: TOnSQLPropInfoRecord2Data=nil); /// add a custom property from its RTTI definition stored as JSON // - handle any kind of record with TypeInfo() generated // - use this method within InternalRegisterCustomProperties overridden method // to define a custom record property containing reference-counted types // - main parameters are the record RTTI information, and the property pointer // - add an TSQLPropInfoCustomJSON instance to the internal list // - can be used as such: // !class procedure TSQLMyRecord.InternalRegisterCustomProperties( // ! Props: TSQLRecordProperties); // !begin // ! Props.RegisterCustomPropertyFromRTTI(self,TypeInfo(TMyRec),'RecField', // ! @TSQLMyRecord(nil).fRecField); // !end; procedure RegisterCustomPropertyFromRTTI(aTable: TClass; aTypeInfo: PTypeInfo; const aName: RawUTF8; aPropertyPointer: pointer; aAttributes: TSQLPropInfoAttributes=[]; aFieldWidth: integer=0); /// add a custom property from its type name, stored as JSON // - handle any kind of registered record, including TGUID // - use this method within InternalRegisterCustomProperties overridden method // to define a custom record property containing reference-counted types // - main parameters are the record RTTI information, and the property pointer // - add an TSQLPropInfoCustomJSON instance to the internal list // - can be used as such: // !class procedure TSQLMyRecord.InternalRegisterCustomProperties( // ! Props: TSQLRecordProperties); // !begin // ! Props.RegisterCustomPropertyFromTypeName(self,'TGUID','GUID', // ! @TSQLMyRecord(nil).fGUID,[aIsUnique],38); // !end; procedure RegisterCustomPropertyFromTypeName(aTable: TClass; const aTypeName, aName: RawUTF8; aPropertyPointer: pointer; aAttributes: TSQLPropInfoAttributes=[]; aFieldWidth: integer=0); /// fast access to the RTTI properties attribute property TableClassType: PClassType read fClassType; /// fast access to the RTTI properties attribute property TableClassProp: PClassProp read fClassProp; /// if this class has any BLOB or TSQLRecodMany fields // - i.e. some fields to be ignored property HasNotSimpleFields: boolean read fHasNotSimpleFields; /// set of field types appearing in this record property HasTypeFields: TSQLFieldTypes read fHasTypeFields; /// list all fields, as retrieved from RTTI property Fields: TSQLPropInfoList read fFields; /// list all "simple" fields of this TSQLRecord // - by default, the TSQLRawBlob and TSQLRecordMany fields are not included // into this set: they must be read specificaly (in order to spare // bandwidth for BLOBs) // - dynamic arrays belong to simple fields: they are sent with other // properties content // - match inverted NOT_SIMPLE_FIELDS mask property SimpleFields: TSQLPropInfoObjArray read fSimpleFields; /// list all fields which can be copied from one TSQLRecord instance to another // - match COPIABLE_FIELDS mask, i.e. all fields except sftMany property CopiableFields: TSQLPropInfoObjArray read fCopiableFields; /// list all TSQLRecordMany fields of this TSQLRecord property ManyFields: TSQLPropInfoRTTIManyObjArray read fManyFields; /// list all TSQLRecord fields of this TSQLRecord // - ready to be used by TSQLTableJSON.CreateFromTables() // - i.e. the class itself then, all fields of type sftID (excluding sftMany) property JoinedFields: TSQLPropInfoRTTIIDObjArray read fJoinedFields; /// wrapper of all nested TSQLRecord class of this TSQLRecord // - ready to be used by TSQLTableJSON.CreateFromTables() // - i.e. the class itself as JoinedFieldsTable[0], then, all nested // TSQLRecord published properties (of type sftID, ergo excluding sftMany) // - equals nil if there is no nested TSQLRecord property (i.e. JoinedFields=nil) property JoinedFieldsTable: TSQLRecordClassDynArray read fJoinedFieldsTable; /// list of all sftBlobDynArray fields of this TSQLRecord property DynArrayFields: TSQLPropInfoRTTIDynArrayObjArray read fDynArrayFields; /// TRUE if any of the sftBlobDynArray fields of this TSQLRecord is a T*ObjArray // - used e.g. by TSQLRecord.Destroy to release all owned nested instances property DynArrayFieldsHasObjArray: boolean read fDynArrayFieldsHasObjArray; /// list of all sftBlobCustom fields of this TSQLRecord // - have been defined e.g. as TSQLPropInfoCustom custom definition property BlobCustomFields: TSQLPropInfoObjArray read fBlobCustomFields; /// list all BLOB fields of this TSQLRecord // - i.e. generic sftBlob fields (not sftBlobDynArray, sftBlobCustom nor // sftBlobRecord) property BlobFields: TSQLPropInfoRTTIObjArray read fBlobFields; /// all TSynFilter or TSynValidate instances registered per each field // - since validation and filtering are used within some CPU-consuming // part of the framework (like UI edition), both filters and validation // rules are grouped in the same list - for TSynTableFieldProperties there // are separated Filters[] and Validates[] arrays, for better performance property Filters: TSynFilterOrValidateObjArrayArray read fFilters; /// for a TSQLRecordMany class, points to the Source property RTTI property RecordManySourceProp: TSQLPropInfoRTTIInstance read fRecordManySourceProp; /// for a TSQLRecordMany class, points to the Dest property RTTI property RecordManyDestProp: TSQLPropInfoRTTIInstance read fRecordManyDestProp; /// points to any TRecordVersion field // - contains nil if no such sftRecordVersion field do exist // - will be used by low-level storage engine to compute and store the // monotonic version number during any write operation property RecordVersionField: TSQLPropInfoRTTIRecordVersion read fRecordVersionField; /// the Table name in the database in uppercase with a final '.' // - e.g. 'TEST.' for TSQLRecordTest class // - can be used with IdemPChar() for fast check of a table name property SQLTableNameUpperWithDot: RawUTF8 read fSQLTableNameUpperWithDot; /// returns 'COL1,COL2' with all COL* set to simple field names // - same value as SQLTableSimpleFields[false,false] // - this won't change depending on the ORM settings: so it can be safely // computed here and not in TSQLModelRecordProperties // - used e.g. by TSQLRecord.GetSQLValues property SQLTableSimpleFieldsNoRowID: RawUTF8 read fSQLTableSimpleFieldsNoRowID; /// returns 'COL1=?,COL2=?' with all BLOB columns names // - used e.g. by TSQLRestServerDB.UpdateBlobFields() property SQLTableUpdateBlobFields: RawUTF8 read fSQLTableUpdateBlobFields; /// returns 'COL1,COL2' with all BLOB columns names // - used e.g. by TSQLRestServerDB.RetrieveBlobFields() property SQLTableRetrieveBlobFields: RawUTF8 read fSQLTableRetrieveBlobFields; public /// bit set to 1 for indicating each TSQLFieldType fields of this TSQLRecord FieldBits: array[TSQLFieldType] of TSQLFieldBits; /// bit set to 1 for indicating TModTime/TSessionUserID fields // of this TSQLRecord (leaving TCreateTime untouched) // - as applied before an UPDATE // - i.e. sftModTime and sftSessionUserID fields ComputeBeforeUpdateFieldsBits: TSQLFieldBits; /// bit set to 1 for indicating TModTime/TCreateTime/TSessionUserID fields // of this TSQLRecord // - as applied before an INSERT // - i.e. sftModTime, sftCreateTime and sftSessionUserID fields ComputeBeforeAddFieldsBits: TSQLFieldBits; /// bit set to 1 for indicating fields to export, i.e. "simple" fields // - this array will handle special cases, like the TCreateTime fields // which shall not be included in soUpdate but soInsert and soSelect e.g. SimpleFieldsBits: array[TSQLOccasion] of TSQLFieldBits; /// number of fields to export, i.e. "simple" fields // - this array will handle special cases, like the TCreateTime fields // which shall not be included in soUpdate but soInsert and soSelect e.g. SimpleFieldsCount: array[TSQLOccasion] of integer; /// bit set to 1 for an unique field // - an unique field is defined as "stored AS_UNIQUE" (i.e. "stored false") // in its property definition IsUniqueFieldsBits: TSQLFieldBits; /// bit set to 1 for the smallest simple fields // - i.e. excluding non only sftBlob and sftMany, but also sftVariant, // sftBlobDynArray, sftBlobCustom and sftUTF8Custom fields // - may be used to minimize the transmitted content, e.g. when serializing // to JSON for the most SmallFieldsBits: TSQLFieldBits; /// bit set to 1 for the all fields storing some data // - match COPIABLE_FIELDS mask, i.e. all fields except sftMany CopiableFieldsBits: TSQLFieldBits; /// contains the main field index (e.g. mostly 'Name') // - the [boolean] is for [ReturnFirstIfNoUnique] version // - contains -1 if no field matches MainField: array[boolean] of integer; /// count of coordinate fields of a TSQLRecordRTree, before auxiliary columns RTreeCoordBoundaryFields: integer; published /// the TSQLRecord class property Table: TSQLRecordClass read fTable; /// the Table name in the database, associated with this TSQLRecord class // - 'TSQL' or 'TSQLRecord' chars are trimmed at the beginning of the ClassName // - or the ClassName is returned as is, if no 'TSQL' or 'TSQLRecord' at first property SQLTableName: RawUTF8 read fSQLTableName; /// returns 'COL1,COL2' with all COL* set to all field names, including // RowID, TRecordVersion and BLOBs // - this won't change depending on the ORM settings: so it can be safely // computed here and not in TSQLModelRecordProperties // - used e.g. by TSQLRest.InternalListJSON() property SQLTableRetrieveAllFields: RawUTF8 read fSQLTableRetrieveAllFields; end; TServiceFactoryServer = class; PSQLAccessRights = ^TSQLAccessRights; /// flags which may be set by the caller to notify low-level context // - llfHttps will indicates that the communication was made over HTTPS // - llfSecured is set if the transmission is encrypted or in-process, // using e.g. HTTPS/SSL/TLS or our proprietary AES/ECDHE algorithms // - llfWebsockets communication was made using WebSockets TSQLRestURIParamsLowLevelFlag = (llfHttps, llfSecured, llfWebsockets); /// some flags set by the caller to notify low-level context TSQLRestURIParamsLowLevelFlags = set of TSQLRestURIParamsLowLevelFlag; /// store all parameters for a Client or Server method call // - as used by TSQLRestServer.URI or TSQLRestClientURI.InternalURI {$ifdef USERECORDWITHMETHODS}TSQLRestURIParams = record {$else}TSQLRestURIParams = object{$endif} public /// input parameter containing the caller URI Url: RawUTF8; /// input parameter containing the caller method // - handle enhanced REST codes: LOCK/UNLOCK/BEGIN/END/ABORT Method: RawUTF8; /// input parameter containing the caller message headers // - you can use e.g. to retrieve the remote IP: // ! Call.Header(HEADER_REMOTEIP_UPPER) // ! or FindNameValue(Call.InHead,HEADER_REMOTEIP_UPPER) // but consider rather using TSQLRestServerURIContext.RemoteIP InHead: RawUTF8; /// input parameter containing the caller message body // - e.g. some GET/POST/PUT JSON data can be specified here InBody: RawUTF8; /// output parameter to be set to the response message header // - it is the right place to set the returned message body content type, // e.g. TEXT_CONTENT_TYPE_HEADER or HTTP_CONTENT_TYPE_HEADER: if not set, // the default JSON_CONTENT_TYPE_HEADER will be returned to the client, // meaning that the message is JSON // - you can use OutBodyType() function to retrieve the stored content-type OutHead: RawUTF8; /// output parameter to be set to the response message body OutBody: RawUTF8; /// output parameter to be set to the HTTP status integer code // - HTTP_NOTFOUND=404 e.g. if the url doesn't start with Model.Root (caller // can try another TSQLRestServer) OutStatus: cardinal; /// output parameter to be set to the database internal state OutInternalState: cardinal; /// associated RESTful access rights // - AccessRights must be handled by the TSQLRestServer child, according // to the Application Security Policy (user logging, authentification and // rights management) - making access rights a parameter allows this method // to be handled as pure stateless, thread-safe and session-free RestAccessRights: PSQLAccessRights; /// opaque reference to the protocol context which made this request // - may point e.g. to a THttpServerResp, a TWebSocketServerResp, // a THttpApiServer, a TSQLRestClientURI, a TFastCGIServer or a // TSQLRestServerNamedPipeResponse instance // - stores SynCrtSock's THttpServerConnectionID, i.e. a Int64 as expected // by http.sys, or an incremental rolling sequence of 31-bit integers for // THttpServer/TWebSocketServer, or maybe a raw PtrInt(self/THandle) LowLevelConnectionID: Int64; /// low-level properties of the current protocol context LowLevelFlags: TSQLRestURIParamsLowLevelFlags; /// initialize the non RawUTF8 values procedure Init; overload; /// initialize the input values procedure Init(const aURI,aMethod,aInHead,aInBody: RawUTF8); overload; /// retrieve the "Content-Type" value from InHead // - if GuessJSONIfNoneSet is TRUE, returns JSON if none was set in headers function InBodyType(GuessJSONIfNoneSet: boolean=True): RawUTF8; /// check if the "Content-Type" value from InHead is JSON // - if GuessJSONIfNoneSet is TRUE, assume JSON is used function InBodyTypeIsJson(GuessJSONIfNoneSet: boolean=True): boolean; /// retrieve the "Content-Type" value from OutHead // - if GuessJSONIfNoneSet is TRUE, returns JSON if none was set in headers function OutBodyType(GuessJSONIfNoneSet: boolean=True): RawUTF8; /// check if the "Content-Type" value from OutHead is JSON // - if GuessJSONIfNoneSet is TRUE, assume JSON is used function OutBodyTypeIsJson(GuessJSONIfNoneSet: boolean=True): boolean; /// just a wrapper around FindNameValue(InHead,UpperName) // - use e.g. as // ! Call.Header(HEADER_REMOTEIP_UPPER) or Call.Header(HEADER_BEARER_UPPER) // - consider rather using TSQLRestServerURIContext.InHeader[] or even // dedicated TSQLRestServerURIContext.RemoteIP/AuthenticationBearerToken function Header(UpperName: PAnsiChar): RawUTF8; {$ifdef HASINLINE}inline;{$endif} /// wrap FindNameValue(InHead,UpperName) with a cache store function HeaderOnce(var Store: RawUTF8; UpperName: PAnsiChar): RawUTF8; end; /// used to map set of parameters for a Client or Server method call PSQLRestURIParams = ^TSQLRestURIParams; /// callback event signature before/after a Client or Server method call // - to allow low-level interception of the request bodies e.g. for low-level // logging/audit, or on-the-fly encryption and/or signature of the content // - used by TSQLRest.OnDecryptBody and TSQLRest.OnEncryptBody - so the very // same callbacks may be used on both client and server sides // - for server-only process (e.g. to check for authorization), see rather // TSQLRestServer.OnBeforeURI and TSQLRestServer.OnAfterURI events // - used e.g. by TSQLRest.SetCustomEncryption method TNotifyRestBody = procedure(Sender: TSQLRest; var Body,Head,URL: RawUTF8) of object; /// points to the currently running service on the server side // - your code may use such a local pointer to retrieve the ServiceContext // threadvar once in a method, since threadvar access does cost some CPU // !var context: PServiceRunningContext; // !begin // ! context := @ServiceContext; // threadvar access once // ! ... PServiceRunningContext = ^TServiceRunningContext; TSQLRestServerURIContext = class; TAuthSession = class; /// used to identify the authentication failure reason // - as transmitted e.g. by TSQLRestServerURIContext.AuthenticationFailed or // TSQLRestServer.OnAuthenticationFailed TNotifyAuthenticationFailedReason = ( afInvalidSignature,afRemoteServiceExecutionNotAllowed, afUnknownUser,afInvalidPassword, afSessionAlreadyStartedForThisUser,afSessionCreationAborted, afSecureConnectionRequired, afJWTRequired); /// will identify the currently running service on the server side // - is the type of the global ServiceContext threadvar // - to access the current TSQLRestServer instance (and e.g. its ORM/CRUD // or SOA methods), use Request.Server and not Factory.Server, which may not // be available e.g. if you run the service from the server side (so no // factory is involved) // - note that the safest (and slightly faster) access to the TSQLRestServer // instance associated with a service is to inherit your implementation // class from TInjectableObjectRest TServiceRunningContext = record /// the currently running service factory // - it can be used within server-side implementation to retrieve the // associated TSQLRestServer instance // - note that TServiceFactoryServer.Get() won't override this value, when // called within another service (i.e. if Factory is not nil) Factory: TServiceFactoryServer; /// the currently runnning context which launched the method // - low-level RESTful context is also available in its Call member // - Request.Server is the safe access point to the underlying TSQLRestServer, // unless the service is implemented via TInjectableObjectRest, so the // TInjectableObjectRest.Server property is preferred // - make available e.g. current session or authentication parameters // (including e.g. user details via Request.Server.SessionGetUser) Request: TSQLRestServerURIContext; /// the thread which launched the request // - is set by TSQLRestServer.BeginCurrentThread from multi-thread server // handlers - e.g. TSQLite3HttpServer or TSQLRestServerNamedPipeResponse RunningThread: TThread; end; /// possible service provider method options, e.g. about logging or execution // - see TServiceMethodOptions for a description of each available option TServiceMethodOption = ( optExecLockedPerInterface, optExecInPerInterfaceThread, optFreeInPerInterfaceThread, {$ifndef LVCL} optExecInMainThread, optFreeInMainThread, optVariantCopiedByReference, optInterceptInputOutput, {$endif} optNoLogInput, optNoLogOutput, optErrorOnMissingParam, optForceStandardJSON, optDontStoreVoidJSON, optIgnoreException); /// how TServiceFactoryServer.SetOptions() will set the options value TServiceMethodOptionsAction = (moaReplace, moaInclude, moaExclude); /// set of per-method execution options for an interface-based service provider // - by default, mehthod executions are concurrent, for better server // responsiveness; if you set optExecLockedPerInterface, all methods of // a given interface will be executed with a critical section // - optExecInMainThread will force the method to be called within // a RunningThread.Synchronize() call - it can be used e.g. if your // implementation rely heavily on COM servers - by default, service methods // are called within the thread which received them, on multi-thread server // instances (e.g. TSQLite3HttpServer or TSQLRestServerNamedPipeResponse), // for better response time and CPU use (this is the technical reason why // service implementation methods have to handle multi-threading safety // carefully, e.g. by using TRTLCriticalSection mutex on purpose) // - optFreeInMainThread will force the _Release/Destroy method to be run // in the main thread: setting this option for any method will affect the // whole service class - is not set by default, for performance reasons // - optExecInPerInterfaceThread and optFreeInPerInterfaceThread will allow // creation of a per-interface dedicated thread // - if optInterceptInputOutput is set, TServiceFactoryServer.AddInterceptor() // events will have their Sender.Input/Output values defined // - if optNoLogInput/optNoLogOutput is set, TSynLog and ServiceLog() database // won't log any parameter values at input/output - this may be useful for // regulatory/safety purposes, e.g. to ensure that no sensitive information // (like a credit card number or a password), is logged during process - // consider using TInterfaceFactory.RegisterUnsafeSPIType() instead if you // prefer a more tuned filtering, for specific high-level types // - when parameters are transmitted as JSON object, any missing parameter // will be replaced by their default value, unless optErrorOnMissingParam // is defined to reject the call // - by default, it wil check for the client user agent, and use extended // JSON if none is found (e.g. from WebSockets), or if it contains 'mORMot': // you can set optForceStandardJSON to ensure standard JSON is always returned // - optDontStoreVoidJSON will reduce the JSON object verbosity by not writing // void (e.g. 0 or '') properties when serializing objects and records // - any exceptions will be propagated during execution, unless // optIgnoreException is set and the exception is trapped (not to be used // unless you know what you are doing) TServiceMethodOptions = set of TServiceMethodOption; /// class-reference type (metaclass) for storing interface-based service // execution statistics // - you could inherit from TSQLRecordServiceLog, and specify additional // fields corresponding to the execution context TSQLRecordServiceLogClass = class of TSQLRecordServiceLog; /// class-reference type (metaclass) for storing interface-based service // execution statistics used for DB-based asynchronous notifications // - as used by TServiceFactoryClient.SendNotifications TSQLRecordServiceNotificationsClass = class of TSQLRecordServiceNotifications; /// internal per-method list of execution context as hold in TServiceFactory TServiceFactoryExecution = record /// the list of denied TSQLAuthGroup ID(s) // - used on server side within TSQLRestServerURIContext.ExecuteSOAByInterface // - bit 0 for client TSQLAuthGroup.ID=1 and so on... // - is therefore able to store IDs up to 256 // - void by default, i.e. no denial = all groups allowed for this method Denied: set of 0..255; /// execution options for this method (about thread safety or logging) Options: TServiceMethodOptions; /// where execution information should be written as TSQLRecordServiceLog LogRest: TSQLRest; /// the TSQLRecordServiceLog class to use, as defined in LogRest.Model LogClass: TSQLRecordServiceLogClass; end; /// points to the execution context of one method within TServiceFactory PServiceFactoryExecution = ^TServiceFactoryExecution; /// all commands which may be executed by TSQLRestServer.URI() method // - execSOAByMethod for method-based services // - execSOAByInterface for interface-based services // - execORMGet for ORM reads i.e. Retrieve*() methods // - execORMWrite for ORM writes i.e. Add Update Delete TransactionBegin // Commit Rollback methods TSQLRestServerURIContextCommand = ( execNone, execSOAByMethod, execSOAByInterface, execORMGet, execORMWrite); /// class used to define the Client-Server expected routing // - most of the internal methods are declared as virtual, so it allows any // kind of custom routing or execution scheme // - TSQLRestRoutingREST and TSQLRestRoutingJSON_RPC classes // are provided in this unit, to allow RESTful and JSON/RPC protocols TSQLRestServerURIContextClass = class of TSQLRestServerURIContext; /// a set of potential actions to be executed from the server // - reSQL will indicate the right to execute any POST SQL statement (not only // SELECT statements) // - reSQLSelectWithoutTable will allow executing a SELECT statement with // arbitrary content via GET/LOCK (simple SELECT .. FROM aTable will be checked // against TSQLAccessRights.GET[] per-table right // - reService will indicate the right to execute the interface-based JSON-RPC // service implementation // - reUrlEncodedSQL will indicate the right to execute a SQL query encoded // at the URI level, for a GET (to be used e.g. with XMLHTTPRequest, which // forced SentData='' by definition), encoded as sql=.... inline parameter // - reUrlEncodedDelete will indicate the right to delete items using a // WHERE clause for DELETE verb at /root/TableName?WhereClause // - reOneSessionPerUser will force that only one session may be created // for one user, even if connection comes from the same IP: in this case, // you may have to set the SessionTimeOut to a small value, in case the // session is not closed gracefully // - by default, read/write access to the TSQLAuthUser table is disallowed, // for obvious security reasons: but you can define reUserCanChangeOwnPassword // so that the current logged user will be able to change its own password // - order of this set does matter, since it will be stored as a byte value // e.g. by TSQLAccessRights.ToString: ensure that new items will always be // appended to the list, not inserted within TSQLAllowRemoteExecute = set of ( reSQL, reService, reUrlEncodedSQL, reUrlEncodedDelete, reOneSessionPerUser, reSQLSelectWithoutTable, reUserCanChangeOwnPassword); /// set the User Access Rights, for each Table // - one property for every and each URI method (GET/POST/PUT/DELETE) // - one bit for every and each Table in Model.Tables[] {$ifdef USERECORDWITHMETHODS}TSQLAccessRights = record {$else}TSQLAccessRights = object{$endif} public /// set of allowed actions on the server side AllowRemoteExecute: TSQLAllowRemoteExecute; /// GET method (retrieve record) table access bits // - note that a GET request with a SQL statement without a table (i.e. // on 'ModelRoot' URI with a SQL statement as SentData, as used in // TSQLRestClientURI.UpdateFromServer) will be checked for simple cases // (i.e. the first table in the FROM clause), otherwise will follow , whatever the bits // here are: since TSQLRestClientURI.UpdateFromServer() is called only // for refreshing a direct statement, it will be OK; you can improve this // by overriding the TSQLRestServer.URI() method // - if the REST request is LOCK, the PUT access bits will be read instead // of the GET bits value GET: TSQLFieldTables; /// POST method (create record) table access bits POST: TSQLFieldTables; /// PUT method (update record) table access bits // - if the REST request is LOCK, the PUT access bits will be read instead // of the GET bits value PUT: TSQLFieldTables; /// DELETE method (delete record) table access bits DELETE: TSQLFieldTables; /// wrapper method which can be used to set the CRUD abilities over a table // - C=Create, R=Read, U=Update, D=Delete rights procedure Edit(aTableIndex: integer; C, R, U, D: Boolean); overload; /// wrapper method which can be used to set the CRUD abilities over a table // - use TSQLOccasion set as parameter procedure Edit(aTableIndex: integer; aRights: TSQLOccasions); overload; /// wrapper method which can be used to set the CRUD abilities over a table // - will raise an EModelException if the supplied table is incorrect // - C=Create, R=Read, U=Update, D=Delete rights procedure Edit(aModel: TSQLModel; aTable: TSQLRecordClass; C, R, U, D: Boolean); overload; /// wrapper method which can be used to set the CRUD abilities over a table // - will raise an EModelException if the supplied table is incorrect // - use TSQLOccasion set as parameter procedure Edit(aModel: TSQLModel; aTable: TSQLRecordClass; aRights: TSQLOccasions); overload; /// serialize the content as TEXT // - use the TSQLAuthGroup.AccessRights CSV format function ToString: RawUTF8; /// unserialize the content from TEXT // - use the TSQLAuthGroup.AccessRights CSV format procedure FromString(P: PUTF8Char); /// validate mPost/mPut/mDelete action against those access rights // - used by TSQLRestServerURIContext.ExecuteORMWrite and // TSQLRestServer.EngineBatchSend methods for proper security checks function CanExecuteORMWrite(Method: TSQLURIMethod; Table: TSQLRecordClass; TableIndex: integer; const TableID: TID; Context: TSQLRestServerURIContext): boolean; end; /// used by TSQLRestServerURIContext.ClientKind to identify the currently // connected client TSQLRestServerURIContextClientKind = (ckUnknown, ckFramework, ckAJAX); //// used to customize TSQLRestServerURIContext.ClientSideInvoke process TSQLRestServerURIContextClientInvoke = set of (csiAsOctetStream); /// abstract calling context for a TSQLRestServerCallBack event handler // - having a dedicated class avoid changing the implementation methods // signature if the framework add some parameters or behavior to it // - see TSQLRestServerCallBack for general code use // - most of the internal methods are declared as virtual, so it allows any // kind of custom routing or execution scheme // - instantiated by the TSQLRestServer.URI() method using its ServicesRouting // property // - see TSQLRestRoutingREST and TSQLRestRoutingJSON_RPC for working inherited // classes - NEVER set this abstract TSQLRestServerURIContext class to // TSQLRest.ServicesRouting property ! TSQLRestServerURIContext = class protected fInput: TRawUTF8DynArray; // even items are parameter names, odd are values fInputPostContentType: RawUTF8; fInputCookiesRetrieved: boolean; fInputCookies: array of record Name, Value: RawUTF8; // only computed if InCookie[] is used end; fInHeaderLastName: RawUTF8; fInHeaderLastValue: RawUTF8; fOutSetCookie: RawUTF8; fUserAgent: RawUTF8; fAuthenticationBearerToken: RawUTF8; fRemoteIP: RawUTF8; fSession: TAuthSession; // not published to avoid unexpected GPF on access fServiceListInterfaceMethodIndex: integer; fClientKind: TSQLRestServerURIContextClientKind; // just a wrapper over @ServiceContext threadvar fThreadServer: PServiceRunningContext; fSessionAccessRights: TSQLAccessRights; // fSession may be deleted meanwhile {$ifndef NOVARIANTS} function GetInput(const ParamName: RawUTF8): variant; function GetInputOrVoid(const ParamName: RawUTF8): variant; {$endif} function GetInputNameIndex(const ParamName: RawUTF8): PtrInt; function GetInputExists(const ParamName: RawUTF8): Boolean; function GetInputInt(const ParamName: RawUTF8): Int64; function GetInputDouble(const ParamName: RawUTF8): Double; procedure GetInputByName(const ParamName,InputName: RawUTF8; var result: RawUTF8); function GetInputUTF8(const ParamName: RawUTF8): RawUTF8; {$ifdef HASINLINE}inline;{$endif} function GetInputString(const ParamName: RawUTF8): string; function GetInputIntOrVoid(const ParamName: RawUTF8): Int64; {$ifdef HASINLINE}inline;{$endif} function GetInputHexaOrVoid(const ParamName: RawUTF8): cardinal; function GetInputDoubleOrVoid(const ParamName: RawUTF8): Double; function GetInputUTF8OrVoid(const ParamName: RawUTF8): RawUTF8; function GetInputStringOrVoid(const ParamName: RawUTF8): string; function GetInHeader(const HeaderName: RawUTF8): RawUTF8; procedure RetrieveCookies; function GetInCookie(CookieName: RawUTF8): RawUTF8; procedure SetInCookie(CookieName, CookieValue: RawUTF8); function GetUserAgent: RawUTF8; {$ifdef HASINLINE}inline;{$endif} function GetRemoteIP: RawUTF8; {$ifdef HASINLINE}inline;{$endif} function GetRemoteIPNotLocal: RawUTF8; {$ifdef HASINLINE}inline;{$endif} function GetRemoteIPIsLocalHost: boolean; {$ifdef HASINLINE}inline;{$endif} function GetResourceFileName: TFileName; procedure SetOutSetCookie(aOutSetCookie: RawUTF8); procedure ServiceResultStart(WR: TTextWriter); virtual; procedure ServiceResultEnd(WR: TTextWriter; ID: TID); virtual; procedure InternalSetTableFromTableIndex(Index: integer); virtual; procedure InternalSetTableFromTableName(TableName: PUTF8Char); virtual; procedure InternalExecuteSOAByInterface; virtual; procedure StatsFromContext(Stats: TSynMonitorInputOutput; var Diff: Int64; DiffIsMicroSecs: boolean); /// event raised by ExecuteMethod() for interface parameters // - match TServiceMethodInternalExecuteCallback signature procedure ExecuteCallback(var Par: PUTF8Char; ParamInterfaceInfo: PTypeInfo; out Obj); /// retrieve RESTful URI routing // - should set URI, Table,TableIndex,TableRecordProps,TableEngine, // ID, URIBlobFieldName and Parameters members // - all Table* members will be set via a InternalSetTableFromTableName() call // - default implementation expects an URI encoded with // 'ModelRoot[/TableName[/TableID][/BlobFieldName]][?param=...]' format // - will also set URISessionSignaturePos and URIWithoutSignature members // - return FALSE in case of incorrect URI (e.g. does not match Model.Root) function URIDecodeREST: boolean; virtual; /// retrieve method-based SOA URI routing with optional RESTful mode // - should set MethodIndex member // - default RESTful implementation expects an URI encoded with // 'ModelRoot/MethodName' or 'ModelRoot/TableName[/TableID]/MethodName' formats procedure URIDecodeSOAByMethod; virtual; /// retrieve interface-based SOA // - should set Service member (and possibly ServiceMethodIndex) // - abstract implementation which is to be overridden procedure URIDecodeSOAByInterface; virtual; abstract; /// process authentication // - return FALSE in case of invalid signature, TRUE if authenticated function Authenticate: boolean; virtual; /// method called in case of authentication failure // - the failure origin is stated by the Reason parameter // - this default implementation will just set OutStatus := HTTP_FORBIDDEN // and call TSQLRestServer.OnAuthenticationFailed event (if any) procedure AuthenticationFailed(Reason: TNotifyAuthenticationFailedReason); virtual; /// direct launch of a method-based service // - URI() will ensure that MethodIndex>=0 before calling it procedure ExecuteSOAByMethod; virtual; /// direct launch of an interface-based service // - URI() will ensure that Service<>nil before calling it // - abstract implementation which is to be overridden procedure ExecuteSOAByInterface; virtual; abstract; /// handle GET/LOCK/UNLOCK/STATE verbs for ORM/CRUD process procedure ExecuteORMGet; virtual; /// handle POST/PUT/DELETE/BEGIN/END/ABORT verbs for ORM/CRUD process // - execution of this method is protected by a critical section procedure ExecuteORMWrite; virtual; /// launch the Execute* method in the execution mode // set by Server.AcquireExecutionMode/AcquireExecutionLockedTimeOut // - this is the main process point from TSQLRestServer.URI() procedure ExecuteCommand; public /// the associated TSQLRestServer instance which executes its URI method Server: TSQLRestServer; /// the used Client-Server method (matching the corresponding HTTP Verb) // - this property will be set from incoming URI, even if RESTful // authentication is not enabled Method: TSQLURIMethod; /// the URI address, excluding trailing /info and ?par1=.... parameters // - can be either the table name (in RESTful protocol), or a service name URI: RawUTF8; /// same as Call^.URI, but without the &session_signature=... ending URIWithoutSignature: RawUTF8; /// points inside Call^.URI, after the 'root/' prefix URIAfterRoot: PUTF8Char; /// the optional Blob field name as specified in URI // - e.g. retrieved from "ModelRoot/TableName/TableID/BlobFieldName" URIBlobFieldName: RawUTF8; /// position of the &session_signature=... text in Call^.url string URISessionSignaturePos: integer; /// the Table as specified at the URI level (if any) Table: TSQLRecordClass; /// the index in the Model of the Table specified at the URI level (if any) TableIndex: integer; /// the RTTI properties of the Table specified at the URI level (if any) TableRecordProps: TSQLModelRecordProperties; /// the RESTful instance implementing the Table specified at the URI level (if any) // - equals TSQLRestServer most of the time, but may be an TSQLRestStorage // for any in-memory/MongoDB/virtual instance TableEngine: TSQLRest; /// the associated TSQLRecord.ID, as decoded from URI scheme // - this property will be set from incoming URI, even if RESTful // authentication is not enabled TableID: TID; /// the current execution command Command: TSQLRestServerURIContextCommand; /// the index of the callback published method within the internal class list MethodIndex: integer; /// the service identified by an interface-based URI Service: TServiceFactoryServer; /// the method index for an interface-based service // - Service member has already be retrieved from URI (so is not nil) // - 0..2 are the internal _free_/_contract_/_signature_ pseudo-methods ServiceMethodIndex: integer; /// access to the raw PServiceMethod information of an interface-based URI // - equals nil if ServiceMethodIndex in 0..2 (pseudo-methods) ServiceMethod: pointer; /// the JSON array of parameters for an the interface-based service // - Service member has already be retrieved from URI (so is not nil) ServiceParameters: PUTF8Char; /// the instance ID for interface-based services instance // - can be e.g. the client session ID for sicPerSession or the thread ID for // sicPerThread ServiceInstanceID: PtrUInt; /// the current execution context of an interface-based service // - maps to Service.fExecution[ServiceMethodIndex-SERVICE_PSEUDO_METHOD_COUNT] ServiceExecution: PServiceFactoryExecution; /// the current execution options of an interface-based service // - contain ServiceExecution.Options including optNoLogInput/optNoLogOutput // in case of TInterfaceFactory.RegisterUnsafeSPIType ServiceExecutionOptions: TServiceMethodOptions; /// force the interface-based service methods to return a JSON object // - default behavior is to follow Service.ResultAsJSONObject property value // (which own default is to return a more convenient JSON array) // - if set to TRUE, this execution context will FORCE the method to return // a JSON object, even if Service.ResultAsJSONObject=false: this may be // handy when the method is executed from a JavaScript content ForceServiceResultAsJSONObject: boolean; /// force the interface-based service methods to return a plain JSON object // - i.e. '{....}' instead of '{"result":{....}}' // - only set if ForceServiceResultAsJSONObject=TRUE and if no ID is about // to be returned // - could be used e.g. for stateless interaction with a (non mORMot) // stateless JSON REST Server ForceServiceResultAsJSONObjectWithoutResult: boolean; /// force the interface-based service methods to return a XML object // - default behavior is to follow Service.ResultAsJSONObject property value // (which own default is to return a more convenient JSON array) // - if set to TRUE, this execution context will FORCE the method to return // a XML object, by setting ForceServiceResultAsJSONObject then converting // the resulting JSON object into the corresponding XML via JSONBufferToXML() // - TSQLRestServerURIContext.InternalExecuteSOAByInterface will inspect the // Accept HTTP header to check if the answer should be XML rather than JSON ForceServiceResultAsXMLObject: boolean; /// specify a custom name space content when returning a XML object // - default behavior is to follow Service.ResultAsXMLObjectNameSpace // property (which is void by default) // - service may set e.g. XMLUTF8_NAMESPACE, which will append // around the generated XML data, to avoid validation problems // or set a particular XML name space, depending on the application ForceServiceResultAsXMLObjectNameSpace: RawUTF8; /// URI inlined parameters // - use UrlDecodeValue*() functions to retrieve the values // - for mPOST requests, will also be filled for following content types: // ! application/x-www-form-urlencoded or multipart/form-data Parameters: PUTF8Char; /// URI inlined parameters position in Call^.url string // - use Parameters field to retrieve the values ParametersPos: integer; /// access to all input/output parameters at TSQLRestServer.URI() level // - process should better call Results() or Success() methods to set the // appropriate answer or Error() method in case of an error // - low-level access to the call parameters can be made via this pointer Call: PSQLRestURIParams; /// the corresponding session TAuthSession.IDCardinal value // - equals 0 (CONST_AUTHENTICATION_SESSION_NOT_STARTED) if the session // is not started yet - i.e. if still in handshaking phase // - equals 1 (CONST_AUTHENTICATION_NOT_USED) if authentication mode // is not enabled - i.e. if TSQLRestServer.HandleAuthentication = FALSE Session: cardinal; /// the corresponding TAuthSession.User.GroupRights.ID value // - is undefined if Session is 0 or 1 (no authentication running) SessionGroup: integer; /// the corresponding TAuthSession.User.ID value // - is undefined if Session is 0 or 1 (no authentication running) SessionUser: TID; /// the corresponding TAuthSession.User.LogonName value // - is undefined if Session is 0 or 1 (no authentication running) SessionUserName: RawUTF8; /// the internal ID used to identify modelroot/_safe_ custom encryption SafeProtocolID: integer; /// the static instance corresponding to the associated Table (if any) {$ifdef FPC}&Static{$else}Static{$endif}: TSQLRest; /// the kind of static instance corresponding to the associated Table (if any) StaticKind: TSQLRestServerKind; /// optional error message which will be transmitted as JSON error (if set) // - contains e.g. TNotifyAuthenticationFailedReason text during // TSQLRestServer.OnAuthenticationFailed event call, or the reason of a // TSQLRestServer.RecordCanBeUpdated failure CustomErrorMsg: RawUTF8; /// high-resolution timimg of the execution command, in micro-seconds // - only set when TSQLRestServer.URI finished MicroSecondsElapsed: QWord; /// JWT validation information, as filled by AuthenticationCheck() JWTContent: TJWTContent; {$ifdef WITHLOG} /// associated logging instance for the current thread on the server // - you can use it to log some process on the server side Log: TSynLog; {$endif} /// initialize the execution context // - this method could have been declared as protected, since it should // never be called outside the TSQLRestServer.URI() method workflow // - should set Call, and Method members constructor Create(aServer: TSQLRestServer; const aCall: TSQLRestURIParams); virtual; /// finalize the execution context destructor Destroy; override; /// extract the input parameters from its URI // - you should not have to call this method directly, but rather // all the InputInt/InputDouble/InputUTF8/InputExists/... properties // - may be useful if you want to access directly to InputPairs[] with no // prior knowledge of the input parameter names // - you can specify a title text to optionally log the input array procedure FillInput(const LogInputIdent: RawUTF8=''); /// retrieve one input parameter from its URI name as Int64 // - raise an EParsingException if the parameter is not found property InputInt[const ParamName: RawUTF8]: Int64 read GetInputInt; /// retrieve one input parameter from its URI name as double // - raise an EParsingException if the parameter is not found property InputDouble[const ParamName: RawUTF8]: double read GetInputDouble; /// retrieve one input parameter from its URI name as RawUTF8 // - raise an EParsingException if the parameter is not found property InputUTF8[const ParamName: RawUTF8]: RawUTF8 read GetInputUTF8; /// retrieve one input parameter from its URI name as a VCL string // - raise an EParsingException if the parameter is not found // - prior to Delphi 2009, some Unicode characters may be missing in the // returned AnsiString value property InputString[const ParamName: RawUTF8]: string read GetInputString; /// retrieve one input parameter from its URI name as Int64 // - returns 0 if the parameter is not found property InputIntOrVoid[const ParamName: RawUTF8]: Int64 read GetInputIntOrVoid; /// retrieve one hexadecimal input parameter from its URI name as cardinal // - returns 0 if the parameter is not found property InputHexaOrVoid[const ParamName: RawUTF8]: cardinal read GetInputHexaOrVoid; /// retrieve one input parameter from its URI name as double // - returns 0 if the parameter is not found property InputDoubleOrVoid[const ParamName: RawUTF8]: double read GetInputDoubleOrVoid; /// retrieve one input parameter from its URI name as RawUTF8 // - returns '' if the parameter is not found property InputUTF8OrVoid[const ParamName: RawUTF8]: RawUTF8 read GetInputUTF8OrVoid; /// retrieve one input parameter from its URI name as a VCL string // - returns '' if the parameter is not found // - prior to Delphi 2009, some Unicode characters may be missing in the // returned AnsiString value property InputStringOrVoid[const ParamName: RawUTF8]: string read GetInputStringOrVoid; /// retrieve one input parameter from its URI name as RawUTF8 // - returns FALSE and call Error(ErrorMessageForMissingParameter) - which // may be a resourcestring - if the parameter is not found // - returns TRUE and set Value if the parameter is found function InputUTF8OrError(const ParamName: RawUTF8; out Value: RawUTF8; const ErrorMessageForMissingParameter: string): boolean; /// retrieve one input parameter from its URI name as RawUTF8 // - returns supplied DefaultValue if the parameter is not found function InputUTF8OrDefault(const ParamName, DefaultValue: RawUTF8): RawUTF8; /// retrieve one input parameter from its URI name as an enumeration // - will expect the value to be specified as integer, or as the textual // representation of the enumerate, ignoring any optional lowercase prefix // as featured by TEnumType.GetEnumNameValue() // - returns TRUE and set ValueEnum if the parameter is found and correct // - returns FALSE and set ValueEnum to first item (i.e. DefaultEnumOrd) if // the parameter is not found, or not containing a correct value function InputEnum(const ParamName: RawUTF8; EnumType: PTypeInfo; out ValueEnum; DefaultEnumOrd: integer=0): boolean; /// return TRUE if the input parameter is available at URI // - even if InputUTF8['param']='', there may be '..?param=&another=2' property InputExists[const ParamName: RawUTF8]: Boolean read GetInputExists; {$ifndef NOVARIANTS} /// retrieve one input parameter from its URI name as variant // - if the parameter value is text, it is stored in the variant as // a generic VCL string content: so before Delphi 2009, you may loose // some characters at decoding from UTF-8 input buffer // - raise an EParsingException if the parameter is not found property Input[const ParamName: RawUTF8]: variant read GetInput; default; /// retrieve one input parameter from its URI name as variant // - if the parameter value is text, it is stored in the variant as // a RawUTF8: so before Delphi 2009, you won't loose any Unicode character, // but you should convert its value to AnsiString using UTF8ToString() // - returns Unassigned if the parameter is not found property InputOrVoid[const ParamName: RawUTF8]: variant read GetInputOrVoid; /// retrieve one input parameter from its URI name as variant // - returns FALSE and call Error(ErrorMessageForMissingParameter) - which // may be a resourcestring - if the parameter is not found // - returns TRUE and set Value if the parameter is found // - if the parameter value is text, it is stored in the variant as // a RawUTF8: so before Delphi 2009, you won't loose any Unicode character, // but you should convert its value to AnsiString using UTF8ToString() function InputOrError(const ParamName: RawUTF8; out Value: variant; const ErrorMessageForMissingParameter: string): boolean; /// retrieve all input parameters from URI as a variant JSON object // - returns Unassigned if no parameter was defined // - returns a JSON object with input parameters encoded as // ! {"name1":value1,"name2":value2...} // - optionally with a PServiceMethod information about the actual values types // - if the parameters were encoded as multipart, the JSON object // will be encoded with its textual values, or with nested objects, if // the data was supplied as binary: // ! {"name1":{"data":..,"filename":...,"contenttype":...},"name2":...} // since name1.data will be Base64 encoded, so you should better // use the InputAsMultiPart() method instead when working with binary function GetInputAsTDocVariant(const Options: TDocVariantOptions; ServiceMethod: pointer): variant; {$endif} /// decode any multipart/form-data POST request input // - returns TRUE and set MultiPart array as expected, on success function InputAsMultiPart(var MultiPart: TMultiPartDynArray): Boolean; /// low-level access to the input parameters, stored as pairs of UTF-8 // - even items are parameter names, odd are values // - Input*[] properties should have been called previously to fill the // internal array, or by calling FillInput if you do not know the input // parameters which may appear property InputPairs: TRawUTF8DynArray read FInput; /// retrieve an incoming HTTP header // - the supplied header name is case-insensitive // - but rather call RemoteIP or UserAgent properties instead of // InHeader['remoteip'] or InHeader['User-Agent'] property InHeader[const HeaderName: RawUTF8]: RawUTF8 read GetInHeader; /// retrieve an incoming HTTP cookie value // - cookie name are case-sensitive property InCookie[CookieName: RawUTF8]: RawUTF8 read GetInCookie write SetInCookie; /// define a new 'name=value' cookie to be returned to the client // - if not void, TSQLRestServer.URI() will define a new 'set-cookie: ...' // header in Call^.OutHead // - you can use COOKIE_EXPIRED as value to delete a cookie in the browser // - if no Path=/.. is included, it will append // $ '; Path=/'+Server.Model.Root+'; HttpOnly' property OutSetCookie: RawUTF8 read fOutSetCookie write SetOutSetCookie; /// retrieve the "User-Agent" value from the incoming HTTP headers property UserAgent: RawUTF8 read GetUserAgent; /// retrieve the "RemoteIP" value from the incoming HTTP headers property RemoteIP: RawUTF8 read GetRemoteIP; /// true if the "RemoteIP" value from the incoming HTTP headers is '127.0.0.1' property RemoteIPIsLocalHost: boolean read GetRemoteIPIsLocalHost; /// "RemoteIP" value from the incoming HTTP headers but '' for '127.0.0.1' property RemoteIPNotLocal: RawUTF8 read GetRemoteIPNotLocal; /// retrieve the "Authorization: Bearer " value from incoming HTTP headers // - typically returns a JWT for statelesss self-contained authentication, // as expected by TJWTAbstract.Verify method // - as an alternative, a non-standard and slightly less safe way of // token transmission may be to encode its value as ?authenticationbearer=.... // URI parameter (may be convenient when embedding resources in HTML DOM) function AuthenticationBearerToken: RawUTF8; /// validate "Authorization: Bearer " content from incoming HTTP headers // - returns true on success, storing the payload in the JWTContent field // - set JWTContent.result = jwtNoToken if jwt is nil // - on failure (i.e. returns false), will set the error context as // 403 HTTP_FORBIDDEN so that you may directly write: // ! procedure TMyDaemon.Files(Ctxt: TSQLRestServerURIContext); // ! begin // ! if Ctxt.AuthenticationCheck(fJWT) then // ! Ctxt.ReturnFileFromFolder('c:\datafolder'); // ! end; function AuthenticationCheck(jwt: TJWTAbstract): boolean; virtual; /// identify which kind of client is actually connected // - the "User-Agent" HTTP will be checked for 'mORMot' substring, and // set ckFramework on match // - either ckAjax for a classic (AJAX) browser, or any other kind of // HTTP client // - will be used e.g. by ClientSQLRecordOptions to check if the // current remote client expects standard JSON in all cases function ClientKind: TSQLRestServerURIContextClientKind; /// identify if the request is about a Table containing nested objects or // arrays, which could be serialized as JSON objects or arrays, instead // of plain JSON string (as stored in the database) // - will idenfity ClientKind=ckAjax, or check for rsoGetAsJsonNotAsString // in TSQLRestServer.Options function ClientSQLRecordOptions: TJSONSerializerSQLRecordOptions; /// true if called from TSQLRestServer.AdministrationExecute function IsRemoteAdministrationExecute: boolean; /// compute the file name corresponding to the URI // - e.g. '/root/methodname/toto/index.html' will return 'toto\index.html' property ResourceFileName: TFileName read GetResourceFileName; /// use this method to send back directly a result value to the caller // - expects Status to be either HTTP_SUCCESS, HTTP_NOTMODIFIED, // HTTP_CREATED, or HTTP_TEMPORARYREDIRECT, and will return as answer the // supplied Result content with no transformation // - if Status is an error code, it will call Error() method // - CustomHeader optional parameter can be set e.g. to // TEXT_CONTENT_TYPE_HEADER if the default JSON_CONTENT_TYPE is not OK, // or calling GetMimeContentTypeHeader() on the returned binary buffer // - if Handle304NotModified is TRUE and Status is HTTP_SUCCESS, the Result // content will be hashed (using crc32c) and in case of no modification // will return HTTP_NOTMODIFIED to the browser, without the actual result // content (to save bandwidth) // - set CacheControlMaxAge<>0 to include a Cache-Control: max-age=xxx header procedure Returns(const Result: RawUTF8; Status: integer=HTTP_SUCCESS; const CustomHeader: RawUTF8=''; Handle304NotModified: boolean=false; HandleErrorAsRegularResult: boolean=false; CacheControlMaxAge: integer=0; ServerHash: RawUTF8=''); overload; /// use this method to send back a JSON object to the caller // - this method will encode the supplied values e.g. as // ! JSONEncode(['name','John','year',1972]) = '{"name":"John","year":1972}' // - implementation is just a wrapper around Returns(JSONEncode([])) // - note that cardinal values should be type-casted to Int64() (otherwise // the integer mapped value will be transmitted, therefore wrongly) // - expects Status to be either HTTP_SUCCESS or HTTP_CREATED // - caller can set Handle304NotModified=TRUE for Status=HTTP_SUCCESS procedure Returns(const NameValuePairs: array of const; Status: integer=HTTP_SUCCESS; Handle304NotModified: boolean=false; HandleErrorAsRegularResult: boolean=false; const CustomHeader: RawUTF8=''); overload; /// use this method to send back any object as JSON document to the caller // - this method will call ObjectToJson() to compute the returned content // - you can customize SQLRecordOptions, to force the returned JSON // object to have its TSQLRecord nested fields serialized as true JSON // arrays or objects, or add an "ID_str" string field for JavaScript procedure Returns(Value: TObject; Status: integer=HTTP_SUCCESS; Handle304NotModified: boolean=false; SQLRecordOptions: TJSONSerializerSQLRecordOptions=[]; const CustomHeader: RawUTF8=''); overload; /// use this method to send back any variant as JSON to the caller // - this method will call VariantSaveJSON() to compute the returned content procedure ReturnsJson(const Value: variant; Status: integer=HTTP_SUCCESS; Handle304NotModified: boolean=false; Escape: TTextWriterKind=twJSONEscape; MakeHumanReadable: boolean=false; const CustomHeader: RawUTF8=''); /// uses this method to send back directly any binary content to the caller // - the exact MIME type will be retrieved using GetMimeContentTypeHeader(), // from the supplied Blob binary buffer, and optional a file name // - by default, the HTTP_NOTMODIFIED process will take place, to minimize // bandwidth between the server and the client // - set CacheControlMaxAge<>0 to include a Cache-Control: max-age=xxx header procedure ReturnBlob(const Blob: RawByteString; Status: integer=HTTP_SUCCESS; Handle304NotModified: boolean=true; const FileName: TFileName=''; CacheControlMaxAge: integer=0); /// use this method to send back a file to the caller // - this method will let the HTTP server return the file content // - if Handle304NotModified is TRUE, will check the file age to ensure // that the file content will be sent back to the server only if it changed; // set CacheControlMaxAge<>0 to include a Cache-Control: max-age=xxx header // - if ContentType is left to default '', method will guess the expected // mime-type from the file name extension // - if the file name does not exist, a generic 404 error page will be // returned, unless an explicit redirection is defined in Error404Redirect // - you can also specify the resulting file name, as downloaded and written // by the client browser, in the optional AttachmentFileName parameter, if // the URI does not match the expected file name procedure ReturnFile(const FileName: TFileName; Handle304NotModified: boolean=false; const ContentType: RawUTF8=''; const AttachmentFileName: RawUTF8=''; const Error404Redirect: RawUTF8=''; CacheControlMaxAge: integer=0); /// use this method to send back a file from a local folder to the caller // - URIBlobFieldName value, as parsed from the URI, will containn the // expected file name in the local folder, using DefaultFileName if the // URI is void, and redirecting to Error404Redirect if the file is not found // - this method will let the HTTP server return the file content // - if Handle304NotModified is TRUE, will check the file age to ensure // that the file content will be sent back to the server only if it changed // set CacheControlMaxAge<>0 to include a Cache-Control: max-age=xxx header procedure ReturnFileFromFolder(const FolderName: TFileName; Handle304NotModified: boolean=true; const DefaultFileName: TFileName='index.html'; const Error404Redirect: RawUTF8=''; CacheControlMaxAge: integer=0); /// use this method notify the caller that the resource URI has changed // - returns a HTTP_TEMPORARYREDIRECT status with the specified location, // or HTTP_MOVEDPERMANENTLY if PermanentChange is TRUE procedure Redirect(const NewLocation: RawUTF8; PermanentChange: boolean=false); /// use this method to send back a JSON object with a "result" field // - this method will encode the supplied values as a {"result":"...} // JSON object, as such for one value: // $ {"result":"OneValue"} // (with one value, you can just call TSQLRestClientURI.CallBackGetResult // method to call and decode this value) // or as a JSON object containing an array of values: // $ {"result":["One","two"]} // - expects Status to be either HTTP_SUCCESS or HTTP_CREATED // - caller can set Handle304NotModified=TRUE for Status=HTTP_SUCCESS and/or // set CacheControlMaxAge<>0 to include a Cache-Control: max-age=xxx header procedure Results(const Values: array of const; Status: integer=HTTP_SUCCESS; Handle304NotModified: boolean=false; CacheControlMaxAge: integer=0); /// use this method if the caller expect no data, just a status // - just wrap the overloaded Returns() method with no result value // - if Status is an error code, it will call Error() method // - by default, calling this method will mark process as successfull procedure Success(Status: integer=HTTP_SUCCESS); virtual; /// use this method to send back an error to the caller // - expects Status to not be HTTP_SUCCESS neither HTTP_CREATED, // and will send back a JSON error message to the caller, with the // supplied error text // - set CacheControlMaxAge<>0 to include a Cache-Control: max-age=xxx header // - if no ErrorMessage is specified, will return a default text // corresponding to the Status code procedure Error(const ErrorMessage: RawUTF8=''; Status: integer=HTTP_BADREQUEST; CacheControlMaxAge: integer=0); overload; virtual; /// use this method to send back an error to the caller // - implementation is just a wrapper over Error(FormatUTF8(Format,Args)) procedure Error(const Format: RawUTF8; const Args: array of const; Status: integer=HTTP_BADREQUEST; CacheControlMaxAge: integer=0); overload; /// use this method to send back an error to the caller // - will serialize the supplied exception, with an optional error message procedure Error(E: Exception; const Format: RawUTF8; const Args: array of const; Status: integer=HTTP_BADREQUEST); overload; /// implements a method-based service for live update of some settings // - should be called from a method-based service, e.g. Configuration() // - the settings are expected to be stored e.g. in a TSynAutoCreateFields // instance, potentially with nested objects // - accept the following REST methods to read and write the settings: // ! GET http://server:888/root/configuration // ! GET http://server:888/root/configuration/propname // ! GET http://server:888/root/configuration/propname?value=propvalue // - could be used e.g. as such: // ! procedure TMyRestServerMethods.Configuration(Ctxt: TSQLRestServerURIContext); // ! begin // http://server:888/myrestserver/configuration/name?value=newValue // ! Ctxt.ConfigurationRestMethod(fSettings); // ! end; procedure ConfigurationRestMethod(SettingsStorage: TObject); /// at Client Side, compute URI and BODY according to the routing scheme // - abstract implementation which is to be overridden // - as input, method should be the method name to be executed, // params should contain the incoming parameters as JSON CSV (without []), // and clientDriven ID should contain the optional Client ID value // - at output, should update the HTTP uri corresponding to the proper // routing, and should return the corresponding HTTP body within sent class procedure ClientSideInvoke(var uri: RawUTF8; ctxt: TSQLRestServerURIContextClientInvoke; const method, params, clientDrivenID: RawUTF8; out sent,head: RawUTF8); virtual; abstract; end; /// calling context for a TSQLRestServerCallBack using simple REST for // interface-based services // - this class will use RESTful routing for interface-based services: // method name will be identified within the URI, as // $ /Model/Interface.Method[/ClientDrivenID] // e.g. for ICalculator.Add: // $ POST /root/Calculator.Add // $ (...) // $ [1,2] // or, for a sicClientDriven mode service: // $ POST /root/ComplexNumber.Add/1234 // $ (...) // $ [20,30] // in this case, the sent content will be a JSON array of [parameters...] // - as an alternative, input parameters may be encoded at URI level (with // a size limit depending on the HTTP routers, whereas there is no such // limitation when they are transmitted as message body) // - one benefit of having .../ClientDrivenID encoded at URI is that it will // be more secured in our RESTful authentication scheme: each method and even // client driven session will be signed individualy TSQLRestRoutingREST = class(TSQLRestServerURIContext) protected /// retrieve interface-based SOA with URI RESTful routing // - should set Service member (and possibly ServiceMethodIndex) // - this overridden implementation expects an URI encoded with // '/Model/Interface.Method[/ClientDrivenID]' for this class, and // will set ServiceMethodIndex for next ExecuteSOAByInterface method call procedure URIDecodeSOAByInterface; override; /// direct launch of an interface-based service with URI RESTful routing // - this overridden implementation expects parameters to be sent as one JSON // array body (Delphi/AJAX way) or optionally with URI decoding (HTML way): // ! function TServiceCalculator.Add(n1, n2: integer): integer; // will accept such requests: // ! URL='root/Calculator.Add' and InBody='[ 1,2 ]' // ! URL='root/Calculator.Add?+%5B+1%2C2+%5D' // decoded as ' [ 1,2 ]' // ! URL='root/Calculator.Add?n1=1&n2=2' // in any order, even missing procedure ExecuteSOAByInterface; override; public /// at Client Side, compute URI and BODY according to RESTful routing scheme // - e.g. on input uri='root/Calculator', method='Add', params='1,2' and // clientDrivenID='1234' -> on output uri='root/Calculator.Add/1234' and // sent='[1,2]' class procedure ClientSideInvoke(var uri: RawUTF8; ctxt: TSQLRestServerURIContextClientInvoke; const method, params, clientDrivenID: RawUTF8; out sent,head: RawUTF8); override; end; /// calling context for a TSQLRestServerCallBack using JSON/RPC for // interface-based services // - in this routing scheme, the URI will define the interface, then the // method name will be inlined with parameters, e.g. // $ POST /root/Calculator // $ (...) // $ {"method":"Add","params":[1,2]} // or, for a sicClientDriven mode service: // $ POST /root/ComplexNumber // $ (...) // $ {"method":"Add","params":[20,30],"id":1234} TSQLRestRoutingJSON_RPC = class(TSQLRestServerURIContext) protected /// retrieve interface-based SOA with URI JSON/RPC routing // - this overridden implementation expects an URI encoded with // '/Model/Interface' as for the JSON/RPC routing scheme, and won't // set ServiceMethodIndex at this level (but in ExecuteSOAByInterface) procedure URIDecodeSOAByInterface; override; /// direct launch of an interface-based service with URI JSON/RPC routing // - URI() will ensure that Service<>nil before calling it // - this overridden implementation expects parameters to be sent as part // of a JSON object body: // $ {"method":"Add","params":[20,30],"id":1234} procedure ExecuteSOAByInterface; override; public /// at Client Side, compute URI and BODY according to JSON/RPC routing scheme // - e.g. on input uri='root/Calculator', method='Add', params='1,2' and // clientDrivenID='1234' -> on output uri='root/Calculator' and // sent={"method":"Add","params":[1,2],"id":1234} class procedure ClientSideInvoke(var uri: RawUTF8; ctxt: TSQLRestServerURIContextClientInvoke; const method, params, clientDrivenID: RawUTF8; out sent,head: RawUTF8); override; end; /// method prototype to be used on Server-Side for method-based services // - will be routed as ModelRoot/[TableName/TableID/]MethodName RESTful requests // - this mechanism is able to handle some custom Client/Server request, similar // to the DataSnap technology, but in a KISS way; it's fully integrated in the // Client/Server architecture of our framework // - just add a published method of this type to any TSQLRestServer descendant // - when TSQLRestServer.URI receive a request for ModelRoot/MethodName // or ModelRoot/TableName/TableID/MethodName, it will check for a published method // in its self instance named MethodName which MUST be of TSQLRestServerCallBack // type (not checked neither at compile time neither at runtime: beware!) and // call it to handle the request // - important warning: the method implementation MUST be thread-safe // - when TSQLRestServer.URI receive a request for ModelRoot/MethodName, // it calls the corresponding published method with aRecord set to nil // - when TSQLRestServer.URI receive a request for ModelRoot/TableName/TableID/MethodName, // it calls the corresponding published method with aRecord pointing to a // just created instance of the corresponding class, with its field ID set; // note that the only set field is ID: other fields of aRecord are not set, but // must secificaly be retrieved on purpose // - for ModelRoot/TableName/TableID/MethodName, the just created instance will // be freed by TSQLRestServer.URI when the method returns // - Ctxt.Parameters values are set from incoming URI, and each parameter can be // retrieved with a loop like this: // ! if not UrlDecodeNeedParameters(Ctxt.Parameters,'SORT,COUNT') then // ! exit; // ! while Ctxt.Parameters<>nil do begin // ! UrlDecodeValue(Ctxt.Parameters,'SORT=',aSortString); // ! UrlDecodeValueInteger(Ctxt.Parameters,'COUNT=',aCountInteger,@Ctxt.Parameters); // ! end; // - Ctxt.Call is set with low-level incoming and outgoing data from client // (e.g. Ctxt.Call.InBody contain POST/PUT data message) // - Ctxt.Session* will identify to the authentication session of the remote client // (CONST_AUTHENTICATION_NOT_USED=1 if authentication mode is not enabled or // CONST_AUTHENTICATION_SESSION_NOT_STARTED=0 if the session not started yet) - // code may use SessionGetUser() method to retrieve the user details // - Ctxt.Method will indicate the used HTTP verb (e.g. GET/POST/PUT..) // - if process succeeded, implementation shall call Ctxt.Results([]) method to // set a JSON response object with one "result" field name or Ctxt.Returns([]) // with a JSON object described in Name/Value pairs; if the returned value is // not JSON_CONTENT_TYPE, use Ctxt.Returns() and its optional CustomHeader // parameter can specify a custom header like TEXT_CONTENT_TYPE_HEADER // - if process succeeded, and no data is expected to be returned to the caller, // implementation shall call overloaded Ctxt.Success() method with the // expected status (i.e. just Ctxt.Success will return HTTP_SUCCESS) // - if process failed, implementation shall call Ctxt.Error() method to // set the corresponding error message and error code number // - a typical implementation may be: // ! procedure TSQLRestServerTest.Sum(Ctxt: TSQLRestServerURIContext); // ! var a,b: TSynExtended; // ! begin // ! if UrlDecodeNeedParameters(Ctxt.Parameters,'A,B') then begin // ! while Ctxt.Parameters<>nil do begin // ! UrlDecodeExtended(Ctxt.Parameters,'A=',a); // ! UrlDecodeExtended(Ctxt.Parameters,'B=',b,@Ctxt.Parameters); // ! end; // ! Ctxt.Results([a+b]); // ! // same as: Ctxt.Returns(JSONEncode(['result',a+b])); // ! // same as: Ctxt.Returns(['result',a+b]); // ! end else // ! Ctxt.Error('Missing Parameter'); // ! end; // - Client-Side can be implemented as you wish. By convention, it could be // appropriate to define in either TSQLRestServer (if to be called as // ModelRoot/MethodName), either TSQLRecord (if to be called as // ModelRoot/TableName[/TableID]/MethodName) a custom public or protected method, // calling TSQLRestClientURI.URL with the appropriate parameters, and named // (by convention) as MethodName; TSQLRestClientURI has dedicated methods // like CallBackGetResult, CallBackGet, CallBackPut and CallBack; see also // TSQLModel.getURICallBack and JSONDecode function // ! function TSQLRecordPeople.Sum(aClient: TSQLRestClientURI; a, b: double): double; // ! var err: integer; // ! begin // ! val(aClient.CallBackGetResult('sum',['a',a,'b',b]),result,err); // ! end; TSQLRestServerCallBack = procedure(Ctxt: TSQLRestServerURIContext) of object; /// description of a method-based service TSQLRestServerMethod = record /// the method name Name: RawUTF8; /// the event which will be executed for this method CallBack: TSQLRestServerCallBack; /// set to TRUE disable Authentication check for this method // - use TSQLRestServer.ServiceMethodByPassAuthentication() method ByPassAuthentication: boolean; /// detailed statistics associated with this method Stats: TSynMonitorInputOutput; end; /// used to store all method-based services of a TSQLRestServer instance TSQLRestServerMethods = array of TSQLRestServerMethod; /// pointer to a description of a method-based service PSQLRestServerMethod = ^TSQLRestServerMethod; /// the possible options for handling table names TSQLCheckTableName = (ctnNoCheck,ctnMustExist,ctnTrimExisting); /// the possible options for TSQLRestServer.CreateMissingTables and // TSQLRecord.InitializeTable methods // - itoNoAutoCreateGroups and itoNoAutoCreateUsers will avoid // TSQLAuthGroup.InitializeTable to fill the TSQLAuthGroup and TSQLAuthUser // tables with default records // - itoNoCreateMissingField will avoid to create the missing fields on a table // - itoNoIndex4ID won't create the index for the main ID field (do nothing // on SQLite3, by design - but may be used for tables on external databases) // - itoNoIndex4UniqueField won't create indexes for "stored AS_UNIQUE" fields // - itoNoIndex4NestedRecord won't create indexes for TSQLRecord fields // - itoNoIndex4RecordReference won't create indexes for TRecordReference fields // - itoNoIndex4TID won't create indexes for TID fields // - itoNoIndex4RecordVersion won't create indexes for TRecordVersion fields // - INITIALIZETABLE_NOINDEX constant contain all itoNoIndex* items TSQLInitializeTableOption = ( itoNoAutoCreateGroups, itoNoAutoCreateUsers, itoNoCreateMissingField, itoNoIndex4ID, itoNoIndex4UniqueField, itoNoIndex4NestedRecord, itoNoIndex4RecordReference, itoNoIndex4TID, itoNoIndex4RecordVersion); /// the options to be specified for TSQLRestServer.CreateMissingTables and // TSQLRecord.InitializeTable methods TSQLInitializeTableOptions = set of TSQLInitializeTableOption; /// a dynamic array of TSQLRecordMany instances TSQLRecordManyObjArray = array of TSQLRecordMany; /// internal data used by TSQLRecord.FillPrepare()/FillPrepareMany() methods // - using a dedicated class will reduce memory usage for each TSQLRecord // instance (which won't need these properties most of the time) TSQLRecordFill = class protected /// associated table fTable: TSQLTable; /// current retrieved row fFillCurrentRow: integer; /// number of used items in TableMap[] array // - calculated in FillPrepare() or FillPrepareMany() methods fTableMapCount: integer; /// set by TSQLRecord.FillPrepareMany() to release M.fDestID^ instances fTableMapRecordManyInstances: TSQLRecordManyObjArray; /// map the published fields index // - calculated in FillPrepare() or FillPrepareMany() methods fTableMap: array of record /// the class instance to be filled from the TSQLTable // - can be a TSQLRecordMany instance after FillPrepareMany() method call Dest: TSQLRecord; /// the published property RTTI to be filled from the TSQLTable // - is nil for the RowID/ID field DestField: TSQLPropInfo; /// the column index in TSQLTable TableIndex: integer; end; /// mark all mapped or TModTime fields fTableMapFields: TSQLFieldBits; /// if Joined instances were initialized via TSQLRecord.CreateJoined() fJoinedFields: boolean; /// return fJoinedFields or false if self=nil function GetJoinedFields: boolean; {$ifdef HASINLINE}inline;{$endif} /// add a property to the fTableMap[] array // - aIndex is the column index in TSQLTable procedure AddMap(aRecord: TSQLRecord; aField: TSQLPropInfo; aIndex: integer); overload; /// add a property to the fTableMap[] array // - aIndex is the column index in TSQLTable procedure AddMap(aRecord: TSQLRecord; const aFieldName: RawUTF8; aIndex: integer); overload; /// add all simple property names, with to the fTableMap[] array // - will map ID/RowID, then all simple fields of this TSQLRecord // - aIndex is the column index in TSQLTable procedure AddMapSimpleFields(aRecord: TSQLRecord; const aProps: array of TSQLPropInfo; var aIndex: integer); public /// finalize the mapping destructor Destroy; override; /// map all columns of a TSQLTable to a record mapping procedure Map(aRecord: TSQLRecord; aTable: TSQLTable; aCheckTableName: TSQLCheckTableName); /// reset the mapping // - is called e.g. by TSQLRecord.FillClose // - will free any previous Table if necessary // - will release TSQLRecordMany.Dest instances as set by TSQLRecord.FillPrepareMany() procedure UnMap; /// fill a TSQLRecord published properties from a TSQLTable row // - use the mapping prepared with Map() method function Fill(aRow: integer): Boolean; overload; {$ifdef HASINLINE}inline;{$endif} /// fill a TSQLRecord published properties from a TSQLTable row // - use the mapping prepared with Map() method // - aTableRow will point to the first column of the matching row procedure Fill(aTableRow: PPUtf8CharArray); overload; /// fill a TSQLRecord published properties from a TSQLTable row // - overloaded method using a specified destination record to be filled // - won't work with cross-reference mapping (FillPrepareMany) // - use the mapping prepared with Map() method // - aTableRow will point to the first column of the matching row procedure Fill(aTableRow: PPUtf8CharArray; aDest: TSQLRecord); overload; /// fill a TSQLRecord published properties from a TSQLTable row // - overloaded method using a specified destination record to be filled // - won't work with cross-reference mapping (FillPrepareMany) // - use the mapping prepared with Map() method function Fill(aRow: integer; aDest: TSQLRecord): Boolean; overload; {$ifdef HASINLINE}inline;{$endif} /// used to compute the updated field bits during a fill // - will return Props.SimpleFieldsBits[soUpdate] if no fill is in process procedure ComputeSetUpdatedFieldBits(Props: TSQLRecordProperties; out Bits: TSQLFieldBits); /// return all mapped fields, or [] if nil function TableMapFields: TSQLFieldBits; /// the TSQLTable stated as FillPrepare() parameter // - the internal temporary table is stored here for TSQLRecordMany // - this instance is freed by TSQLRecord.Destroy if fTable.OwnerMustFree=true property Table: TSQLTable read fTable; /// the current Row during a Loop property FillCurrentRow: integer read fFillCurrentRow; /// equals TRUE if the instance was initialized via TSQLRecord.CreateJoined() // TSQLRecord.CreateAndFillPrepareJoined() // - it means that all nested TSQLRecord are pre-allocated instances, // not trans-typed pointer(IDs) property JoinedFields: boolean read GetJoinedFields; end; /// event signature triggered by TSQLRestBatch.OnWrite // - also used by TSQLRestServer.RecordVersionSynchronizeSlave*() methods TOnBatchWrite = procedure(Sender: TSQLRestBatch; Event: TSQLOccasion; Table: TSQLRecordClass; const ID: TID; Value: TSQLRecord; const ValueFields: TSQLFieldBits) of object; /// used to store a BATCH sequence of writing operations // - is used by TSQLRest to process BATCH requests using BatchSend() method, // or TSQLRestClientURI for its Batch*() methods // - but you can create your own stand-alone BATCH process, so that it will // be able to make some transactional process - aka the "Unit Of Work" pattern TSQLRestBatch = class protected fRest: TSQLRest; fInternalBufferSize: integer; fCalledWithinRest: boolean; fBatch: TJSONSerializer; fBatchFields: TSQLFieldBits; fTable: TSQLRecordClass; fTablePreviousSendData: TSQLRecordClass; fTableIndex: integer; fBatchCount: integer; fDeletedRecordRef: TIDDynArray; fDeletedCount: integer; fAddCount: integer; fUpdateCount: integer; fDeleteCount: integer; fAutomaticTransactionPerRow: cardinal; fOptions: TSQLRestBatchOptions; fOnWrite: TOnBatchWrite; function GetCount: integer; function GetSizeBytes: cardinal; procedure SetExpandedJSONWriter(Props: TSQLRecordProperties; ForceResetFields, withID: boolean; const WrittenFields: TSQLFieldBits); public /// begin a BATCH sequence to speed up huge database changes // - each call to normal Add/Update/Delete methods will create a Server request, // therefore can be slow (e.g. if the remote server has bad ping timing) // - start a BATCH sequence using this method, then call BatchAdd() BatchUpdate() // or BatchDelete() methods to make some changes to the database // - when BatchSend will be called, all the sequence transactions will be sent // as one to the remote server, i.e. in one URI request // - if BatchAbort is called instead, all pending BatchAdd/Update/Delete // transactions will be aborted, i.e. ignored // - expect one TSQLRecordClass as parameter, which will be used for the whole // sequence (in this case, you can't mix classes in the same BATCH sequence) // - if no TSQLRecordClass is supplied, the BATCH sequence will allow any // kind of individual record in BatchAdd/BatchUpdate/BatchDelete // - return TRUE on success, FALSE if aTable is incorrect or a previous BATCH // sequence was already initiated // - should normally be used inside a Transaction block: there is no automated // TransactionBegin..Commit/RollBack generated in the BATCH sequence if // you leave the default AutomaticTransactionPerRow=0 parameter - but // this may be a concern with a lot of concurrent clients // - you should better set AutomaticTransactionPerRow > 0 to execute all // BATCH processes within an unique transaction grouped by a given number // of rows, on the server side - set AutomaticTransactionPerRow=maxInt if // you want one huge transaction, or set a convenient value (e.g. 10000) // depending on the back-end database engine abilities, if you want to // retain the transaction log file small enough for the database engine // - BatchOptions could be set to tune the SQL execution, e.g. force INSERT // OR IGNORE on internal SQLite3 engine // - InternalBufferSize could be set to some high value (e.g. 10 shl 20), // if you expect a very high number of rows in this BATCH constructor Create(aRest: TSQLRest; aTable: TSQLRecordClass; AutomaticTransactionPerRow: cardinal=0; Options: TSQLRestBatchOptions=[]; InternalBufferSize: cardinal=65536); virtual; /// finalize the BATCH instance destructor Destroy; override; /// reset the BATCH sequence so that you can re-use the same TSQLRestBatch procedure Reset(aTable: TSQLRecordClass; AutomaticTransactionPerRow: cardinal=0; Options: TSQLRestBatchOptions=[]); overload; virtual; /// reset the BATCH sequence to its previous state // - could be used to prepare a next chunk of values, after a call to // TSQLRest.BatchSend procedure Reset; overload; /// create a new member in current BATCH sequence // - work in BATCH mode: nothing is sent to the server until BatchSend call // - returns the corresponding index in the current BATCH sequence, -1 on error // - if SendData is true, content of Value is sent to the server as JSON // - if ForceID is true, client sends the Value.ID field to use this ID for // adding the record (instead of a database-generated ID) // - if Value is TSQLRecordFTS3/4/5, Value.ID is stored to the virtual table // - Value class MUST match the TSQLRecordClass used at BatchStart, // or may be of any kind if no class was specified // - BLOB fields are NEVER transmitted here, even if ForceBlobTransfert=TRUE // - if CustomFields is left void, the simple fields will be used; otherwise, // you can specify your own set of fields to be transmitted when SendData=TRUE // (including BLOBs, even if they will be Base64-encoded within JSON content) - // CustomFields could be computed by TSQLRecordProperties.FieldBitsFromCSV() // or TSQLRecordProperties.FieldBitsFromRawUTF8(), or by setting ALL_FIELDS // - this method will always compute and send TCreateTime/TModTime fields function Add(Value: TSQLRecord; SendData: boolean; ForceID: boolean=false; const CustomFields: TSQLFieldBits=[]; DoNotAutoComputeFields: boolean=false): integer; /// update a member in current BATCH sequence // - work in BATCH mode: nothing is sent to the server until BatchSend call // - returns the corresponding index in the current BATCH sequence, -1 on error // - Value class MUST match the TSQLRecordClass used at BatchStart, // or may be of any kind if no class was specified // - BLOB fields are NEVER transmitted here, even if ForceBlobTransfert=TRUE // - if Value has an opened FillPrepare() mapping, only the mapped fields // will be updated (and also ID and TModTime fields) - FillPrepareMany() is // not handled yet (all simple fields will be updated) // - if CustomFields is left void, the simple fields will be used, or the // fields retrieved via a previous FillPrepare() call; otherwise, you can // specify your own set of fields to be transmitted (including BLOBs, even // if they will be Base64-encoded within the JSON content) - CustomFields // could be computed by TSQLRecordProperties.FieldBitsFromCSV() // or TSQLRecordProperties.FieldBitsFromRawUTF8() // - this method will always compute and send any TModTime fields, unless // DoNotAutoComputeFields is set to true // - if not all fields are specified, will reset the cache entry associated // with this value, unless ForceCacheUpdate is TRUE function Update(Value: TSQLRecord; const CustomFields: TSQLFieldBits=[]; DoNotAutoComputeFields: boolean=false; ForceCacheUpdate: boolean=false): integer; overload; virtual; /// update a member in current BATCH sequence // - work in BATCH mode: nothing is sent to the server until BatchSend call // - is an overloaded method to Update(Value,FieldBitsFromCSV()) function Update(Value: TSQLRecord; const CustomCSVFields: RawUTF8; DoNotAutoComputeFields: boolean=false; ForceCacheUpdate: boolean=false): integer; overload; /// delete a member in current BATCH sequence // - work in BATCH mode: nothing is sent to the server until BatchSend call // - returns the corresponding index in the current BATCH sequence, -1 on error // - deleted record class is the TSQLRecordClass used at BatchStart() // call: it will fail if no class was specified for this BATCH sequence function Delete(ID: TID): integer; overload; /// delete a member in current BATCH sequence // - work in BATCH mode: nothing is sent to the server until BatchSend call // - returns the corresponding index in the current BATCH sequence, -1 on error // - with this overloaded method, the deleted record class is specified: // no TSQLRecordClass shall have been set at BatchStart() call function Delete(Table: TSQLRecordClass; ID: TID): integer; overload; /// allow to append some JSON content to the internal raw buffer // - could be used to emulate Add/Update/Delete // - FullRow=TRUE will increment the global Count function RawAppend(FullRow: boolean=true): TTextWriter; /// allow to append some JSON content to the internal raw buffer for a POST // - could be used to emulate Add() with an already pre-computed JSON object // - returns the corresponding index in the current BATCH sequence, -1 on error function RawAdd(const SentData: RawUTF8): integer; /// allow to append some JSON content to the internal raw buffer for a PUT // - could be used to emulate Update() with an already pre-computed JSON object // - returns the corresponding index in the current BATCH sequence, -1 on error function RawUpdate(const SentData: RawUTF8; ID: TID): integer; /// close a BATCH sequence started by Start method // - Data is ready to be supplied to TSQLRest.BatchSend() overloaded method // - will also notify the TSQLRest.Cache for all deleted IDs // - you should not have to call it in normal use cases function PrepareForSending(out Data: RawUTF8): boolean; virtual; /// read only access to the associated TSQLRest instance property Rest: TSQLRest read fRest; /// how many times Add() has been called for this BATCH process property AddCount: integer read fAddCount; /// how many times Update() has been called for this BATCH process property UpdateCount: integer read fUpdateCount; /// how many times Delete() has been called for this BATCH process property DeleteCount: integer read fDeleteCount; /// this event handler will be triggerred by each Add/Update/Delete method property OnWrite: TOnBatchWrite read fOnWrite write fOnWrite; published /// read only access to the main associated TSQLRecord class (if any) property Table: TSQLRecordClass read fTable; /// retrieve the current number of pending transactions in the BATCH sequence property Count: integer read GetCount; /// retrieve the current JSON size of pending transaction in the BATCH sequence property SizeBytes: cardinal read GetSizeBytes; end; /// thread-safe class to store a BATCH sequence of writing operations TSQLRestBatchLocked = class(TSQLRestBatch) protected fTix: Int64; fSafe: TSynLocker; fThreshold: integer; public /// initialize the BATCH instance constructor Create(aRest: TSQLRest; aTable: TSQLRecordClass; AutomaticTransactionPerRow: cardinal=0; Options: TSQLRestBatchOptions=[]; InternalBufferSize: cardinal=65536); override; /// finalize the BATCH instance destructor Destroy; override; /// reset the BATCH sequence so that you can re-use the same TSQLRestBatch procedure Reset(aTable: TSQLRecordClass; AutomaticTransactionPerRow: cardinal=0; Options: TSQLRestBatchOptions=[]); override; /// access to the locking methods of this instance // - use Safe.Lock/TryLock with a try ... finally Safe.Unlock block property Safe: TSynLocker read fSafe; /// property set to the current GetTickCount64 value when Reset is called property ResetTix: Int64 read fTix write fTix; /// may be used to store a number of rows to flush the content property Threshold: integer read fThreshold write fThreshold; end; /// root class for defining and mapping database records // - inherits a class from TSQLRecord, and add published properties to describe // the table columns (see TPropInfo for SQL and Delphi type mapping/conversion) // - this published properties can be auto-filled from TSQLTable answer with // FillPrepare() and FillRow(), or FillFrom() with TSQLTable or JSON data // - these published properties can be converted back into UTF-8 encoded SQL // source with GetSQLValues or GetSQLSet or into JSON format with GetJSONValues // - BLOB fields are decoded to auto-freeing TSQLRawBlob properties // - any published property defined as a T*ObjArray dynamic array storage // of persistents (via TJSONSerializer.RegisterObjArrayForJSON) will be freed // - consider inherit from TSQLRecordNoCase and TSQLRecordNoCaseExtended if // you expect regular NOCASE collation and smaller (but not standard JSON) // variant fields persistence TSQLRecord = class(TObject) { note that every TSQLRecord has an Instance size of 20 bytes for private and protected fields (such as fID or fProps e.g.) } protected /// used by FillPrepare() and corresponding Fill*() methods fFill: TSQLRecordFill; /// internal properties getters (using fProps data for speed) function GetHasBlob: boolean; function GetSimpleFieldCount: integer; function GetFillCurrentRow: integer; function GetFillReachedEnd: boolean; function GetTable: TSQLTable; protected fInternalState: cardinal; fID: TID; /// virtual class method to be overridden to register some custom properties // - do nothing by default, but allow inherited classes to define some // properties, by adding some TSQLPropInfo instances to Props.Fields list, // or calling Props.RegisterCustomFixedSizeRecordProperty() or // Props.RegisterCustomRTTIRecordProperty() methods // - can also be used to specify a custom text collation, by calling // Props.SetCustomCollationForAll() or SetCustomCollation() methods // - do not call RecordProps from here (e.g. by calling AddFilter*): it // woult trigger a stack overflow, since at this state Props is not stored - // but rather use InternalDefineModel class method class procedure InternalRegisterCustomProperties(Props: TSQLRecordProperties); virtual; /// virtual class method to be overridden to define some record-level modeling // - do nothing by default, but allow inherited classes to define some // process which will take place after TSQLRecordProperties initialization // - this may be the place e.g. to call AddFilter*() methods, if you do not // want those to be written "in stone", and not manually when creating the // TSQLModel instance, or to call Props.SetCustomCollationForAll class procedure InternalDefineModel(Props: TSQLRecordProperties); virtual; {$ifdef MSWINDOWS}{$ifdef HASINLINE} public {$endif}{$endif} /// trick to get the ID even in case of a sftID published property function GetID: TID; {$ifdef MSWINDOWS}{$ifdef HASINLINE}inline;{$endif}{$endif} /// trick to typecast the ID on 64-bit platform function GetIDAsPointer: pointer; {$ifdef MSWINDOWS}{$ifdef HASINLINE}inline;{$endif}{$endif} public /// direct access to the TSQLRecord properties from RTTI // - TSQLRecordProperties is faster than e.g. the class function FieldProp() // - use internal the unused vmtAutoTable VMT entry to fast retrieve of a // class variable which is unique for each class ("class var" is unique only // for the class within it is defined, and we need a var for each class: // so even Delphi XE syntax is not powerful enough for our purpose, and the // vmtAutoTable trick if very fast, and works with all versions of Delphi - // including 64-bit target) class function RecordProps: TSQLRecordProperties; {$ifdef FPC_OR_PUREPASCAL}{$ifdef HASINLINE}inline;{$endif}{$endif} /// the Table name in the database, associated with this TSQLRecord class // - 'TSQL' or 'TSQLRecord' chars are trimmed at the beginning of the ClassName // - or the ClassName is returned as is, if no 'TSQL' or 'TSQLRecord' at first // - is just a wrapper around RecordProps.SQLTableName class function SQLTableName: RawUTF8; {$ifdef HASINLINE}inline;{$endif} /// register a custom filter (transformation) or validate to the // TSQLRecord class for a specified field // - this will be used by TSQLRecord.Filter and TSQLRecord.Validate // methods (in default implementation) // - will raise an EModelException on failure // - this function is just a wrapper around RecordProps.AddFilterOrValidate class procedure AddFilterOrValidate(const aFieldName: RawUTF8; aFilter: TSynFilterOrValidate); /// register a TSynFilterTrim and a TSynValidateText filters so that // the specified fields, after space trimming, won't be void class procedure AddFilterNotVoidText(const aFieldNames: array of RawUTF8); /// register a TSynFilterTrim and a TSynValidateText filters so that // all text fields, after space trimming, won't be void // - will only affect RAWTEXT_FIELDS class procedure AddFilterNotVoidAllTextFields; /// protect several TSQLRecord local variable instances // - specified as localVariable/recordClass pairs // - is a wrapper around TAutoFree.Several(...) constructor // - be aware that it won't implement a full ARC memory model, but may be // just used to avoid writing some try ... finally blocks on local variables // - use with caution, only on well defined local scope // - you may write for instance: // ! var info: TSQLBlogInfo; // ! article: TSQLArticle; // ! comment: TSQLComment; // ! begin // ! TSQLRecord.AutoFree([ // avoid several try..finally // ! @info,TSQLBlogInfo, @article,TSQLArticle, @comment,TSQLComment]); // ! .... now you can use info, article or comment // ! end; // will call info.Free article.Free and comment.Free // - warning: under FPC, you should assign the result of this method to // a local IAutoFree variable, or use a with TSQLRecord.AutoFree() do // statement - see http://bugs.freepascal.org/view.php?id=26602 class function AutoFree(varClassPairs: array of pointer): IAutoFree; overload; /// protect one TSQLRecord local variable instance // - be aware that it won't implement a full ARC memory model, but may be // just used to avoid writing some try ... finally blocks on local variables // - use with caution, only on well defined local scope // - you may write for instance: // ! var info: TSQLBlogInfo; // ! begin // ! TSQLBlogInfo.AutoFree(info); // ! .... now you can use info // ! end; // will call info.Free // - warning: under FPC, you should assign the result of this method to // a local IAutoFree variable, or use a with TSQLRecord.AutoFree() do // statement - see http://bugs.freepascal.org/view.php?id=26602 class function AutoFree(var localVariable): IAutoFree; overload; /// read and protect one TSQLRecord local variable instance // - is a wrapper around TAutoFree.Create(localVariable,Create(Rest,ID)) // - be aware that it won't implement a full ARC memory model, but may be // just used to avoid writing some try ... finally blocks on local variables // - use with caution, only on well defined local scope // - warning: under FPC, you should assign the result of this method to // a local IAutoFree variable, or use a with TSQLRecord.AutoFree() do // statement - see http://bugs.freepascal.org/view.php?id=26602 class function AutoFree(var localVariable; Rest: TSQLRest; ID: TID): IAutoFree; overload; /// FillPrepare and protect one TSQLRecord local variable instance // - is a wrapper around TAutoFree.Create(localVariable,CreateAndFillPrepare(Rest,...)) // - be aware that it won't implement a full ARC memory model, but may be // just used to avoid writing some try ... finally blocks on local variables // - use with caution, only on well defined local scope // - warning: under FPC, you should assign the result of this method to // a local IAutoFree variable, or use a with TSQLRecord.AutoFree() do // statement - see http://bugs.freepascal.org/view.php?id=26602 class function AutoFree(var localVariable; Rest: TSQLRest; const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const; const aCustomFieldsCSV: RawUTF8=''): IAutoFree; overload; /// FillPrepare and protect one TSQLRecord local variable instance // - is a wrapper around TAutoFree.Create(localVariable,CreateAndFillPrepare(Rest,...)) // - be aware that it won't implement a full ARC memory model, but may be // just used to avoid writing some try ... finally blocks on local variables // - use with caution, only on well defined local scope // - warning: under FPC, you should assign the result of this method to // a local IAutoFree variable, or use a with TSQLRecord.AutoFree() do // statement - see http://bugs.freepascal.org/view.php?id=26602 class function AutoFree(var localVariable; Rest: TSQLRest; const FormatSQLWhere: RawUTF8; const ParamsSQLWhere,BoundsSQLWhere: array of const; const aCustomFieldsCSV: RawUTF8=''): IAutoFree; overload; /// get the captions to be used for this class // - if Action is nil, return the caption of the table name // - if Action is not nil, return the caption of this Action (lowercase left-trimed) // - return "string" type, i.e. UnicodeString for Delphi 2009+ // - internally call UnCamelCase() then System.LoadResStringTranslate() if available // - ForHint is set to TRUE when the record caption name is to be displayed inside // the popup hint of a button (i.e. the name must be fully qualified, not // the default short version) // - is not part of TSQLRecordProperties because has been declared as virtual class function CaptionName(Action: PRawUTF8=nil; ForHint: boolean=false): string; virtual; /// get the captions to be used for this class // - just a wrapper calling CaptionName() virtual method, from a ShortString pointer class function CaptionNameFromRTTI(Action: PShortString): string; /// virtual method called when the associated table is created in the database // - if FieldName is '', initialization regarding all fields must be made; // if FieldName is specified, initialization regarding this field must be processed // - override this method in order to initialize indexs or create default records // - by default, create indexes for all TRecordReference properties, and // for all TSQLRecord inherited properties (i.e. of sftID type, that is // an INTEGER field containing the ID of the pointing record) // - the options specified at CreateMissingTables() are passed to this method, // within the context of an opened DB transaction, in which missing tables // and fields have already been added // - is not part of TSQLRecordProperties because has been declared as virtual class procedure InitializeTable(Server: TSQLRestServer; const FieldName: RawUTF8; Options: TSQLInitializeTableOptions); virtual; /// filter/transform the specified fields values of the TSQLRecord instance // - by default, this will perform all TSynFilter as registered by // [RecordProps.]AddFilterOrValidate() // - inherited classes may add some custom filtering/transformation here, if // it's not needed nor mandatory to create a new TSynFilter class type: in // this case, the function has to return TRUE if the filtering took place, // and FALSE if any default registered TSynFilter must be processed // - the default aFields parameter will process all fields function Filter(const aFields: TSQLFieldBits=[0..MAX_SQLFIELDS-1]): boolean; overload; virtual; /// filter/transform the specified fields values of the TSQLRecord instance // - this version will call the overloaded Filter() method above // - return TRUE if all field names were correct and processed, FALSE otherwise function Filter(const aFields: array of RawUTF8): boolean; overload; /// validate the specified fields values of the current TSQLRecord instance // - by default, this will perform all TSynValidate as registered by // [RecordProps.]AddFilterOrValidate() // - it will also check if any UNIQUE field value won't be duplicated // - inherited classes may add some custom validation here, if it's not needed // nor mandatory to create a new TSynValidate class type: in this case, the // function has to return an explicit error message (as a generic VCL string) // if the custom validation failed, or '' if the validation was successful: // in this later case, all default registered TSynValidate are processed // - the default aFields parameter will process all fields // - if aInvalidFieldIndex is set, it will contain the first invalid field // index found // - caller SHOULD always call the Filter() method before calling Validate() function Validate(aRest: TSQLRest; const aFields: TSQLFieldBits=[0..MAX_SQLFIELDS-1]; aInvalidFieldIndex: PInteger=nil; aValidator: PSynValidate=nil): string; overload; virtual; /// validate the specified fields values of the current TSQLRecord instance // - this version will call the overloaded Validate() method above // - returns '' if all field names were correct and processed, or an // explicit error message (translated in the current language) on error // - if aInvalidFieldIndex is set, it will contain the first invalid field index function Validate(aRest: TSQLRest; const aFields: array of RawUTF8; aInvalidFieldIndex: PInteger=nil; aValidator: PSynValidate=nil): string; overload; /// filter (transform) then validate the specified fields values of the TSQLRecord // - this version will call the overloaded Filter() and Validate() methods // and display the faulty field name at the beginning of the error message // - returns true if all field names were correct and processed, or false // and an explicit error message (translated in the current language) on error function FilterAndValidate(aRest: TSQLRest; out aErrorMessage: string; const aFields: TSQLFieldBits=[0..MAX_SQLFIELDS-1]; aValidator: PSynValidate=nil): boolean; overload; /// filter (transform) then validate the specified fields values of the TSQLRecord // - this version will call the overloaded Filter() and Validate() methods // and return '' on validation success, or an error message with the faulty // field names at the beginning function FilterAndValidate(aRest: TSQLRest; const aFields: TSQLFieldBits=[0..MAX_SQLFIELDS-1]; aValidator: PSynValidate=nil): RawUTF8; overload; /// should modify the record content before writing to the Server // - this default implementation will update any sftModTime / TModTime, // sftCreateTime / TCreateTime and sftSessionUserID / TSessionUserID // properties content with the exact server time stamp // - you may override this method e.g. for custom calculated fields // - note that this is computed only on the Client side, before sending // back the content to the remote Server: therefore, TModTime / TCreateTime // fields are a pure client ORM feature - it won't work directly at REST level procedure ComputeFieldsBeforeWrite(aRest: TSQLRest; aOccasion: TSQLEvent); virtual; /// this constructor initializes the record // - auto-instanciate any TSQLRecordMany instance defined in published properties // - override this method if you want to use some internal objects (e.g. // TStringList or TCollection as published property) constructor Create; overload; virtual; /// this constructor initializes the record and set the simple fields // with the supplied values // - the aSimpleFields parameters must follow explicitely the order of // published properties of the aTable class, excepting the TSQLRawBlob and // TSQLRecordMany kind (i.e. only so called "simple fields") - in // particular, parent properties must appear first in the list // - the aSimpleFields must have exactly the same count of parameters as // there are "simple fields" in the published properties // - will raise an EORMException in case of wrong supplied values constructor Create(const aSimpleFields: array of const; aID: TID); overload; /// this constructor initializes the object as above, and fills its content // from a client or server connection // - if ForUpdate is true, the REST method is LOCK and not GET: it tries to lock // the corresponding record, then retrieve its content; caller has to call // UnLock() method after Value usage, to release the record constructor Create(aClient: TSQLRest; aID: TID; ForUpdate: boolean=false); overload; /// this constructor initializes the object and fills its content from a client // or server connection, from a TSQLRecord published property content // - is just a wrapper around Create(aClient,PtrInt(aPublishedRecord)) // or Create(aClient,aPublishedRecord.ID) // - a published TSQLRecord property is not a class instance, but a typecast to // TObject(RecordID) - you can also use its ID property // - if ForUpdate is true, the REST method is LOCK and not GET: it tries to lock // the corresponding record, then retrieve its content; caller has to call // UnLock() method after Value usage, to release the record constructor Create(aClient: TSQLRest; aPublishedRecord: TSQLRecord; ForUpdate: boolean=false); overload; /// this constructor initializes the object as above, and fills its content // from a client or server connection, using a specified WHERE clause // - the WHERE clause should use inlined parameters (like 'Name=:('Arnaud'):') // for better server speed - note that you can use FormatUTF8() as such: // ! aRec := TSQLMyRec.Create(Client,FormatUTF8('Salary>? AND Salary? AND Salarynil then // ! try // ! while aProd.FillOne do // ! // here e.g. aProd.Categories.Dest are instantied (and Categories.Source=aProd) // ! writeln(aProd.Name,' ',aProd.Owner,' ',aProd.Categories.Dest.Name,' ',aProd.Sizes.Dest.Name); // ! // you may also use aProd.FillTable to fill a grid, e.g. // ! // (do not forget to set aProd.FillTable.OwnerMustFree := false) // ! finally // ! aProd.Free; // will also free aProd.Categories/Sizes instances // ! end; // this will execute a JOINed SELECT statement similar to the following: // $ select p.*, c.*, s.* // $ from Product p, Category c, Categories cc, Size s, Sizes ss // $ where c.id=cc.dest and cc.source=p.id and // $ s.id=ss.dest and ss.source=p.id and // $ p.Owner='mark' and c.Name='for boy' and (s.Name='small' or s.Name='medium') // - you SHALL call explicitely the FillClose method before using any // methods of nested TSQLRecordMany instances which may override the Dest // instance content (e.g. ManySelect) to avoid any GPF // - the aFormatSQLJoin clause will replace all '%' chars with the supplied // aParamsSQLJoin[] supplied values, and bind all '?' chars as bound // parameters with aBoundsSQLJoin[] values constructor CreateAndFillPrepareMany(aClient: TSQLRest; const aFormatSQLJoin: RawUTF8; const aParamsSQLJoin, aBoundsSQLJoin: array of const); /// this method create a clone of the current record, with same ID and properties // - copy all COPIABLE_FIELDS, i.e. all fields excluding tftMany (because // those fields don't contain any data, but a TSQLRecordMany instance // which allow to access to the pivot table data) // - you can override this method to allow custom copy of the object, // including (or not) published properties copy function CreateCopy: TSQLRecord; overload; virtual; /// this method create a clone of the current record, with same ID and properties // - overloaded method to copy the specified properties function CreateCopy(const CustomFields: TSQLFieldBits): TSQLRecord; overload; /// set the bits corresponding to non-void (0,'') copiable fields function GetNonVoidFields: TSQLFieldBits; /// release the associated memory // - in particular, release all TSQLRecordMany instance created by the // constructor of this TSQLRecord destructor Destroy; override; /// return the UTF-8 encoded SQL source to create the table containing the // published fields of a TSQLRecord child // - a 'ID INTEGER PRIMARY KEY' field is always created first (mapping // SQLite3 RowID) // - AnsiString are created as TEXT COLLATE NOCASE (fast SQLite3 7bits compare) // - RawUnicode and RawUTF8 are created as TEXT COLLATE SYSTEMNOCASE // (i.e. use our fast UTF8IComp() for comparaison) // - TDateTime are created as TEXT COLLATE ISO8601 // (which calls our very fast ISO TEXT to Int64 conversion routine) // - an individual bit set in UniqueField forces the corresponding field to // be marked as UNIQUE (an unique index is automaticaly created on the specified // column); use TSQLModel fIsUnique[] array, which set the bits values // to 1 if a property field was published with "stored AS_UNIQUE" // (i.e. "stored false") // - this method will handle TSQLRecordFTS* classes like FTS* virtual tables, // TSQLRecordRTree as RTREE virtual table, and TSQLRecordVirtualTable*ID // classes as corresponding Delphi designed virtual tables // - is not part of TSQLRecordProperties because has been declared as virtual // so that you could specify a custom SQL statement, per TSQLRecord type // - anyway, don't call this method directly, but use TSQLModel.GetSQLCreate() // - the aModel parameter is used to retrieve the Virtual Table module name, // and can be ignored for regular (not virtual) tables class function GetSQLCreate(aModel: TSQLModel): RawUTF8; virtual; /// return the Class Type of the current TSQLRecord function RecordClass: TSQLRecordClass; {$ifdef PUREPASCAL} {$ifdef HASINLINE}inline;{$endif} {$endif} /// return the RTTI property information for this record function ClassProp: PClassProp; {$ifdef PUREPASCAL} {$ifdef HASINLINE}inline;{$endif} {$endif} /// return the TRecordReference Int64 value pointing to this record function RecordReference(Model: TSQLModel): TRecordReference; /// return the UTF-8 encoded SQL source to INSERT the values contained // in the current published fields of a TSQLRecord child // - only simple fields name (i.e. not TSQLRawBlob/TSQLRecordMany) are updated: // BLOB fields are ignored (use direct update via dedicated methods instead) // - format is '(COL1, COL2) VALUES ('VAL1', 'VAL2')' if some column was ignored // (BLOB e.g.) // - format is 'VALUES ('VAL1', 'VAL2')' if all columns values are available // - is not used by the ORM (do not use prepared statements) - only here // for conveniency function GetSQLValues: RawUTF8; /// return the UTF-8 encoded SQL source to UPDATE the values contained // in the current published fields of a TSQLRecord child // - only simple fields name (i.e. not TSQLRawBlob/TSQLRecordMany) are retrieved: // BLOB fields are ignored (use direct access via dedicated methods instead) // - format is 'COL1='VAL1', COL2='VAL2'' // - is not used by the ORM (do not use prepared statements) - only here // for conveniency function GetSQLSet: RawUTF8; /// return the UTF-8 encoded JSON objects for the values of this TSQLRecord // - layout and fields should have been set at TJSONSerializer construction: // to append some content to an existing TJsonSerializer, call the // AppendAsJsonObject() method procedure GetJSONValues(W : TJSONSerializer); overload; /// return the UTF-8 encoded JSON objects for the values of this TSQLRecord // - the JSON buffer will be finalized if needed (e.g. non expanded mode), // and the supplied TJSONSerializer instance will be freed by this method // - layout and fields should have been set at TJSONSerializer construction: // to append some content to an existing TJsonSerializer, call the // AppendAsJsonObject() method procedure GetJSONValuesAndFree(JSON : TJSONSerializer); overload; /// return the UTF-8 encoded JSON objects for the values contained // in the current published fields of a TSQLRecord child // - only simple fields (i.e. not TSQLRawBlob/TSQLRecordMany) are retrieved: // BLOB fields are ignored (use direct access via dedicated methods instead) // - if Expand is true, JSON data is an object, for direct use with any Ajax or .NET client: // ! {"col1":val11,"col2":"val12"} // - if Expand is false, JSON data is serialized (as used in TSQLTableJSON) // ! { "fieldCount":1,"values":["col1","col2",val11,"val12",val21,..] } // - if withID is true, then the first ID field value is included // - you can customize SQLRecordOptions, e.g. if sftObject/sftBlobDynArray // property instance will be serialized as a JSON object or array, not a // JSON string (which is the default, as expected by the database storage), // or if an "ID_str" string field should be added for JavaScript procedure GetJSONValues(JSON: TStream; Expand, withID: boolean; Occasion: TSQLOccasion; SQLRecordOptions: TJSONSerializerSQLRecordOptions=[]); overload; /// same as overloaded GetJSONValues(), but returning result into a RawUTF8 // - if UsingStream is not set, it will use a temporary THeapMemoryStream instance function GetJSONValues(Expand, withID: boolean; Occasion: TSQLOccasion; UsingStream: TCustomMemoryStream=nil; SQLRecordOptions: TJSONSerializerSQLRecordOptions=[]): RawUTF8; overload; /// same as overloaded GetJSONValues(), but allowing to set the fields to // be retrieved, and returning result into a RawUTF8 function GetJSONValues(Expand, withID: boolean; const Fields: TSQLFieldBits; SQLRecordOptions: TJSONSerializerSQLRecordOptions=[]): RawUTF8; overload; /// same as overloaded GetJSONValues(), but allowing to set the fields to // be retrieved, and returning result into a RawUTF8 function GetJSONValues(Expand, withID: boolean; const FieldsCSV: RawUTF8; SQLRecordOptions: TJSONSerializerSQLRecordOptions=[]): RawUTF8; overload; /// will append the record fields as an expanded JSON object // - GetJsonValues() will expect a dedicated TJSONSerializer, whereas this // method will add the JSON object directly to any TJSONSerializer // - by default, will append the simple fields, unless the Fields optional // parameter is customized to a non void value procedure AppendAsJsonObject(W: TJSONSerializer; Fields: TSQLFieldBits=[]); /// will append all the FillPrepare() records as an expanded JSON array // - generates '[{rec1},{rec2},...]' using a loop similar to: // ! while FillOne do .. AppendJsonObject() .. // - if FieldName is set, the JSON array will be written as a JSON property, // i.e. surrounded as '"FieldName":[....],' - note the ',' at the end // - by default, will append the simple fields, unless the Fields optional // parameter is customized to a non void value // - see also TSQLRest.AppendListAsJsonArray for a high-level wrapper method procedure AppendFillAsJsonArray(const FieldName: RawUTF8; W: TJSONSerializer; Fields: TSQLFieldBits=[]); {$ifndef NOVARIANTS} /// change TDocVariantData.Options for all variant published fields // - may be used to replace e.g. JSON_OPTIONS_FAST_EXTENDED by JSON_OPTIONS_FAST procedure ForceVariantFieldsOptions(aOptions: TDocVariantOptions=JSON_OPTIONS_FAST); {$endif} /// write the field values into the binary buffer // - won't write the ID field (should be stored before, with the Count e.g.) procedure GetBinaryValues(W: TFileBufferWriter); overload; /// write the field values into the binary buffer // - won't write the ID field (should be stored before, with the Count e.g.) procedure GetBinaryValues(W: TFileBufferWriter; const aFields: TSQLFieldBits); overload; /// write the simple field values (excluding ID) into the binary buffer procedure GetBinaryValuesSimpleFields(W: TFileBufferWriter); /// set the field values from a binary buffer // - won't read the ID field (should be read before, with the Count e.g.) // - PEnd should point just after the P input buffer, to avoid buffer overflow // - returns true on success, or false in case of invalid content in P^ e.g. // - P is updated to the next pending content after the read values function SetBinaryValues(var P: PAnsiChar; PEnd: PAnsiChar): Boolean; /// set the simple field values from a binary buffer // - won't read the ID field (should be read before, with the Count e.g.) // - PEnd should point just after the P input buffer, to avoid buffer overflow // - returns true on success, or false in case of invalid content in P^ e.g. // - P is updated to the next pending content after the read values, function SetBinaryValuesSimpleFields(var P: PAnsiChar; PEnd: PAnsiChar): Boolean; /// write the record fields into RawByteString a binary buffer // - same as GetBinaryValues(), but also writing the ID field first function GetBinary: RawByteString; /// set the record fields from a binary buffer saved by GetBinary() // - same as SetBinaryValues(), but also reading the ID field first // - PEnd should point to the end of the P input buffer, to avoid any overflow function SetBinary(P,PEnd: PAnsiChar): Boolean; overload; /// set the record fields from a binary buffer saved by GetBinary() // - same as SetBinaryValues(), but also reading the ID field first function SetBinary(const binary: RawByteString): Boolean; overload; /// set all field values from a supplied array of TSQLVar values // - Values[] array must match the RecordProps.Field[] order: will return // false if the Values[].VType does not match RecordProps.FieldType[] function SetFieldSQLVars(const Values: TSQLVarDynArray): boolean; /// retrieve a field value from a given property name, as encoded UTF-8 text // - you should use strong typing and direct property access, following // the ORM approach of the framework; but in some cases (a custom Grid // display, for instance), it could be useful to have this method available // - will return '' in case of wrong property name // - BLOB and dynamic array fields are returned as '\uFFF0base64encodedbinary' function GetFieldValue(const PropName: RawUTF8): RawUTF8; /// set a field value of a given property name, from some encoded UTF-8 text // - you should use strong typing and direct property access, following // the ORM approach of the framework; but in some cases (a custom Grid // display, for instance), it could be useful to have this method available // - won't do anything in case of wrong property name // - expect BLOB and dynamic array fields encoded as SQlite3 BLOB literals // ("x'01234'" e.g.) or '\uFFF0base64encodedbinary' procedure SetFieldValue(const PropName: RawUTF8; Value: PUTF8Char); {$ifndef NOVARIANTS} /// retrieve the record content as a TDocVariant custom variant object function GetAsDocVariant(withID: boolean; const withFields: TSQLFieldBits; options: PDocVariantOptions=nil; replaceRowIDWithID: boolean=false): variant; overload; {$ifdef HASINLINE}inline;{$endif} /// retrieve the record content as a TDocVariant custom variant object procedure GetAsDocVariant(withID: boolean; const withFields: TSQLFieldBits; var result: variant; options: PDocVariantOptions=nil; ReplaceRowIDWithID: boolean=false); overload; /// retrieve the simple record content as a TDocVariant custom variant object function GetSimpleFieldsAsDocVariant(withID: boolean=true; options: PDocVariantOptions=nil): variant; /// retrieve the published property value into a Variant // - will set the Variant type to the best matching kind according to the // property type // - will return a null variant in case of wrong property name // - BLOB fields are returned as SQlite3 BLOB literals ("x'01234'" e.g.) // - dynamic array fields are returned as a Variant array function GetFieldVariant(const PropName: string): Variant; /// set the published property value from a Variant value // - will convert from the variant type into UTF-8 text before setting the // value (so will work with any kind of Variant) // - won't do anything in case of wrong property name // - expect BLOB fields encoded as SQlite3 BLOB literals ("x'01234'" e.g.) procedure SetFieldVariant(const PropName: string; const Source: Variant); {$endif} /// prepare to get values from a TSQLTable result // - then call FillRow(1..Table.RowCount) to get any row value // - or you can also loop through all rows with // ! while Rec.FillOne do // ! dosomethingwith(Rec); // - the specified TSQLTable is stored in an internal fTable protected field // - set aCheckTableName if you want e.g. the Field Names from the Table // any pending 'TableName.' trimmed before matching to the current record procedure FillPrepare(Table: TSQLTable; aCheckTableName: TSQLCheckTableName=ctnNoCheck); overload; /// prepare to get values from a SQL where statement // - returns true in case of success, false in case of an error during SQL request // - then call FillRow(1..Table.RowCount) to get any row value // - or you can also loop through all rows with // ! while Rec.FillOne do // ! dosomethingwith(Rec); // - a temporary TSQLTable is created then stored in an internal fTable protected field // - if aSQLWhere is left to '', all rows are retrieved as fast as possible // (e.g. by-passing SQLite3 virtual table modules for external databases) // - the WHERE clause should use inlined parameters (like 'Name=:('Arnaud'):') // for better server speed - note that you can use FormatUTF8() as such: // ! aRec.FillPrepare(Client,FormatUTF8('Salary>? AND Salary ... procedure GetHtmlTable(Dest: TTextWriter); overload; /// save the table as a
content function GetHtmlTable(const Header: RawUTF8=''#10): RawUTF8; overload; /// get the Field index of a FieldName // - return -1 if not found, index (0..FieldCount-1) if found function FieldIndex(FieldName: PUTF8Char): integer; overload; /// get the Field index of a FieldName // - return -1 if not found, index (0..FieldCount-1) if found function FieldIndex(const FieldName: RawUTF8): integer; overload; {$ifdef HASINLINE}inline;{$endif} /// get the Field index of a FieldName // - raise an ESQLTableException if not found, index (0..FieldCount-1) if found function FieldIndexExisting(const FieldName: RawUTF8): integer; overload; /// get the Field indexes of several Field names // - could be used to speed-up field access in a TSQLTable loop, avoiding // a FieldIndex(aFieldName) lookup for each value // - returns the number of matching Field names // - set -1 in FieldIndexes[]^ if not found, index (0..FieldCount-1) if found function FieldIndex(const FieldNames: array of RawUTF8; const FieldIndexes: array of PInteger): integer; overload; /// get the Field indexes of several Field names // - raise an ESQLTableException if not found // - set FieldIndexes[]^ to the index (0..FieldCount-1) if found // - could be used to speed-up field access in a TSQLTable loop, avoiding // a FieldIndex(aFieldName) lookup for each value, as such: //! list := TSQLTableJSON.Create('',pointer(json),length(json)); //! list.FieldIndexExisting( //! ['FirstName','LastName','YearOfBirth','YearOfDeath','RowID','Data'], //! [@FirstName,@LastName,@YearOfBirth,@YearOfDeath,@RowID,@Data]); //! for i := 1 to list.RowCount do begin //! Check(list.Get(i,FirstName)<>nil); //! Check(list.Get(i,LastName)<>nil); //! Check(list.GetAsInteger(i,YearOfBirth)<10000); procedure FieldIndexExisting(const FieldNames: array of RawUTF8; const FieldIndexes: array of PInteger); overload; /// retrieve all field names as a RawUTF8 dynamic array function FieldNames: TRawUTF8DynArray; /// get the Field content (encoded as UTF-8 text) from a property name // - return nil if not found function FieldValue(const FieldName: RawUTF8; Row: integer): PUTF8Char; {$ifdef HASINLINE}inline;{$endif} /// sort result Rows, according to a specific field // - default is sorting by ascending order (Asc=true) // - you can specify a Row index to be updated during the sort in PCurrentRow // - sort is very fast, even for huge tables (more faster than any indexed // SQL query): 500,000 rows are sorted instantly // - this optimized sort implementation does the comparison first by the // designed field, and, if the field value is identical, the ID value is // used (it will therefore sort by time all identical values) procedure SortFields(Field: integer; Asc: boolean=true; PCurrentRow: PInteger=nil; FieldType: TSQLFieldType=sftUnknown; CustomCompare: TUTF8Compare=nil); overload; /// sort result Rows, according to a specific field // - overloaded method allowing to specify the field by its name procedure SortFields(const FieldName: RawUTF8; Asc: boolean=true; PCurrentRow: PInteger=nil; FieldType: TSQLFieldType=sftUnknown; CustomCompare: TUTF8Compare=nil); overload; /// sort result Rows, according to some specific fields // - is able to make multi-field sort // - both Fields[] and Asc[] arrays should have the same count, otherwise // default Asc[]=true value will be assumed // - set any Fields[]=-1 to identify the ID column (even if is hidden) // - if CustomCompare=[], which use the default comparison function for the // field type, unless you set as many custom comparison function items // as in the Fields[] and Asc[] parameters procedure SortFields(const Fields: array of integer; const Asc: array of boolean; const CustomCompare: array of TUTF8Compare); overload; /// sort result Rows, according to the Bits set to 1 first procedure SortBitsFirst(var Bits); /// guess the field type from first non null data row // - if QueryTables[] are set, exact field type and enumerate TypeInfo() is // retrieved from the Delphi RTTI; otherwise, get from the cells content // - return sftUnknown is all data fields are null // - sftBlob is returned if the field is encoded as SQLite3 BLOB literals // (X'53514C697465' e.g.) // - since TSQLTable data is PUTF8Char, string type is sftUTF8Text only function FieldType(Field: integer): TSQLFieldType; overload; /// guess the field type from first non null data row // - if QueryTables[] are set, exact field type and (enumerate) TypeInfo() is // retrieved from the Delphi RTTI; otherwise, get from the cells content // - return sftUnknown is all data fields are null // - sftBlob is returned if the field is encoded as SQLite3 BLOB literals // (X'53514C697465' e.g.) // - since TSQLTable data is PUTF8Char, string type is sftUTF8Text only function FieldType(Field: integer; out FieldTypeInfo: PSQLTableFieldType): TSQLFieldType; overload; /// get the appropriate Sort comparison function for a field, // nil if not available (bad field index or field is blob) // - field type is guessed from first data row function SortCompare(Field: integer): TUTF8Compare; /// get the mean of characters length of all fields // - the character length is for the first line of text only (stop counting // at every newline character, i.e. #10 or #13 char) // - return the sum of all mean of character lengths function CalculateFieldLengthMean(var aResult: TIntegerDynArray; FromDisplay: boolean=false): integer; /// get the mean of characters length of this field // - the character length is for the first line of text only (stop counting // at every newline character, i.e. #10 or #13 char) // - very fast: calculated only once for all fields function FieldLengthMean(Field: integer): cardinal; /// get the sum of all mean of characters length of all fields // - very fast: calculated only once for all fields function FieldLengthMeanSum: cardinal; /// get the maximum number of characters of this field function FieldLengthMax(Field: integer; NeverReturnsZero: boolean=false): cardinal; /// get the record class (i.e. the table) associated to a field // - is nil if this table has no QueryTables property // - very fast: calculated only once for all fields function FieldTable(Field: integer): TSQLRecordClass; /// force the mean of characters length for every field // - expect as many parameters as fields in this table // - override internal fFieldLengthMean[] and fFieldLengthMeanSum values procedure SetFieldLengthMean(const Lengths: array of cardinal); /// set the exact type of a given field // - by default, column types and sizes will be retrieved from JSON content // from first row, or all rows if FieldTypeIntegerDetectionOnAllRows is set // - you can define a specific type for a given column, and optionally // a maximum column size // - FieldTypeInfo can be specified for sets or enumerations, as such: // ! aTable.SetFieldType(0,sftEnumerate,TypeInfo(TEnumSample)); // ! aTable.SetFieldType(1,sftSet,TypeInfo(TSetSamples)); // or for dynamic arrays procedure SetFieldType(Field: integer; FieldType: TSQLFieldType; FieldTypeInfo: pointer=nil; FieldSize: integer=-1; FieldTableIndex: integer=-1); overload; /// set the exact type of a given field // - by default, column types and sizes will be retrieved from JSON content // from first row, or all rows if FieldTypeIntegerDetectionOnAllRows is set // - you can define a specific type for a given column, and optionally // a maximum column size // - FieldTypeInfo can be specified for sets or enumerations, as such: // ! aTable.SetFieldType('Sample',sftEnumerate,TypeInfo(TEnumSample)); // ! aTable.SetFieldType('Samples',sftSet,TypeInfo(TSetSamples)); procedure SetFieldType(const FieldName: RawUTF8; FieldType: TSQLFieldType; FieldTypeInfo: pointer=nil; FieldSize: integer=-1); overload; /// set the exact type of all fields, from the DB-like information procedure SetFieldTypes(const DBTypes: TSQLDBFieldTypeDynArray); /// increase a particular Field Length Mean value // - to be used to customize the field appareance (e.g. for adding of left // checkbox for Marked[] fields) procedure FieldLengthMeanIncrease(aField, aIncrease: integer); /// copy the parameters of a TSQLTable into this instance // - the fResults remain in the source TSQLTable: source TSQLTable has not to // be destroyed before this TSQLTable procedure Assign(source: TSQLTable); /// search a text value inside the table data in a specified field // - the text value must already be uppercased 7-bits ANSI encoded // - return the Row on success, 0 on error // - search only in the content of FieldIndex data // - you can specify a Soundex pronunciation to use, or leave as sndxNone for // standard case insensitive character match; aUpperValue can optional // indicate a Soundex search, by predeceding the searched text with % for // English, %% for French or %%% for Spanish (only works with WinAnsi // char set - i.e. code page 1252) // - if UnicodeComparison is set to TRUE, search will use low-level Windows // API for Unicode-level conversion - it will be much slower, but accurate // for the whole range of UTF-8 encoding // - if UnicodeComparison is left to FALSE, UTF-8 decoding will be done only // if necessary: it will work only with standard western-occidental alphabet // (i.e. WinAnsi - code page 1252), but it will be very fast function SearchValue(const UpperValue: RawUTF8; StartRow, FieldIndex: integer; Client: TObject; Lang: TSynSoundExPronunciation=sndxNone; UnicodeComparison: boolean=false): integer; overload; /// search a text value inside the table data in all fields // - the text value must already be uppercased 7-bits ANSI encoded // - return the Row on success, 0 on error // - search on all fields, returning field found in FieldIndex (if not nil) // - you can specify a Soundex pronunciation to use, or leave as sndxNone for // standard case insensitive character match; aUpperValue can optional // indicate a Soundex search, by predeceding the searched text with % for // English, %% for French or %%% for Spanish (only works with WinAnsi // char set - i.e. code page 1252) // - if UnicodeComparison is set to TRUE, search will use low-level Windows // API for Unicode-level conversion - it will be much slower, but accurate // for the whole range of UTF-8 encoding // - if UnicodeComparison is left to FALSE, UTF-8 decoding will be done only // if necessary: it will work only with standard western-occidental alphabet // (i.e. WinAnsi - code page 1252), but it will be very fast function SearchValue(const UpperValue: RawUTF8; StartRow: integer; FieldIndex: PInteger; Client: TObject; Lang: TSynSoundExPronunciation=sndxNone; UnicodeComparison: boolean=false): integer; overload; /// search for a value inside the raw table data, using UTF8IComp/StrComp() // - returns 0 if not found, or the matching Row number otherwise function SearchFieldEquals(const Value: RawUTF8; FieldIndex: integer; StartRow: integer=1; CaseSensitive: boolean=false): integer; overload; /// search for a value inside the raw table data, using UTF8IComp/StrComp() // - returns 0 if not found, or the matching Row number otherwise function SearchFieldEquals(Value: PUTF8Char; FieldIndex: integer; StartRow: integer=1; CaseSensitive: boolean=false): integer; overload; /// search for a value inside the raw table data, using IdemPChar() // - returns 0 if not found, or the matching Row number otherwise function SearchFieldIdemPChar(const Value: RawUTF8; FieldIndex: integer; StartRow: integer=1): integer; /// search for a value using O(log(n)) binary search of a sorted field // - here the content should have been previously sorted via Sort(), // or CustomCompare should be defined, otherwise the SearchFieldEquals() // slower O(n) method is called // - returns 0 if not found, or the matching Row number otherwise function SearchFieldSorted(const Value: RawUTF8; FieldIndex: integer; CustomCompare: TUTF8Compare=nil): integer; overload; /// search for a value using O(log(n)) binary search of a sorted field // - here the content should have been previously sorted via Sort(), // or CustomCompare should be defined, otherwise the SearchFieldEquals() // slower O(n) method is called // - returns 0 if not found, or the matching Row number otherwise function SearchFieldSorted(Value: PUTF8Char; FieldIndex: integer; CustomCompare: TUTF8Compare=nil): integer; overload; /// if the ID column is available, hides it from fResults[] // - useful for simplier UI, with a hidden ID field // - use IDColumnHiddenValue() to get the ID of a specific row // - return true is ID was succesfully hidden, false if not possible function IDColumnHide: boolean; /// return the (previously hidden) ID value, 0 on error function IDColumnHiddenValue(Row: integer): TID; /// return all (previously hidden) ID values procedure IDColumnHiddenValues(var IDs: TIDDynArray); /// get all IDs where individual bit in Bits are set procedure IDArrayFromBits(const Bits; var IDs: TIDDynArray); /// get all individual bit in Bits corresponding to the supplied IDs // - warning: IDs integer array will be sorted within this method call procedure IDArrayToBits(var Bits; var IDs: TIDDynArray); /// get the Row index corresponding to a specified ID // - return the Row number, from 1 to RowCount // - return RowCount (last row index) if this ID was not found or no // ID field is available, unless aNotFoundMinusOne is set, and then -1 is // returned function RowFromID(aID: TID; aNotFoundMinusOne: boolean=false): integer; /// delete the specified data Row from the Table // - only overwrite the internal fResults[] pointers, don't free any memory, // nor modify the internal DataSet function DeleteRow(Row: integer): boolean; /// delete the specified Column text from the Table // - don't delete the Column: only delete UTF-8 text in all rows for this field function DeleteColumnValues(Field: integer): boolean; /// retrieve QueryTables[0], if existing function QueryRecordType: TSQLRecordClass; /// create a new TSQLRecord instance for a specific Table // - a void TSQLRecord instance is created, ready to be filled // - use the specified TSQLRecord class or create one instance // of the first associated record class (from internal QueryTables[]) // - the returned records will be managed by this TSQLTable: they will be // freed when the TSQLTable is destroyed: you don't need to make a // try..finally..Free..end block with them function NewRecord(RecordType: TSQLRecordClass=nil): TSQLRecord; /// create a TObjectList with TSQLRecord instances corresponding to this // TSQLTable result set // - use the specified TSQLRecord class or create instances // of the first associated record class (from internal QueryTables[]) // - always returns an instance, even if the TSQLTable is nil or void function ToObjectList(RecordType: TSQLRecordClass=nil): TObjectList; overload; /// fill an existing TObjectList with TSQLRecord instances corresponding // to this TSQLTable result set // - use the specified TSQLRecord class or create instances // of the first associated record class (from internal QueryTables[]) procedure ToObjectList(DestList: TObjectList; RecordType: TSQLRecordClass=nil); overload; {$ifdef ISDELPHI2010} // Delphi 2009/2010 generics are buggy /// create a TObjectList with TSQLRecord instances corresponding // to this TSQLTable result set // - use the specified TSQLRecord class or create instances // of the first associated record class (from internal QueryTables[]) // - always returns an instance, even if the TSQLTable is nil or void function ToObjectList: TObjectList; overload; {$endif} /// fill an existing T*ObjArray variable with TSQLRecord instances // corresponding to this TSQLTable result set // - use the specified TSQLRecord class or create instances // of the first associated record class (from internal QueryTables[]) // - returns TRUE on success (even if ObjArray=[]), FALSE on error function ToObjArray(var ObjArray; RecordType: TSQLRecordClass=nil): boolean; /// after a TSQLTable has been initialized, this method can be called // one or more times to iterate through all data rows // - you shall call this method before calling FieldBuffer()/Field() methods // - return TRUE on success, with data ready to be retrieved by Field*() // - return FALSE if no more row is available (i.e. exceeded RowCount) // - if SeekFirst is TRUE, will put the cursor on the first row of results, // otherwise, it will fetch one row of data, to be called within a loop // - you can specify a variant instance (e.g. allocated on the stack) in // optional RowVariant parameter, to access field values using late binding // - typical use may be: // ! while TableCustomers.Step do // ! writeln(Field('name')); // - or, when using a variant and late-binding: // ! var customer: variant; // ! ... // ! while TableCustomers.Step(false,@customer) do // ! writeln(customer.Name); function Step(SeekFirst: boolean=false; RowVariant: PVariant=nil): boolean; /// read-only access to a particular field value, as UTF-8 encoded buffer // - raise an ESQLTableException if called outside valid Step() sequence // - similar to Get() method, but for the current Step function FieldBuffer(FieldIndex: Integer): PUTF8Char; overload; /// read-only access to a particular field value, as UTF-8 encoded buffer // - raise an ESQLTableException if called outside valid Step() sequence // - similar to Get() method, but for the current Step function FieldBuffer(const FieldName: RawUTF8): PUTF8Char; overload; /// read-only access to a particular field value, as Integer // - raise an ESQLTableException if called outside valid Step() sequence // - similar to GetAsInteger() method, but for the current Step function FieldAsInteger(FieldIndex: Integer): Int64; overload; {$ifdef HASINLINE}inline;{$endif} /// read-only access to a particular field value, as Integer // - raise an ESQLTableException if called outside valid Step() sequence // - similar to GetAsInteger() method, but for the current Step function FieldAsInteger(const FieldName: RawUTF8): Int64; overload; {$ifdef HASINLINE}inline;{$endif} /// read-only access to a particular field value, as floating-point value // - raise an ESQLTableException if called outside valid Step() sequence // - similar to GetAsFloat() method, but for the current Step function FieldAsFloat(FieldIndex: Integer): TSynExtended; overload; {$ifdef HASINLINE}inline;{$endif} /// read-only access to a particular field value, as floating-point value // - raise an ESQLTableException if called outside valid Step() sequence // - similar to GetAsFloat() method, but for the current Step function FieldAsFloat(const FieldName: RawUTF8): TSynExtended; overload; {$ifdef HASINLINE}inline;{$endif} /// read-only access to a particular field value, as RawUTF8 // - raise an ESQLTableException if called outside valid Step() sequence // - similar to GetU() method, but for the current Step function FieldAsRawUTF8(FieldIndex: Integer): RawUTF8; overload; {$ifdef HASINLINE}inline;{$endif} /// read-only access to a particular field value, as RawUTF8 // - raise an ESQLTableException if called outside valid Step() sequence // - similar to GetU() method, but for the current Step function FieldAsRawUTF8(const FieldName: RawUTF8): RawUTF8; overload; {$ifdef HASINLINE}inline;{$endif} /// read-only access to a particular field value, as VCL String // - raise an ESQLTableException if called outside valid Step() sequence // - similar to GetString() method, but for the current Step function FieldAsString(FieldIndex: Integer): String; overload; {$ifdef HASINLINE}inline;{$endif} /// read-only access to a particular field value, as VCL String // - raise an ESQLTableException if called outside valid Step() sequence // - similar to GetString() method, but for the current Step function FieldAsString(const FieldName: RawUTF8): String; overload; {$ifdef HASINLINE}inline;{$endif} {$ifndef NOVARIANTS} /// read-only access to a particular field value, as a variant // - raise an ESQLTableException if called outside valid Step() sequence // - will call GetVariant() method for appropriate data conversion function Field(FieldIndex: integer): variant; overload; /// read-only access to a particular field value, as a variant // - raise an ESQLTableException if called outside valid Step() sequence // - will call GetVariant() method for appropriate data conversion function Field(const FieldName: RawUTF8): variant; overload; {$endif} /// contains the associated record class on Query property QueryTables: TSQLRecordClassDynArray read fQueryTables; /// contains the associated SQL statement on Query property QuerySQL: RawUTF8 read fQuerySQL; /// returns the SQL Table name, guessed from the associated QuerySQL statement property QueryTableNameFromSQL: RawUTF8 read GetQueryTableNameFromSQL; /// read-only access to the number of data Rows in this table // - first row contains field name // - then 1..RowCount rows contain the data itself // - safely returns 0 if the TSQLTable instance is nil property RowCount: integer read GetRowCount; /// read-only access to the number of fields for each Row in this table property FieldCount: integer read fFieldCount; /// read-only access to the ID/RowID field index // - do not use this property if the ID column has been hidden, but // use IDColumnHiddenValue() method instead property FieldIndexID: integer read fFieldIndexID; /// read-only acccess to the current Row number, after a Step() call // - contains 0 if accessed outside valid Step() sequence call // - contains 1..RowCount after a valid Step() iteration property StepRow: integer read fStepRow; /// this property contains the internal state counter of the server database // when the data was retrieved from it // - can be used to check if retrieved data may be out of date property InternalState: cardinal read fInternalState write fInternalState; /// if the TSQLRecord is the owner of this table, i.e. if it must free it property OwnerMustFree: Boolean read fOwnerMustFree write fOwnerMustFree; /// by default, if field types are not set, only the content of the first // row will be checked, to make a difference between a sftInteger and sftFloat // - you can set this property to TRUE so that all non string rows will // be checked for the exact number precision // - note that the safest is to provide the column type, either by supplying // the TSQLRecord class, or by calling SetFieldType() overloaded methods property FieldTypeIntegerDetectionOnAllRows: boolean read fFieldTypeAllRows write fFieldTypeAllRows; /// used by GetJsonValues, GetHtmlTable and GetCSVValues methods // to export custom JSON content property OnExportValue: TOnSQLTableGetValue read fOnExportValue write fOnExportValue; end; {$ifndef NOVARIANTS} /// memory structure used for our TSQLTableRowVariant custom variant type // used to have direct access to TSQLTable content // - the associated TSQLTable must stay allocated as long as this variant // is used, otherwise random GPF issues may occur TSQLTableRowVariantData = packed record /// the custom variant type registered number VType: TVarType; VFiller: array[1..SizeOf(TVarData)-SizeOf(TVarType)-SizeOf(TSQLTable) -SizeOf(integer)] of byte; /// reference to the associated TSQLTable VTable: TSQLTable; /// the row number corresponding to this value // - equals -1 if should follow StepRow property value VRow: integer; end; /// pointer to the memory structure used for TSQLTableRowVariant storage PSQLTableRowVariantData = ^TSQLTableRowVariantData; /// a custom variant type used to have direct access to TSQLTable content // - use TSQLTable.Step(..,@Data) method to initialize such a Variant // - the variant members/fields are read-only by design // - the associated TSQLTable must stay allocated as long as this variant // is used, otherwise random GPF issues may occur TSQLTableRowVariant = class(TSynInvokeableVariantType) protected function IntGet(var Dest: TVarData; const Instance: TVarData; Name: PAnsiChar; NameLen: PtrInt): boolean; override; public /// customization of variant into JSON serialization procedure ToJSON(W: TTextWriter; const Value: variant; Escape: TTextWriterKind); override; /// handle type conversion to string procedure Cast(var Dest: TVarData; const Source: TVarData); override; /// handle type conversion to string procedure CastTo(var Dest: TVarData; const Source: TVarData; const AVarType: TVarType); override; end; EObjectVariant = ESynException; /// a custom variant type used to have direct access to object published properties // - TObjectVariant provides lazy-loading to object properties from a Variant // variable - which may be used with SynMustache or with late-binding // - warning: this custom variant is just a wrapper around an existing TObject // instance, which should remain available as long as the variant is used // - if you want a per-representation stateless variant, use ObjectToVariant() // which convert all properties into a TDocVariant, so may use more resource TObjectVariant = class(TSynInvokeableVariantType) protected function IntGet(var Dest: TVarData; const Instance: TVarData; Name: PAnsiChar; NameLen: PtrInt): boolean; override; function IntSet(const Instance, Value: TVarData; Name: PAnsiChar; NameLen: PtrInt): boolean; override; public /// initialize a new custom variant instance, wrapping the specified object // - warning: this custom variant is just a wrapper around an existing TObject // instance, which should remain available as long as the variant is used class procedure New(var V: Variant; Obj: TObject); /// will perform proper JSON serialization calling W.WriteObject() procedure ToJSON(W: TTextWriter; const Value: variant; Escape: TTextWriterKind); override; end; {$endif NOVARIANTS} /// store a read-only ORM result table from a JSON message // - the JSON data is parsed and unescaped in-place, to enhanced performance // and reduce resource consumption (mainly memory/heap fragmentation) // - is used by the ORM for TSQLRecord.FillPrepare/FillOne methods for // fast access to individual object values TSQLTableJSON = class(TSQLTable) protected /// used if a private copy of the JSON buffer is needed fPrivateCopy: RawUTF8; /// contains the pointers of start of every field value in JSONData fJSONResults: TPUTF8CharDynArray; /// contain the hash value of the last JSON data sent to ContentChanged() // - used to don't repeat parsing if data has not been changed fPrivateCopyHash: cardinal; /// fill the result table content from a JSON-formated Data message // - returns TRUE on parsing success // - returns FALSE if no valid JSON data was found // - update all content fields (fResults[], fRowCount, fFieldCount, etc...) // - expect the UTF-8 Buffer in either TSQLRequest.EngineExecute(DB,SQL,JSON) // format (i.e. expanded) or either in a not expanded format (as an // AJAX-ready array of objects) // - the conversion into PPUTF8CharArray is made inplace and is very fast // (no additional memory buffer is allocated) function ParseAndConvert(Buffer: PUTF8Char; BufferLen: integer): boolean; /// will check then set (if needed) internal fPrivateCopy[Hash] values // - returns TRUE if content changed (then fPrivateCopy+fPrivateCopyHash // will be updated using crc32c hash) function PrivateCopyChanged(aJSON: PUTF8Char; aLen: integer): boolean; public /// create the result table from a JSON-formated Data message // - the JSON data is parsed and formatted in-place // - please note that the supplied JSON buffer content will be changed: // if you want to reuse this JSON content, you shall make a private copy // before calling this constructor and you shall NOT release the corresponding // variable (fResults/JSONResults[] will point inside this memory buffer): // use instead the overloaded Create constructor expecting a const // aJSON: RawUTF8 parameter to allocate and hold a private copy of the data constructor Create(const aSQL: RawUTF8; JSONBuffer: PUTF8Char; JSONBufferLen: integer); reintroduce; overload; /// create the result table from a JSON-formated Data message // - the JSON data is parsed and formatted in-place, after having been // copied in the protected fPrivateCopy variable constructor Create(const aSQL, aJSON: RawUTF8); reintroduce; overload; /// create the result table from a JSON-formated Data message // - the JSON data is parsed and formatted in-place // - you can specify a set of TSQLRecord classes which will be used to // retrieve the column exact type information // - please note that the supplied JSON buffer content will be changed constructor CreateFromTables(const Tables: array of TSQLRecordClass; const aSQL: RawUTF8; JSONBuffer: PUTF8Char; JSONBufferLen: integer); reintroduce; overload; /// create the result table from a JSON-formated Data message // - you can specify a set of TSQLRecord classes which will be used to // retrieve the column exact type information // - the JSON data is parsed and formatted in-place, after having been // copied in the protected fPrivateCopy variable constructor CreateFromTables(const Tables: array of TSQLRecordClass; const aSQL, aJSON: RawUTF8); reintroduce; overload; /// initialize the result table from a JSON-formated Data message // - you can set the expected column types matching the results column layout // - the JSON data is parsed and formatted in-place constructor CreateWithColumnTypes(const ColumnTypes: array of TSQLFieldType; const aSQL: RawUTF8; JSONBuffer: PUTF8Char; JSONBufferLen: integer); reintroduce; overload; /// initialize the result table from a JSON-formated Data message // - you can set the expected column types matching the results column layout // - the JSON data is parsed and formatted in-place, after having been // copied in the protected fPrivateCopy variable constructor CreateWithColumnTypes(const ColumnTypes: array of TSQLFieldType; const aSQL, aJSON: RawUTF8); reintroduce; overload; /// update the result table content from a JSON-formated Data message // - return true on parsing success, false if no valid JSON data was found // - set Refreshed to true if the content changed // - update all content fields (fResults[], fRowCount, fFieldCount, etc...) // - call SortFields() or IDColumnHide if was already done for this TSQLTable // - the conversion into PPUTF8CharArray is made inplace and is very fast // (only one memory buffer is allocated for the whole data) function UpdateFrom(const aJSON: RawUTF8; var Refreshed: boolean; PCurrentRow: PInteger): boolean; /// the private copy of the processed data buffer // - available e.g. for Create constructor using aJSON parameter, // or after the UpdateFrom() process // - 16 more bytes will be allocated, to allow e.g. proper SSE4.2 process // - this buffer is not to be access directly: this won't be a valid JSON // content, but a processed buffer, on which fResults[] elements point to - // it will contain unescaped text and numerical values, ending with #0 property PrivateInternalCopy: RawUTF8 read fPrivateCopy; end; /// store a writable ORM result table, optionally read from a JSON message // - in respect to TSQLTableJSON, this class allows to modify field values, // and add some new fields on the fly, even joined from another TSQLTable TSQLTableWritable = class(TSQLTableJSON) protected fNewValues: TRawUTF8DynArray; fNewValuesCount: integer; fNewValuesInterning: TRawUTF8Interning; public /// modify a field value in-place, using a RawUTF8 text value procedure Update(Row,Field: integer; const Value: RawUTF8); overload; /// modify a field value in-place, using a RawUTF8 text value procedure Update(Row: integer; const FieldName, Value: RawUTF8); overload; {$ifndef NOVARIANTS} /// modify a field value in-place, using a variant value procedure Update(Row,Field: integer; const Value: variant); overload; /// modify a field value in-place, using a variant value procedure Update(Row: integer; const FieldName: RawUTF8; const Value: variant); overload; {$endif NOVARIANTS} /// define a new field to be stored in this table // - returns the internal index of the newly created field function AddField(const FieldName: RawUTF8): integer; overload; /// define a new field to be stored in this table // - returns the internal index of the newly created field function AddField(const FieldName: RawUTF8; FieldType: TSQLFieldType; FieldTypeInfo: pointer=nil; FieldSize: integer=-1): integer; overload; /// define a new field to be stored in this table // - returns the internal index of the newly created field function AddField(const FieldName: RawUTF8; FieldTable: TSQLRecordClass; const FieldTableName: RawUTF8=''): integer; overload; /// append/merge data from a secondary TSQLTable // - you should specify the primary keys on which the data rows are merged // - merged data will point to From.fResults[] content: so the From instance // should remain available as long as you use this TSQLTableWritable // - warning: will call From.SortFields(FromKeyField) for faster process procedure Join(From: TSQLTable; const FromKeyField, KeyField: RawUTF8); /// optionaly de-duplicate Update() values property NewValuesInterning: TRawUTF8Interning read fNewValuesInterning write fNewValuesInterning; /// how many values have been written via Update() overloaded methods // - is not used if NewValuesInterning was defined property NewValuesCount: integer read fNewValuesCount; end; PSQLLocks = ^TSQLLocks; /// used to store the locked record list, in a specified table // - the maximum count of the locked list if fixed to 512 by default, // which seems correct for common usage {$ifdef USERECORDWITHMETHODS}TSQLLocks = record {$else}TSQLLocks = object{$endif} public /// the number of locked records stored in this object Count: integer; /// contains the locked record ID // - an empty position is marked with 0 after UnLock() IDs: TIDDynArray; /// contains the time and date of the lock // - filled internally by the fast GetTickCount64() function (faster than // TDateTime or TSystemTime/GetLocalTime) // - used to purge to old entries - see PurgeOlderThan() method below Ticks64s: TInt64DynArray; /// lock a record, specified by its ID // - returns true on success, false if was already locked function Lock(aID: TID): boolean; /// unlock a record, specified by its ID // - returns true on success, false if was not already locked function UnLock(aID: TID): boolean; /// return true if a record, specified by its ID, is locked function isLocked(aID: TID): boolean; /// delete all the locked IDs entries, after a specified time // - to be used to release locked records if the client crashed // - default value is 30 minutes, which seems correct for common database usage procedure PurgeOlderThan(MinutesFromNow: cardinal=30); end; TSQLLocksDynArray = array of TSQLLocks; /// UI Query comparison operators // - these operators are e.g. used to mark or unmark some lines in a UI Grid // or for TInterfaceStub.ExpectsCount() methods TSQLQueryOperator = (qoNone, qoEqualTo, qoNotEqualTo, qoLessThan, qoLessThanOrEqualTo, qoGreaterThan, qoGreaterThanOrEqualTo, qoEqualToWithCase, qoNotEqualToWithCase, qoContains, qoBeginWith, qoSoundsLikeEnglish, qoSoundsLikeFrench, qoSoundsLikeSpanish); /// set of UI Query comparison operators TSQLQueryOperators = set of TSQLQueryOperator; /// User Interface Query action evaluation function prototype // - Operator is ord(TSQLQueryOperator) by default (i.e. for class function // TSQLRest.QueryIsTrue), or is a custom enumeration index for custom queries // (see TSQLQueryCustom.EnumIndex below, and TSQLRest.QueryAddCustom() method) // - for default Operator as ord(TSQLQueryOperator), qoContains and qoBeginWith // expect the Reference to be already uppercase // - qoEqualTo to qoGreaterThanOrEqualTo apply to all field kind (work with // either numeric either UTF-8 values) // - qoEqualToWithCase to qoSoundsLikeSpanish handle the field as UTF-8 text, // and make the comparison using the phonetic algorithm corresponding to // a language family // - for default Operator as ord(TSQLQueryOperator), qoSoundsLike* operators // expect the Reference not to be a PUTF8Char, but a typecast of a prepared // TSynSoundEx object instance (i.e. pointer(@SoundEx)) by the caller // - for custom query (from TSQLQueryCustom below), the event must // handle a special first call with Value=nil to select if this custom // Operator/Query is available for the specified aTable: in this case, // returning true indicates that this custom query is available for this table // - for custom query (from TSQLQueryCustom below), the event is called with // FieldType := TSQLFieldType(TSQLQueryCustom.EnumIndex)+64 TSQLQueryEvent = function(aTable: TSQLRecordClass; aID: TID; FieldType: TSQLFieldType; Value: PUTF8Char; Operator: integer; Reference: PUTF8Char): boolean of object; /// store one custom query parameters // - add custom query by using the TSQLRest.QueryAddCustom() method // - use EnumType^.GetCaption(EnumIndex) to retrieve the caption associated // to this custom query TSQLQueryCustom = record /// the associated enumeration type EnumType: PEnumType; /// the associated enumeration index in EnumType // - will be used to fill the Operator parameter for the Event call EnumIndex: integer; /// the associated evaluation Event handler // - the Operator parameter will be filled with the EnumIndex value Event: TSQLQueryEvent; /// User Interface Query action operators Operators: TSQLQueryOperators; end; /// standard actions for User Interface generation // - actNoAction for not defined action // - actMark (standard action) to Mark rows, i.e. display sub-menu with // actmarkAllEntries..actmarkOlderThanOneYear items // - actUnmarkAll (standard action) to UnMark all rows // - actmarkAllEntries to Mark all rows // - actmarkToday to Mark rows for today // - actmarkThisWeek to Mark rows for this Week // - actmarkThisMonth to Mark rows for this month // - actmarkYesterday to Mark rows for today // - actmarkLastWeek to Mark rows for Last Week // - actmarkLastMonth to Mark rows for Last month // - actmarkOlderThanOneDay to Mark rows After one day // - actmarkOlderThanOneWeek to Mark rows older than one week // - actmarkOlderThanOneMonth to Mark rows older than one month // - actmarkOlderThanSixMonths to Mark rows older than one half year // - actmarkOlderThanOneYear to Mark rows older than one year // - actmarkInverse to Inverse Mark values (ON->OFF, OFF->ON) TSQLAction = ( actNoAction, actMark, actUnmarkAll, actmarkAllEntries, actmarkToday, actmarkThisWeek, actmarkThisMonth, actmarkYesterday, actmarkLastWeek, actmarkLastMonth, actmarkOlderThanOneDay, actmarkOlderThanOneWeek, actmarkOlderThanOneMonth, actmarkOlderThanSixMonths, actmarkOlderThanOneYear, actmarkInverse); /// set of standard actions for User Interface generation TSQLActions = set of TSQLAction; /// how TSQLModel.URIMatch() will compare an URI // - will allow to make a difference about case-sensitivity TSQLRestModelMatch = (rmNoMatch, rmMatchExact, rmMatchWithCaseChange); /// defines the way the TDrawGrid is displayed by User Interface generation TSQLListLayout = (llLeft, llUp, llClient, llLeftUp); PSQLRibbonTabParameters = ^TSQLRibbonTabParameters; /// defines the settings for a Tab for User Interface generation // - used in mORMotToolBar.pas unit and TSQLModel.Create() overloaded method // - is defined as an object and not a record to allow easy inheritance for // proper per-application customization - see e.g. FileTables.pas in main demo TSQLRibbonTabParameters = object public /// the Table associated to this Tab Table: TSQLRecordClass; /// the caption of the Tab, to be translated on the screen // - by default, Tab name is taken from TSQLRecord.Caption(nil) method // - but you can override this value by setting a pointer to a resourcestring CustomCaption: PResStringRec; /// the hint type of the Tab, to be translated on the screen // - by default, hint will replace all %s instance by the Tab name, as taken // from TSQLRecord.Caption(nil) method // - but you can override this value by setting a pointer to a resourcestring CustomHint: PResStringRec; /// SQL fields to be displayed on the data lists // 'ID,' is always added at the beginning Select: RawUTF8; /// Tab Group number (index starting at 0) Group: integer; /// displayed field length mean, one char per field (A=1,Z=26) // - put lowercase character in order to center the field data FieldWidth: RawUTF8; /// if set, the ID column is shown ShowID: boolean; /// index of field used for displaying order OrderFieldIndex: integer; /// if set, the list is displayed in reverse order (i.e. decreasing) ReverseOrder: boolean; /// layout of the List, below the ribbon Layout: TSQLListLayout; /// width of the List, in percent of the client area // - default value (as stated in TSQLRibbonTab.Create) is 30% ListWidth: integer; /// by default, the detail are displayed as a report (TGDIPages component) // - set this property to true to customize the details display // - this property is ignored if Layout is llClient (i.e. details hidden) NoReport: boolean; /// by default, the screens are not refreshed automaticaly // - but you can enable the auto-refresh feature by setting this // property to TRUE, and creating a WM_TIMER message handler for the form, // which will handle both WM_TIMER_REFRESH_SCREEN and WM_TIMER_REFRESH_REPORT // timers: // !procedure TMainForm.WMRefreshTimer(var Msg: TWMTimer); // !begin // ! Ribbon.WMRefreshTimer(Msg); // !end; AutoRefresh: boolean; /// the associated hints to be displayed during the edition of this table // - every field hint must be separated by a '|' character // (e.g. 'The First Name|Its Company Name') // - all fields need to be listed in this text resource, even if it won't // be displayed on screen (enter a void item like ||) // - you can define some value by setting a pointer to a resourcestring EditFieldHints: PResStringRec; /// write hints above field during the edition of this table // - if EditExpandFieldHints is TRUE, the hints are written as text on the // dialog, just above the field content; by default, hints are displayed as // standard delayed popup when the mouse hover the field editor EditExpandFieldHints: boolean; /// the associated field name width (in pixels) to be used for creating // the edition dialog for this table EditFieldNameWidth: integer; /// a CSV list of field names to be hidden in both editor and default report // - handy to hide fields containing JSON data or the name of another // sftRecord/sftID/sftTID (i.e. TRecordReference/TSQLRecord props) fields // - list is to be separated by commas (e.g. "RunLogJSON,OrdersJSON" or // "ConnectionName") EditFieldNameToHideCSV: RawUTF8; /// if the default report must contain the edit field hints // - i.e. if the resourcestring pointed by EditFieldHints must be used // to display some text above every property value on the reports EditFieldHintsToReport: boolean; end; /// parent of all virtual classes // - you can define a plain TSQLRecord class as virtual if needed - e.g. // inheriting from TSQLRecordMany then calling VirtualTableExternalRegister() - // but using this class will seal its state to be virtual TSQLRecordVirtual = class(TSQLRecord); TSQLVirtualTable = class; /// class-reference type (metaclass) of a virtual table implementation TSQLVirtualTableClass = class of TSQLVirtualTable; /// pre-computed SQL statements for ORM operations for a given // TSQLModelRecordProperties instance TSQLModelRecordPropertiesSQL = record /// the simple field names in a SQL SELECT compatible format: 'COL1,COL2' e.g. // - format is // ! SQL.TableSimpleFields[withID: boolean; withTableName: boolean] // - returns '*' if no field is of TSQLRawBlob/TSQLRecordMany kind // - returns 'COL1,COL2' with all COL* set to simple field names if withID is false // - returns 'ID,COL1,COL2' with all COL* set to simple field names if withID is true // - returns 'Table.ID,Table.COL1,Table.COL2' if withTableName and withID are true TableSimpleFields: array[boolean,boolean] of RawUTF8; /// the SQL statement for reading all simple fields and RowID // - to be checked if we may safely call EngineList() SelectAllWithRowID: RawUTF8; /// the SQL statement for reading all simple fields with ID // - to be checked if we may safely call EngineList() SelectAllWithID: RawUTF8; /// the JOINed SQL statement for reading all fields with ID, including // nested TSQLRecord pre-allocated instances // - is '' if there is no nested TSQLRecord SelectAllJoined: RawUTF8; /// the updated simple fields exposed as 'COL1=?,COL2=?' // - excluding ID (but including TCreateTime fields - as used in // TSQLVirtualTableExternal.Update method) // - to be used e.g. for UPDATE statements UpdateSetSimple: RawUTF8; /// all updated fields exposed as 'COL1=?,COL2=?' // - excluding ID (but including TCreateTime fields - as used in // TSQLVirtualTableExternal.Update method) // - to be used e.g. for UPDATE statements UpdateSetAll: RawUTF8; /// all fields, excluding the ID field, exposed as 'COL1,COL2' // - to be used e.g. in TSQLVirtualTableExternal.Insert() InsertSet: RawUTF8; end; /// used by TSQLRecordPropertiesMapping.Options for custom field mapping // of a TSQLRecord on an external database process // - rpmAutoMapKeywordFields is set if MapAutoKeywordFields has been defined, // i.e. if field names which may conflict with a keyword should be // automatically mapped to a harmless symbol name // - rpmNoCreateMissingTable will bypass the existing table check, e.g. // to circumvent some specific DB provider or case sensitivity issue on tables // - rpmNoCreateMissingField will bypass the existing field check, e.g. // to circumvent some specific DB provider or case sensitivity issue on fields // - by default, check of missing field name will be case insensitive, unless // the rpmMissingFieldNameCaseSensitive option is set // - rpmQuoteFieldName will quote the field names - to be used e.g. with // FireBird in its Dialect 3 // - rpmClearPoolOnConnectionIssue will enable detecting connection loss TSQLRecordPropertiesMappingOptions = set of ( rpmAutoMapKeywordFields, rpmNoCreateMissingTable, rpmNoCreateMissingField, rpmMissingFieldNameCaseSensitive, rpmQuoteFieldName, rpmClearPoolOnConnectionIssue); /// pointer to external database properties for ORM // - is used e.g. to allow a "fluent" interface for MapField() method PSQLRecordPropertiesMapping = ^TSQLRecordPropertiesMapping; /// allow custom field mapping of a TSQLRecord // - used e.g. for external database process, including SQL generation, // as implemented in the mORMotDB.pas unit // - in end user code, mostly MapField/MapFields/Options methods // should be used, if needed as a fluent chained interface - other lower // level methods will be used by the framework internals {$ifdef USERECORDWITHMETHODS}TSQLRecordPropertiesMapping = record {$else}TSQLRecordPropertiesMapping = object{$endif} private /// storage of main read-only properties fProps: TSQLRecordProperties; fConnectionProperties: TObject; fTableName: RawUTF8; fRowIDFieldName: RawUTF8; fExtFieldNames: TRawUTF8DynArray; fExtFieldNamesUnQuotedSQL: TRawUTF8DynArray; fSQL: TSQLModelRecordPropertiesSQL; fFieldNamesMatchInternal: TSQLFieldBits; fOptions: TSQLRecordPropertiesMappingOptions; fAutoComputeSQL: boolean; fMappingVersion: cardinal; /// fill fRowIDFieldName/fSQL with the current information procedure ComputeSQL; public /// add a custom field mapping // - will re-compute all needed SQL statements as needed, and initialize // fSortedFieldsName[] and fSortedFieldsIndex[] internal sorted arrays // - can be used e.g. as // ! aModel.Props[TSQLMyExternal].ExternalDB.MapField('IntField','ExtField'); // - since it returns a PSQLRecordPropertiesMapping instance, you can // chain MapField().MapField().MapField(); calls to map several fields function MapField(const InternalName, ExternalName: RawUTF8): PSQLRecordPropertiesMapping; /// call this method to ensure that all fields won't conflict with a SQL // keyword for the given database // - by default, no check is performed: you can use this method to ensure // that all field names won't conflict with a SQL reserved keyword: such // fields will be identified and automatically mapped as fieldname_ // - can be used e.g. as // ! aModel.Props[TSQLMyExternal].ExternalDB. // ! MapField('IntField','ExtField'). // ! MapAutoKeywordFields; // - will in fact include the rpmAutoMapKeywordFields flag in Options // - since it returns a PSQLRecordPropertiesMapping instance, you can // chain MapField().MapAutoKeywordFields.MapField(); calls to map several fields function MapAutoKeywordFields: PSQLRecordPropertiesMapping; /// specify some advanced options for the field mapping // - see TSQLRecordPropertiesMappingOptions for all possibilities // - can be used e.g. as // ! aModel.Props[TSQLMyExternal].ExternalDB. // ! MapField('IntField','ExtField'). // ! SetOptions([rpmNoCreateMissingTable,rpmNoCreateMissingField]); // - since it returns a PSQLRecordPropertiesMapping instance, you can // chain MapField().SetOptions().MapField(); calls to map several fields function SetOptions(aOptions: TSQLRecordPropertiesMappingOptions): PSQLRecordPropertiesMapping; /// add several custom field mappings // - can be used e.g. as // ! aModel.Props[TSQLMyExternal].ExternalDB. // ! MapFields(['IntField1','ExtField1', 'IntField2','ExtField2']); // - will re-compute all needed SQL statements as needed, and initialize // fSortedFieldsName[] and fSortedFieldsIndex[] internal sorted arrays // - is slightly faster than several chained MapField() calls, since SQL // will be computed only once function MapFields(const InternalExternalPairs: array of RawUTF8): PSQLRecordPropertiesMapping; public /// initialize the field mapping for a given TSQLRecord // - if AutoComputeSQL is true, will pre-compute all needed SQL from the // supplied information // - will left void fSortedFieldsName[] and fSortedFieldsIndex[], to disable // custom field mapping procedure Init(Table: TSQLRecordClass; const MappedTableName: RawUTF8; MappedConnection: TObject; AutoComputeSQL: boolean; MappingOptions: TSQLRecordPropertiesMappingOptions); /// map a field name from its internal name to its external name // - raise an EORMException if the supplied field name is not defined in // the TSQLRecord as ID or a published property function InternalToExternal(const FieldName: RawUTF8): RawUTF8; /// map a CSV list of field names from its internals to its externals values // - raise an EORMException if any of the supplied field name is not defined // in the TSQLRecord as ID or as property (RowIDFieldName or FieldNames[]) // - to be used for a simple CSV (e.g. for INSERT/SELECT statements): // ! ExtCSV := InternalCSVToExternalCSV('ID,Name'); // - or for a more complex CSV (e.g. for UPDATE statements); // ! ExtCSV := InternalCSVToExternalCSV('ID=?,Name=?','=?,'=?'); function InternalCSVToExternalCSV(const CSVFieldNames: RawUTF8; const Sep: RawUTF8=','; const SepEnd: RawUTF8=''): RawUTF8; /// create a list of external field names, from the internal field names // - raise an EORMException if any of the supplied field name is not defined // in the TSQLRecord as ID or a published property // - if IntFieldIndex is set, it will store an array of internal field // indexes, i.e. -1 for ID or index in in FieldNames[] for other fields procedure InternalToExternalDynArray(const IntFieldNames: array of RawUTF8; out result: TRawUTF8DynArray; IntFieldIndex: PIntegerDynArray=nil); /// map an external field name into its internal field name // - return '' if the external field name is not RowIDFieldName nor in // FieldNames[] function ExternalToInternalOrNull(const ExtFieldName: RawUTF8): RawUTF8; /// map an external field name into its internal field index // - returns the index >=0 in FieldNames[] for a matching external field // - returns -1 if the field name is RowIDFieldName // - returns -2 if the field name is not mapped function ExternalToInternalIndex(const ExtFieldName: RawUTF8): integer; /// append a field name to a RawUTF8 Text buffer // - if FieldIndex=VIRTUAL_TABLE_ROWID_COLUMN (-1), appends RowIDFieldName // - on error (i.e. if FieldIndex is out of range) will return TRUE // - otherwise, will return FALSE and append the external field name to Text function AppendFieldName(FieldIndex: Integer; var Text: RawUTF8): boolean; /// return the field name as RawUTF8 value // - if FieldIndex=VIRTUAL_TABLE_ROWID_COLUMN (-1), appends RowIDFieldName // - otherwise, will return the external field name function FieldNameByIndex(FieldIndex: Integer): RawUTF8; /// opaque object used on the Server side to specify e.g. the DB connection // - will define such a generic TObject, to avoid any unecessary type // dependency to other units, e.g. the SynDB unit in mORMot.pas // - in practice, will be assigned by VirtualTableExternalRegister() to // a TSQLDBConnectionProperties instance in mORMotDB.pas, or by // StaticMongoDBRegister() to a TMongoCollection instance, or by // TDDDRepositoryRestObjectMapping.Create to its associated TSQLRest // - in ORM context, equals nil if the table is internal to SQLite3: // ! if Server.Model.Props[TSQLArticle].ExternalDB.ConnectionProperties=nil then // ! // this is not an external table, since Init() was not called property ConnectionProperties: TObject read fConnectionProperties; /// the associated TSQLRecordProperties property Properties: TSQLRecordProperties read fProps; /// used on the Server side to specify the external DB table name // - e.g. for including a schema name or an existing table name, with an // OleDB/MSSQL/Oracle/MySQL/PostgreSQL/Jet/SQLite3 backend // - equals SQLTableName by default (may be overridden e.g. by mORMotDB's // VirtualTableExternalRegister procedure) property TableName: RawUTF8 read fTableName; /// pre-computed SQL statements for this external TSQLRecord in this model // - you can use those SQL statements directly with the external engine // - filled if AutoComputeSQL was set to true in Init() method property SQL: TSQLModelRecordPropertiesSQL read fSQL; /// the ID/RowID customized external field name, if any // - is 'ID' by default, since 'RowID' is a reserved column name for some // database engines (e.g. Oracle) // - can be customized e.g. via // ! aModel.Props[TSQLMyExternal].ExternalDB.MapField('ID','ExternalID'); property RowIDFieldName: RawUTF8 read fRowIDFieldName; /// the external field names, following fProps.Props.Field[] order // - excluding ID/RowID field, which is stored in RowIDFieldName property ExtFieldNames: TRawUTF8DynArray read fExtFieldNames; /// the unquoted external field names, following fProps.Props.Field[] order // - excluding ID/RowID field, which is stored in RowIDFieldName // - in respect to ExtFieldNames[], this array will never quote the field name property ExtFieldNamesUnQuotedSQL: TRawUTF8DynArray read fExtFieldNamesUnQuotedSQL; /// each bit set, following fProps.Props.Field[]+1 order (i.e. 0=ID, // 1=Field[0], ...), indicates that this external field name // has not been mapped property FieldNamesMatchInternal: TSQLFieldBits read fFieldNamesMatchInternal; /// how the mapping process will take place property Options: TSQLRecordPropertiesMappingOptions read fOptions; /// each time MapField/MapFields is called, this number will increase // - can be used to track mapping changes in real time property MappingVersion: cardinal read fMappingVersion; end; /// dynamic array of TSQLModelRecordProperties // - used by TSQLModel to store the non-shared information of all its tables TSQLModelRecordPropertiesObjArray = array of TSQLModelRecordProperties; /// ORM properties associated to a TSQLRecord within a given model // - "stable" / common properties derivated from RTTI are shared in the // TSQLRecordProperties instance // - since the same TSQLRecord can be defined in several models, with diverse // implementation patterns (e.g. internal in one, external in another), // this class is used to regroup all model-specific settings, like SQL // pre-generated patterns or external DB properties TSQLModelRecordProperties = class protected fProps: TSQLRecordProperties; fKind: TSQLRecordVirtualKind; fModel: TSQLModel; fTableIndex: integer; fFTSWithoutContentTableIndex: integer; fFTSWithoutContentFields: RawUTF8; procedure SetKind(Value: TSQLRecordVirtualKind); function GetProp(const PropName: RawUTF8): TSQLPropInfo; public /// pre-computed SQL statements for this TSQLRecord in this model // - those statements will work for internal tables, not for external // tables with mapped table or fields names SQL: TSQLModelRecordPropertiesSQL; /// allow SQL process for one external TSQLRecord in this model ExternalDB: TSQLRecordPropertiesMapping; /// will by-pass automated table and field creation for this TSQLRecord // - may be used e.g. when the TSQLRecord is in fact mapped into a View, // or is attached as external table and not a real local table NoCreateMissingTable: boolean; /// initialize the ORM properties from the TSQLRecord RTTI and the supplied // TSQLModel constructor Create(aModel: TSQLModel; aTable: TSQLRecordClass; aKind: TSQLRecordVirtualKind); /// clone ORM properties from an existing TSQLModelRecordProperties to // another model constructor CreateFrom(aModel: TSQLModel; aSource: TSQLModelRecordProperties); /// compute the SQL statement to be executed for a specific SELECT // - non simple fields (e.g. BLOBs) will be excluded if SelectFields='*' // - by default, will return the SELECT statement to be used for internal // virtual SQLite3 table - but if ExternalTable is TRUE, then it will // compute a SELECT matching ExternalDB settings function SQLFromSelectWhere(const SelectFields, Where: RawUTF8): RawUTF8; /// define if a FTS4 virtual table will not store its content, but will // be defined as an "external content" FTS4 table // - see https://www.sqlite.org/fts3.html#section_6_2_2 // - the virtual table will be created with content="ContentTableName", // and all fields of the FTS4 table // - by design, all fields of the FTS4 table should exist in the source // ContentTable - otherwise an exception is raised // - the indexed text will be assigned to the FTS4 table, using triggers // generated by TSQLRecordFTS4.InitializeTable at table creation // - note that FTS3 does not support this feature procedure FTS4WithoutContent(ContentTable: TSQLRecordClass); /// the table index of this TSQLRecord in the associated Model property TableIndex: Integer read fTableIndex; /// direct access to a property RTTI information, by name property Prop[const PropName: RawUTF8]: TSQLPropInfo read GetProp; default; published /// the shared TSQLRecordProperties information of this TSQLRecord // - as retrieved from RTTI property Props: TSQLRecordProperties read fProps; /// define if is a normal table (rSQLite3), an FTS/R-Tree virtual // table or a custom TSQLVirtualTable*ID (rCustomForcedID/rCustomAutoID) // - when set, all internal SQL statements will be (re)created, depending of // the expected ID/RowID column name expected (i.e. SQLTableSimpleFields[] // and SQLSelectAll[] - SQLUpdateSet and SQLInsertSet do not include ID) property Kind: TSQLRecordVirtualKind read fKind write SetKind default rSQLite3; end; /// how a TSQLModel stores a foreign link to be cascaded TSQLModelRecordReference = record /// refers to the source TSQLRecordClass as model Tables[] index TableIndex: integer; /// the property FieldType: TSQLPropInfo; /// the target TSQLRecordClass of the field FieldTable: TSQLRecordClass; /// the target TSQLRecordClass of the field, from its Tables[] index FieldTableIndex: integer; /// TRUE if this field is a TRecordReferenceToBeDeleted CascadeDelete: boolean; end; PSQLModelRecordReference = ^TSQLModelRecordReference; TSQLModelRecordReferenceDynArray = array of TSQLModelRecordReference; /// a Database Model (in a MVC-driven way), for storing some tables types // as TSQLRecord classes // - share this Model between TSQLRest Client and Server // - use this class to access the table properties: do not rely on the // low-level database methods (e.g. TSQLDataBase.GetTableNames), since the // tables may not exist in the main SQLite3 database, but in-memory or external // - don't modify the order of Tables inside this Model, if you publish // some TRecordReference property in any of your tables TSQLModel = class private fTables: TSQLRecordClassDynArray; fRoot: RawUTF8; fRootUpper: RawUTF8; fTablesMax: integer; fActions: PEnumType; fEvents: PEnumType; fTableProps: TSQLModelRecordPropertiesObjArray; fCustomCollationForAll: array[TSQLFieldType] of RawUTF8; {$ifndef LVCL} fOnClientIdle: TOnIdleSynBackgroundThread; {$endif} /// contains the caller of CreateOwnedStream() fRestOwner: TSQLRest; /// for every table, contains a locked record list // - very fast, thanks to the use of a dynamic array with one entry by table fLocks: TSQLLocksDynArray; /// for fastest SQL Table name lookup via O(log(n)) binary search fSortedTablesNameUpper: TRawUTF8DynArray; fSortedTablesNameIndex: TIntegerDynArray; /// will contain the registered virtual table modules fVirtualTableModule: array of TSQLVirtualTableClass; /// all TRecordReference and TSQLRecord properties of the model fRecordReferences: TSQLModelRecordReferenceDynArray; fIDGenerator: array of TSynUniqueIdentifierGenerator; procedure SetRoot(const aRoot: RawUTF8); procedure SetTableProps(aIndex: integer); function GetTableIndexSafe(aTable: TSQLRecordClass; RaiseExceptionIfNotExisting: boolean): integer; function GetTableProps(aClass: TSQLRecordClass): TSQLModelRecordProperties; /// get the enumerate type information about the possible actions to be function GetLocks(aTable: TSQLRecordClass): PSQLLocks; function GetTable(const SQLTableName: RawUTF8): TSQLRecordClass; function GetTableExactIndex(const TableName: RawUTF8): integer; function GetTableExactClass(const TableName: RawUTF8): TSQLRecordClass; function getURI(aTable: TSQLRecordClass): RawUTF8; function getURIID(aTable: TSQLRecordClass; aID: TID): RawUTF8; function getURICallBack(const aMethodName: RawUTF8; aTable: TSQLRecordClass; aID: TID): RawUTF8; public /// initialize the Database Model // - set the Tables to be associated with this Model, as TSQLRecord classes // - set the optional Root URI path of this Model // - initialize the fIsUnique[] array from "stored AS_UNIQUE" (i.e. "stored // false") published properties of every TSQLRecordClass constructor Create(const Tables: array of TSQLRecordClass; const aRoot: RawUTF8='root'); reintroduce; overload; /// you should not use this constructor, but one of the overloaded versions, // specifying the associated TSQLRecordClass constructor Create; reintroduce; overload; /// clone an existing Database Model // - all supplied classes won't be redefined as non-virtual: // VirtualTableExternalRegister explicit calls are not mandatory here constructor Create(CloneFrom: TSQLModel); reintroduce; overload; /// initialize the Database Model from an User Interface parameter structure // - this constructor will reset all supplied classes to be defined as // non-virtual (i.e. Kind=rSQLite3): VirtualTableExternalRegister explicit // calls are to be made if tables should be managed as external constructor Create(Owner: TSQLRest; TabParameters: PSQLRibbonTabParameters; TabParametersCount, TabParametersSize: integer; const NonVisibleTables: array of TSQLRecordClass; Actions: PTypeInfo=nil; Events: PTypeInfo=nil; const aRoot: RawUTF8='root'); reintroduce; overload; /// release associated memory destructor Destroy; override; /// add the class if it doesn't exist yet // - return index in Tables[] if not existing yet and successfully added (in this case, // aTableIndexCreated^ is set to the newly created index in Tables[]) // - supplied class will be redefined as non-virtual: VirtualTableExternalRegister // explicit call is to be made if table should be managed as external // - return FALSE if already present, or TRUE if was added to the internal list function AddTable(aTable: TSQLRecordClass; aTableIndexCreated: PInteger=nil): boolean; /// add the class if it doesn't exist yet as itself or as inherited class // - similar to AddTable(), but any class inheriting from the supplied type // will be considered as sufficient // - return the class which has been added, or was already there as // inherited, so that could be used for further instance creation: // ! fSQLAuthUserClass := Model.AddTableInherited(TSQLAuthUser); function AddTableInherited(aTable: TSQLRecordClass): pointer; /// return any class inheriting from the given table in the model // - if the model does not contain such table, supplied aTable is returned function GetTableInherited(aTable: TSQLRecordClass): TSQLRecordClass; /// get the index of aTable in Tables[] // - returns -1 if the table is not in the model function GetTableIndex(aTable: TSQLRecordClass): integer; overload; /// get the index of any class inherithing from aTable in Tables[] // - returns -1 if no table is matching in the model function GetTableIndexInheritsFrom(aTable: TSQLRecordClass): integer; /// get the index of aTable in Tables[] // - raise an EModelException if the table is not in the model function GetTableIndexExisting(aTable: TSQLRecordClass): integer; /// get the index of a table in Tables[] // - expects SQLTableName to be SQL-like formatted (i.e. without TSQL[Record]) function GetTableIndex(const SQLTableName: RawUTF8): integer; overload; /// get the index of a table in Tables[] // - expects SQLTableName to be SQL-like formatted (i.e. without TSQL[Record]) function GetTableIndexPtr(SQLTableName: PUTF8Char): integer; /// return the UTF-8 encoded SQL source to create the table function GetSQLCreate(aTableIndex: integer): RawUTF8; /// return the UTF-8 encoded SQL source to add the corresponding field // via a "ALTER TABLE" statement function GetSQLAddField(aTableIndex, aFieldIndex: integer): RawUTF8; /// return the TRecordReference pointing to the specified record function RecordReference(Table: TSQLRecordClass; ID: TID): TRecordReference; /// return the table class correspondig to a TRecordReference function RecordReferenceTable(const Ref: TRecordReference): TSQLRecordClass; /// return TRUE if the specified field of this class was marked as unique // - an unique field is defined as "stored AS_UNIQUE" (i.e. "stored false") // in its property definition // - reflects the internal private fIsUnique propery function GetIsUnique(aTable: TSQLRecordClass; aFieldIndex: integer): boolean; /// try to retrieve a table index from a SQL statement // - naive search of '... FROM TableName' pattern in the supplied SQL, // using GetTableNameFromSQLSelect() function // - if EnsureUniqueTableInFrom is TRUE, it will check that only one Table // is in the FROM clause, otherwise it will return the first Table specified function GetTableIndexFromSQLSelect(const SQL: RawUTF8; EnsureUniqueTableInFrom: boolean): integer; /// try to retrieve one or several table index from a SQL statement // - naive search of '... FROM Table1,Table2' pattern in the supplied SQL, // using GetTableNamesFromSQLSelect() function function GetTableIndexesFromSQLSelect(const SQL: RawUTF8): TIntegerDynArray; /// try to retrieve one or several TSQLRecordClass from a SQL statement // - naive search of '... FROM Table1,Table2' pattern in the supplied SQL, // using GetTableNamesFromSQLSelect() function function GetTablesFromSQLSelect(const SQL: RawUTF8): TSQLRecordClassDynArray; /// check if the supplied URI matches the model's Root property // - allows sub-domains, e.g. if Root='root/sub1', then '/root/sub1/toto' and // '/root/sub1?n=1' will match, whereas '/root/sub1nope/toto' won't // - the returned enumerates allow to check if the match was exact (e.g. // 'root/sub' matches exactly Root='root'), or with character case // approximation (e.g. 'Root/sub' approximates Root='root') function URIMatch(const URI: RawUTF8): TSQLRestModelMatch; /// compute the SQL statement to be executed for a specific SELECT on Tables // - you can set multiple Table class in Tables: the statement will contain the // table name ('SELECT T1.F1,T1.F2,T1.F3,T2.F1,T2.F2 FROM T1,T2 WHERE ..' e.g.) function SQLFromSelectWhere(const Tables: array of TSQLRecordClass; const SQLSelect, SQLWhere: RawUTF8): RawUTF8; /// set a custom SQlite3 text column collation for all fields of a given // type for all TSQLRecord of this model // - can be used e.g. to override ALL default COLLATE SYSTEMNOCASE of RawUTF8, // or COLLATE ISO8601 for TDateTime, and let the generated SQLite3 file be // available outside the scope of mORMot's SQLite3 engine // - collations defined within our SynSQLite3 unit are named BINARY, NOCASE, // RTRIM and our custom SYSTEMNOCASE, ISO8601, WIN32CASE, WIN32NOCASE: if // you want to use the slow but Unicode ready Windows API, set for each model: // ! SetCustomCollationForAll(sftUTF8Text,'WIN32CASE'); // - shall be set on both Client and Server sides, otherwise some issues // may occur procedure SetCustomCollationForAll(aFieldType: TSQLFieldType; const aCollationName: RawUTF8); /// allow to validate length of all text published properties of all tables // of this model // - the "index" attribute of the RawUTF8/string published properties could // be used to specify a maximum length for external VARCHAR() columns // - SQLite3 will just ignore this "index" information, but it could be // handy to be able to validate the value length before sending to the DB // - this method will create TSynValidateText corresponding to the maximum // field size specified by the "index" attribute, to validate before write // - will expect the "index" value to be in UTF-16 codepoints, unless // IndexIsUTF8Length is set to TRUE, indicating UTF-8 length procedure SetMaxLengthValidatorForAllTextFields(IndexIsUTF8Length: boolean=false); /// allow to filter the length of all text published properties of all tables // of this model // - the "index" attribute of the RawUTF8/string published properties could // be used to specify a maximum length for external VARCHAR() columns // - SQLite3 will just ignore this "index" information, but it could be // handy to be able to filter the value length before sending to the DB // - this method will create TSynFilterTruncate corresponding to the maximum // field size specified by the "index" attribute, to validate before write // - will expect the "index" value to be in UTF-16 codepoints, unless // IndexIsUTF8Length is set to TRUE, indicating UTF-8 length procedure SetMaxLengthFilterForAllTextFields(IndexIsUTF8Length: boolean=false); {$ifndef NOVARIANTS} /// customize the TDocVariant options for all variant published properties // - will change the TSQLPropInfoRTTIVariant.DocVariantOptions value // - use e.g. as SetVariantFieldDocVariantOptions(JSON_OPTIONS_FAST_EXTENDED) // - see also TSQLRecordNoCaseExtended root class procedure SetVariantFieldsDocVariantOptions(const Options: TDocVariantOptions); {$endif} /// force a given table to use a TSynUniqueIdentifierGenerator for its IDs /// - will initialize a generator for the supplied table, using the // given 16-bit process identifier // - you can supply an obfuscation key, which should be shared for the // whole system, so that you may use FromObfuscated/ToObfuscated methods function SetIDGenerator(aTable: TSQLRecordClass; aIdentifier: TSynUniqueIdentifierProcess; const aSharedObfuscationKey: RawUTF8=''): TSynUniqueIdentifierGenerator; /// returns the TSynUniqueIdentifierGenerator associated to a table, if any function GetIDGenerator(aTable: TSQLRecordClass): TSynUniqueIdentifierGenerator; /// assign an enumeration type to the possible actions to be performed // with this model // - call with the TypeInfo() pointer result of an enumeration type // - actions are handled by TSQLRecordForList in the mORMotToolBar.pas unit procedure SetActions(aActions: PTypeInfo); /// assign an enumeration type to the possible events to be triggered // with this class model // - call with the TypeInfo() pointer result of an enumeration type procedure SetEvents(aEvents: PTypeInfo); /// get the text conversion of a given Action, ready to be displayed function ActionName(const Action): string; /// get the text conversion of a given Event, ready to be displayed function EventName(const Event): string; /// register a Virtual Table module for a specified class // - to be called server-side only (Client don't need to know the virtual // table implementation details, and it will increase the code size) // - aClass parameter could be either a TSQLRecordVirtual class, either // a TSQLRecord class which has its kind set to rCustomForcedID or // rCustomAutoID (e.g. TSQLRecordMany calling VirtualTableExternalRegister) // - optional aExternalTableName, aExternalDataBase and aMappingOptions can // be used to specify e.g. connection parameters as expected by mORMotDB // - call it before TSQLRestServer.Create() function VirtualTableRegister(aClass: TSQLRecordClass; aModule: TSQLVirtualTableClass; const aExternalTableName: RawUTF8=''; aExternalDataBase: TObject=nil; aMappingOptions: TSQLRecordPropertiesMappingOptions=[]): boolean; /// retrieve a Virtual Table module associated to a class function VirtualTableModule(aClass: TSQLRecordClass): TSQLVirtualTableClass; /// create a New TSQLRecord instance for a specific Table // - expects SQLTableName to be SQL-like formated (i.e. without TSQL[Record]) // - use this to create a working copy of a table's record, e.g. // - don't forget to Free it when not used any more (use a try...finally // block) // - it's prefered in practice to directly call TSQLRecord*.Create() // in your code function NewRecord(const SQLTableName: RawUTF8): TSQLRecord; /// lock a record // - returns true on success, false if was already locked function Lock(aTable: TSQLRecordClass; aID: TID): boolean; overload; /// lock a record // - returns true on success, false if was already locked function Lock(aTableIndex: integer; aID: TID): boolean; overload; /// lock a record // - returns true on success, false if was already locked function Lock(aRec: TSQLRecord): boolean; overload; /// unlock a specified record // - returns true on success, false if was not already locked function UnLock(aTable: TSQLRecordClass; aID: TID): boolean; overload; /// unlock a specified record // - returns true on success, false if was not already locked function UnLock(aTableIndex: integer; aID: TID): boolean; overload; /// unlock a specified record // - returns true on success, false if was not already locked function UnLock(aRec: TSQLRecord): boolean; overload; /// unlock all previously locked records procedure UnLockAll; /// return true if a specified record is locked function isLocked(aTable: TSQLRecordClass; aID: TID): boolean; overload; /// return true if a specified record is locked function isLocked(aRec: TSQLRecord): boolean; overload; /// delete all the locked IDs entries, after a specified time // - to be used to release locked records if the client crashed // - default value is 30 minutes, which seems correct for common usage procedure PurgeOlderThan(MinutesFromNow: cardinal=30); /// returns the Root property, or '' if the instance is nil function SafeRoot: RawUTF8; /// get the classes list (TSQLRecord descendent) of all available tables property Tables: TSQLRecordClassDynArray read fTables; /// get a class from a table name // - expects SQLTableName to be SQL-like formated (i.e. without TSQL[Record]) property Table[const SQLTableName: RawUTF8]: TSQLRecordClass read GetTable; default; /// get a class from a table TableName (don't truncate TSQLRecord* if necessary) property TableExact[const TableName: RawUTF8]: TSQLRecordClass read GetTableExactClass; /// get the URI for a class in this Model, as 'ModelRoot/SQLTableName' property URI[aClass: TSQLRecordClass]: RawUTF8 read getURI; /// the associated ORM information for a given TSQLRecord class // - raise an EModelException if aClass is not declared within this model // - returns the corresponding TableProps[] item if the class is known property Props[aClass: TSQLRecordClass]: TSQLModelRecordProperties read GetTableProps; /// the maximum index of TableProps[] class properties array property TablesMax: integer read fTablesMax; // performed with this model // - Actions are e.g. linked to some buttons in the User Interface property Actions: PEnumType read fActions; /// get the enumerate type information about the possible Events to be // performed with this model // - Events can be linked to actions and custom status, to provide a // centralized handling of logging (e.g. in an Audit Trail table) property Events: PEnumType read fEvents; /// this property value is used to auto free the database Model class // - set this property after Owner.Create() in order to have // Owner.Destroy autofreeing it property Owner: TSQLRest read fRestOwner write fRestOwner; /// for every table, contains a locked record list // - very fast, thanks to the use one TSQLLocks entry by table property Locks: TSQLLocksDynArray read fLocks; /// this array contain all TRecordReference and TSQLRecord properties // existing in the database model // - used in TSQLRestServer.Delete() to enforce relational database coherency // after deletion of a record: all other records pointing to it will be // reset to 0 or deleted (if CascadeDelete is true) property RecordReferences: TSQLModelRecordReferenceDynArray read fRecordReferences; {$ifndef LVCL} /// set a callback event to be executed in loop during client remote // blocking process, e.g. to refresh the UI during a somewhat long request // - will be passed to TSQLRestClientURI.OnIdle property by // TSQLRestClientURI.RegisteredClassCreateFrom() method, if applying property OnClientIdle: TOnIdleSynBackgroundThread read fOnClientIdle write fOnClientIdle; {$endif} published /// the Root URI path of this Database Model // - this textual value will be used directly to compute the URI for REST // routing, so it should contain only URI-friendly characters, // i.e. only alphanumerical characters, excluding e.g. space or '+', // otherwise an EModelException is raised property Root: RawUTF8 read fRoot write SetRoot; /// the associated ORM information about all handled TSQLRecord class properties // - this TableProps[] array will map the Tables[] array, and will allow // fast direct access to the Tables[].RecordProps values property TableProps: TSQLModelRecordPropertiesObjArray read fTableProps; end; PRecordRef = ^RecordRef; /// useful object to type cast TRecordReference type value into explicit // TSQLRecordClass and ID // - use RecordRef(Reference).TableIndex/Table/ID/Text methods to retrieve // the details of a TRecordReference encoded value // - use TSQLRest.Retrieve(Reference) to get a record content from DB // - instead of From(Reference).From(), you could use the more explicit // TSQLRecord.RecordReference(Model) or TSQLModel.RecordReference() // methods or RecordReference() function to encode the value // - don't change associated TSQLModel tables order, since TRecordReference // depends on it to store the Table type // - since 6 bits are used for the table index, the corresponding table // MUST appear in the first 64 items of the associated TSQLModel.Tables[] {$ifdef FPC_OR_UNICODE}RecordRef = record {$else}RecordRef = object{$endif} public /// the value itself // - (value and 63) is the TableIndex in the current database Model // - (value shr 6) is the ID of the record in this table // - value=0 means no reference stored // - we use this coding and not the opposite (Table in MSB) to minimize // integer values; but special UTF8CompareRecord() function has to be used // for sorting // - type definition matches TRecordReference (i.e. Int64/TID) to allow // typecast as such: // ! aClass := PRecordRef(@Reference)^.Table(Model); Value: TID; /// return the index of the content Table in the TSQLModel function TableIndex: integer; {$ifdef HASINLINE}inline;{$endif} /// return the class of the content in a specified TSQLModel function Table(Model: TSQLModel): TSQLRecordClass; /// return the ID of the content function ID: TID; {$ifdef HASINLINE}inline;{$endif} /// fill Value with the corresponding parameters // - since 6 bits are used for the table index, aTable MUST appear in the // first 64 items of the associated TSQLModel.Tables[] array procedure From(Model: TSQLModel; aTable: TSQLRecordClass; aID: TID); /// get a ready to be displayed text from the stored Table and ID // - display 'Record 2301' e.g. function Text(Model: TSQLModel): RawUTF8; overload; /// get a ready to be displayed text from the stored Table and ID // - display 'Record "RecordName"' e.g. function Text(Rest: TSQLRest): RawUTF8; overload; end; /// an abstract base class, corresponding to an R-Tree table of values // - do not use this class, but either TSQLRecordRTree or TSQLRecordRTreeInteger // - an R-Tree is a special index that is designed for doing range queries. // R-Trees are most commonly used in geospatial systems where each entry is a // rectangle with minimum and maximum X and Y coordinates. Given a query // rectangle, an R-Tree is able to quickly find all entries that are contained // within the query rectangle or which overlap the query rectangle. This idea // is easily extended to three dimensions for use in CAD systems. R-Trees also // find use in time-domain range look-ups. For example, suppose a database // records the starting and ending times for a large number of events. A R-Tree // is able to quickly find all events, for example, that were active at any // time during a given time interval, or all events that started during a // particular time interval, or all events that both started and ended within // a given time interval. And so forth. See http:// www.sqlite.org/rtree.html // - any record which inherits from this class as TSQLRecordRTree must have // only sftFloat (double) fields (or integer fields for TSQLRecordRTreeInteger) // grouped by pairs, each as minimum- and maximum-value, up to 5 dimensions // (i.e. 11 columns, including the ID property) // - since SQLite version 3.24.0 (2018-06-04), R-Tree tables can have // auxiliary columns that store arbitrary data: such fields should appear after // the boundary columns, and have their property name starting with '_' in the // class definition; in both SQL and Where clause, the '_' will be trimmed - note // that you should better use the SynSQLite3Static unit, since an external // SQLite3 .dll/.so library as supplied by the system may be outdated // - internally, the SQlite3 R-Tree extension will be implemented as a virtual // table, storing coordinates/values as 32-bit floating point (single - as // TSQLRecordRTree kind of ORM classes) or 32-bit integers (as TSQLRecordRTreeInteger), // but will make all R-Tree computation using 64-bit floating point (double) // - as with any virtual table, the ID: TID property must be set before adding // a TSQLRecordRTree to the database, e.g. to link a R-Tree representation to // a regular TSQLRecord table // - queries against the ID or the coordinate ranges are almost immediate: so // you can e.g. extract some coordinates box from the regular TSQLRecord // table, then use a TSQLRecordRTree joined query to make the process faster; // this is exactly what the TSQLRestClient.RTreeMatch method offers - of // course Auxiliary Columns could avoid to make the JOIN and call RTreeMatch TSQLRecordRTreeAbstract = class(TSQLRecordVirtual) public /// override this class function to implement a custom SQL *_in() function // - in practice, an R-Tree index does not normally provide the exact answer // but merely reduces the set of potential answers from millions to dozens: // this method will be called from the *_in() SQL function to actually // return exact matches // - by default, the BLOB array will be decoded via the BlobToCoord class // procedure, and will create a SQL function from the class name // - used e.g. by the TSQLRestClient.RTreeMatch method class function ContainedIn(const BlobA,BlobB): boolean; virtual; abstract; /// will return 'MapBox_in' e.g. for TSQLRecordMapBox class function RTreeSQLFunctionName: RawUTF8; virtual; end; /// this kind of record array can be used for direct floating-point // coordinates storage as in TSQLRecordRTree.BlobToCoord TSQLRecordTreeCoords = array[0..RTREE_MAX_DIMENSION-1] of packed record min, max: double; end; /// a base record, corresponding to an R-Tree table of floating-point values // - for instance, the following class will define a 2 dimensional RTree // of floating point coordinates, and an associated MapBox_in() function: // ! TSQLRecordMapBox = class(TSQLRecordRTree) // ! protected // ! fMinX, fMaxX, fMinY, fMaxY: double; // ! published // ! property MinX: double read fMinX write fMinX; // ! property MaxX: double read fMaxX write fMaxX; // ! property MinY: double read fMinY write fMinY; // ! property MaxY: double read fMaxY write fMaxY; // ! end; // - since SQLite version 3.24.0, TSQLRecordRTree can have auxiliary columns // that store arbitrary data, having their property name starting with '_' // (only in this class definition: SQL and Where clauses will trim it) TSQLRecordRTree = class(TSQLRecordRTreeAbstract) public /// override this class function to implement a custom SQL *_in() function // - by default, the BLOB array will be decoded via the BlobToCoord() class // procedure, and will create a SQL function from the class name // - used e.g. by the TSQLRestClient.RTreeMatch method class function ContainedIn(const BlobA,BlobB): boolean; override; /// override this class function to implement a custom box coordinates // from a given BLOB content // - by default, the BLOB array will contain a simple array of double // - but you can override this method to handle a custom BLOB field content, // intended to hold some kind of binary representation of the precise // boundaries of the object, and convert it into box coordinates as // understood by the ContainedIn() class function // - the number of pairs in OutCoord will be taken from the current number // of published double properties // - used e.g. by the TSQLRest.RTreeMatch method class procedure BlobToCoord(const InBlob; var OutCoord: TSQLRecordTreeCoords); virtual; end; /// this kind of record array can be used for direct 32-bit integer // coordinates storage as in TSQLRecordRTreeInteger.BlobToCoord TSQLRecordTreeCoordsInteger = array[0..RTREE_MAX_DIMENSION-1] of packed record min, max: integer; end; /// a base record, corresponding to an R-Tree table of 32-bit integer values // - for instance, the following class will define a 2 dimensional RTree // of 32-bit integer coordinates, and an associated MapBox_in() function: // ! TSQLRecordMapBox = class(TSQLRecordRTree) // ! protected // ! fMinX, fMaxX, fMinY, fMaxY: integer; // ! published // ! property MinX: integer read fMinX write fMinX; // ! property MaxX: integer read fMaxX write fMaxX; // ! property MinY: integer read fMinY write fMinY; // ! property MaxY: integer read fMaxY write fMaxY; // ! end; // - since SQLite version 3.24.0, TSQLRecordRTreeInteger can have auxiliary // columns that store arbitrary data, having their property name starting with '_' // (only in this class definition: SQL and Where clauses will trim it) TSQLRecordRTreeInteger = class(TSQLRecordRTreeAbstract) public /// override this class function to implement a custom SQL *_in() function // - by default, the BLOB array will be decoded via the BlobToCoord() class // procedure, and will create a SQL function from the class name // - used e.g. by the TSQLRest.RTreeMatch method class function ContainedIn(const BlobA,BlobB): boolean; override; /// override this class function to implement a custom box coordinates // from a given BLOB content // - by default, the BLOB array will contain a simple array of integer // - but you can override this method to handle a custom BLOB field content, // intended to hold some kind of binary representation of the precise // boundaries of the object, and convert it into box coordinates as // understood by the ContainedIn() class function // - the number of pairs in OutCoord will be taken from the current number // of published integer properties // - used e.g. by the TSQLRest.RTreeMatch method class procedure BlobToCoord(const InBlob; var OutCoord: TSQLRecordTreeCoordsInteger); virtual; end; /// a base record, corresponding to a FTS3 table, i.e. implementing full-text // - FTS3/FTS4/FTS5 tables are SQLite virtual tables allowing users to perform // full-text searches on a set of documents. The most common (and effective) // way to describe full-text searches is "what Google, Yahoo and Altavista do // with documents placed on the World Wide Web". Users input a term, or // series of terms, perhaps connected by a binary operator or grouped together // into a phrase, and the full-text query system finds the set of documents // that best matches those terms considering the operators and groupings the // user has specified. See http:// sqlite.org/fts3.html // - any record which inherits from this class must have only sftUTF8Text // (RawUTF8) fields - with Delphi 2009+, you can have string fields // - this record has its fID: TID property which may be published // as DocID, to be consistent with SQLite3 praxis, and reflect that it // points to an ID of another associated TSQLRecord // - a good approach is to store your data in a regular TSQLRecord table, then // store your text content in a separated FTS3 table, associated to this // TSQLRecordFTS3 table via its ID/DocID // - the ID/DocID property can be set when the record is added, to retrieve any // associated TSQLRecord (note that for a TSQLRecord record, // the ID property can't be set at adding, but is calculated by the engine) // - static tables don't handle TSQLRecordFTS3 classes // - by default, the FTS3 engine ignore all characters >= #80, but handle // low-level case insentivity (i.e. 'A'..'Z') so you must keep your // request with the same range for upper case // - by default, the "simple" tokenizer is used, but you can inherits from // TSQLRecordFTS3Porter class if you want a better English matching, using // the Porter Stemming algorithm, or TSQLRecordFTS3Unicode61 for Unicode // support - see http:// sqlite.org/fts3.html#tokenizer // - you can select either the FTS3 engine, or the more efficient (and new) // FTS4 engine (available since version 3.7.4), by using the TSQLRecordFTS4 // type, or TSQLRecordFTS5 for the latest (and preferred) FTS5 engine // - in order to make FTS queries, use the dedicated TSQLRest.FTSMatch // method, with the MATCH operator (you can use regular queries, but you must // specify 'RowID' instead of 'DocID' or 'ID' because of FTS3 Virtual // table specificity): // ! var IDs: TIDDynArray; // ! if FTSMatch(TSQLMyFTS3Table,'text MATCH "linu*"',IDs) then // ! // you have all matching IDs in IDs[] // - by convention, inherited class name could specify a custom stemming // algorithm by starting with "TSQLRecordFTS3", and adding the algorithm name as // suffix, e.g. TSQLRecordFTS3Porter will create a "tokenize=porter" virtual table TSQLRecordFTS3 = class(TSQLRecordVirtual) public /// optimize the FTS3 virtual table // - this causes FTS3 to merge all existing index b-trees into a single large // b-tree containing the entire index. This can be an expensive operation, // but may speed up future queries. See http://sqlite.org/fts3.html#section_1_2 // - this method must be called server-side // - returns TRUE on success class function OptimizeFTS3Index(Server: TSQLRestServer): boolean; /// this DocID property map the internal Row_ID property // - but you can set a value to this property before calling the Add() // method, to associate this TSQLRecordFTS3 to another TSQLRecord // - ID property is read-only, but this DocID property can be written/set // - internaly, we use RowID in the SQL statements, which is compatible // with both TSQLRecord and TSQLRecordFTS3 kind of table property DocID: TID read GetID write fID; end; /// this base class will create a FTS3 table using the Porter Stemming algorithm // - see http://sqlite.org/fts3.html#tokenizer // - will generate tokenize=porter by convention from the class name TSQLRecordFTS3Porter = class(TSQLRecordFTS3); /// this base class will create a FTS3 table using the Unicode61 Stemming algorithm // - see http://sqlite.org/fts3.html#tokenizer // - will generate tokenize=unicode64 by convention from the class name TSQLRecordFTS3Unicode61 = class(TSQLRecordFTS3); /// a base record, corresponding to a FTS4 table, which is an enhancement to FTS3 // - FTS3 and FTS4 are nearly identical. They share most of their code in common, // and their interfaces are the same. The only difference is that FTS4 stores // some additional information about the document collection in two of new FTS // shadow tables. This additional information allows FTS4 to use certain // query performance optimizations that FTS3 cannot use. And the added information // permits some additional useful output options in the matchinfo() function. // - for newer applications, TSQLRecordFTS5 is recommended; though if minimal // disk usage or compatibility with older versions of SQLite are important, // then TSQLRecordFTS3 will usually serve just as well // - see http:// sqlite.org/fts3.html#section_1_1 // - by convention, inherited class name could specify a custom stemming // algorithm by starting with "TSQLRecordFTS4", and adding the algorithm name as // suffix, e.g. TSQLRecordFTS'Porter will create a "tokenize=porter" virtual table TSQLRecordFTS4 = class(TSQLRecordFTS3) public /// this overriden method will create TRIGGERs for FTSWithoutContent() class procedure InitializeTable(Server: TSQLRestServer; const FieldName: RawUTF8; Options: TSQLInitializeTableOptions); override; end; /// this base class will create a FTS4 table using the Porter Stemming algorithm // - see http://sqlite.org/fts3.html#tokenizer // - will generate tokenize=porter by convention from the class name TSQLRecordFTS4Porter = class(TSQLRecordFTS4); /// this base class will create a FTS4 table using the Unicode61 Stemming algorithm // - see http://sqlite.org/fts3.html#tokenizer // - will generate tokenize=unicode64 by convention from the class name TSQLRecordFTS4Unicode61 = class(TSQLRecordFTS4); /// a base record, corresponding to a FTS5 table, which is an enhancement to FTS4 // - FTS5 is a new version of FTS4 that includes various fixes and solutions for // problems that could not be fixed in FTS4 without sacrificing backwards compatibility // - for newer applications, TSQLRecordFTS5 is recommended; though if minimal // disk usage or compatibility with older versions of SQLite are important, // then TSQLRecordFTS3/TSQLRecordFTS4 will usually serve just as well // - see https://sqlite.org/fts5.html#appendix_a // - by convention, inherited class name could specify a custom stemming // algorithm by starting with "TSQLRecordFTS5", and adding the algorithm name as // suffix, e.g. TSQLRecordFTS5Porter will create a "tokenize=porter" virtual table TSQLRecordFTS5 = class(TSQLRecordFTS4); /// this base class will create a FTS5 table using the Porter Stemming algorithm // - see https://sqlite.org/fts5.html#tokenizers // - will generate tokenize=porter by convention from the class name TSQLRecordFTS5Porter = class(TSQLRecordFTS5); /// this base class will create a FTS5 table using the Unicode61 Stemming algorithm // - see https://sqlite.org/fts5.html#tokenizers // - will generate tokenize=unicode64 by convention from the class name TSQLRecordFTS5Unicode61 = class(TSQLRecordFTS5); /// class-reference type (metaclass) of a FTS3/FTS4/FTS5 virtual table TSQLRecordFTS3Class = class of TSQLRecordFTS3; /// class-reference type (metaclass) of a RTREE virtual table // - either a TSQLRecordRTree or a TSQLRecordRTreeInteger TSQLRecordRTreeClass = class of TSQLRecordRTreeAbstract; /// the kind of fields to be available in a Table resulting of // a TSQLRecordMany.DestGetJoinedTable() method call // - Source fields are not available, because they will be always the same for // a same SourceID, and they should be available from the TSQLRecord which // hold the TSQLRecordMany instance // - jkDestID and jkPivotID will retrieve only DestTable.ID and PivotTable.ID // - jkDestFields will retrieve DestTable.* simple fields, or the fields // specified by aCustomFieldsCSV (the Dest table name will be added: e.g. // for aCustomFieldsCSV='One,Two', will retrieve DestTable.One, DestTable.Two) // - jkPivotFields will retrieve PivotTable.* simple fields, or the fields // specified by aCustomFieldsCSV (the Pivot table name will be added: e.g. // for aCustomFieldsCSV='One,Two', will retrieve PivotTable.One, PivotTable.Two) // - jkPivotAndDestAllFields for PivotTable.* and DestTable.* simple fields, // or will retrieve the specified aCustomFieldsCSV fields (with // the table name associated: e.g. 'PivotTable.One, DestTable.Two') TSQLRecordManyJoinKind = ( jkDestID, jkPivotID, jkDestFields, jkPivotFields, jkPivotAndDestFields); /// handle "has many" and "has many through" relationships // - many-to-many relationship is tracked using a table specifically for that // relationship, turning the relationship into two one-to-many relationships // pointing in opposite directions // - by default, only two TSQLRecord (i.e. INTEGER) fields must be created, // named "Source" and "Dest", the first pointing to the source record (the one // with a TSQLRecordMany published property) and the second to the destination record // - you should first create a type inheriting from TSQLRecordMany, which // will define the pivot table, providing optional "through" parameters if needed // ! TSQLDest = class(TSQLRecord); // ! TSQLSource = class; // ! TSQLDestPivot = class(TSQLRecordMany) // ! private // ! fSource: TSQLSource; // ! fDest: TSQLDest; // ! fTime: TDateTime; // ! published // ! property Source: TSQLSource read fSource; // map Source column // ! property Dest: TSQLDest read fDest; // map Dest column // ! property AssociationTime: TDateTime read fTime write fTime; // ! end; // ! TSQLSource = class(TSQLRecord) // ! private // ! fDestList: TSQLDestPivot; // ! published // ! DestList: TSQLDestPivot read fDestList; // ! end; // - in all cases, at leat two 'Source' and 'Dest' published properties must // be declared as TSQLRecord children in any TSQLRecordMany descendant // because they will always be needed for the 'many to many' relationship // - when a TSQLRecordMany published property exists in a TSQLRecord, it is // initialized automaticaly by TSQLRecord.Create // - to add some associations to the pivot table, use the ManyAdd() method // - to retrieve an association, use the ManySelect() method // - to delete an association, use the ManyDelete() method // - to read all Dest records IDs, use the DestGet() method // - to read the Dest records and the associated "through" fields content, use // FillMany then FillRow, FillOne and FillRewind methods to loop through records // - to read all Source records and the associaed "through" fields content, // FillManyFromDest then FillRow, FillOne and FillRewind methods // - to read all Dest IDs after a join to the pivot table, use DestGetJoined TSQLRecordMany = class(TSQLRecord) protected // internal fields initialized during TSQLRecord.Create // - map to the Source and Dest properties field values in TSQLRecord values fSourceID: PPtrInt; fDestID: PPtrInt; /// retrieve the TSQLRecordMany ID from a given source+dest IDs pair function InternalIDFromSourceDest(aClient: TSQLRest; aSourceID, aDestID: TID): TID; function InternalFillMany(aClient: TSQLRest; aID: TID; const aAndWhereSQL: RawUTF8; isDest: boolean): integer; public /// initialize this instance, and needed internal fields // - will set protected fSourceID/fDestID fields constructor Create; override; /// retrieve all records associated to a particular source record, which // has a TSQLRecordMany property // - returns the Count of records corresponding to this aSource record // - the records are stored in an internal TSQLTable, refered in the private // fTable field, and initialized via a FillPrepare call: all Dest items // are therefore accessible with standard FillRow, FillOne and FillRewind methods // - use a "for .." loop or a "while FillOne do ..." loop to iterate // through all Dest items, getting also any additional 'through' columns // - if source ID parameter is 0, the ID is taken from the fSourceID field // (set by TSQLRecord.Create) // - note that if the Source record has just been added, fSourceID is not // set, so this method will fail: please specify aSourceID parameter with // the one just added/created // - the optional aAndWhereSQL parameter can be used to add any additional // condition to the WHERE statement (e.g. 'Salary>:(1000): AND Salary<:(2000):') // according to TSQLRecordMany properties - note that you should better use // inlined parameters for faster processing on server, so you may call e.g. // ! aRec.FillMany(Client,0,FormatUTF8('Salary>? AND Salary:(1000): AND Salary<:(2000):') // according to TSQLRecordMany properties - note that you should better use // inlined parameters for faster processing on server, so you may call e.g. // ! aRec.FillManyFromDest(Client,DestID,FormatUTF8('Salary>? AND Salary:(1000): AND Salary<:(2000):' - note that you should better use // inlined parameters for faster processing on server, so you may use the // more convenient function // ! FormatUTF8('Salary>? AND Salary:(1000): AND Salary<:(2000):') according to TSQLRecordMany // properties - note that you should better use such inlined parameters as // ! FormatUTF8('Salary>? AND Salary:(1000): AND Salary<:(2000):') according to TSQLRecordMany // properties - note that you should better use such inlined parameters as // ! FormatUTF8('Salary>? AND Salary:(1000): AND Salary<:(2000):') // according to TSQLRecordMany properties - note that you should better use // such inlined parameters e.g. calling // ! FormatUTF8('Salary>? AND Salary 'Calculator' property InterfaceURI: RawUTF8 read fInterfaceURI write fInterfaceURI; {$ifndef NOVARIANTS} /// how this interface will work with variants (including TDocVariant) // - by default, contains JSON_OPTIONS_FAST for best performance - i.e. // [dvoReturnNullForUnknownProperty,dvoValueCopiedByReference] property DocVariantOptions: TDocVariantOptions read fDocVariantOptions write fDocVariantOptions; {$endif} published /// will return the interface name, e.g. 'ICalculator' // - published property to be serializable as JSON e.g. for debbuging info property InterfaceName: RawUTF8 read fInterfaceName; end; {$ifdef HASINTERFACERTTI} /// class handling interface RTTI and fake implementation class // - this class only exists for Delphi 6 and up, since FPC does not generate // the expected RTTI - see http://bugs.freepascal.org/view.php?id=26774 TInterfaceFactoryRTTI = class(TInterfaceFactory) protected procedure AddMethodsFromTypeInfo(aInterface: PTypeInfo); override; end; {$endif HASINTERFACERTTI} {$M+} /// how TInterfacedObjectFromFactory will perform its execution // - by default, fInvoke() will receive standard JSON content, unless // ifoJsonAsExtended is set, and extended JSON is used // - ifoDontStoreVoidJSON will ensure objects and records won't include // default void fields in JSON serialization TInterfacedObjectFromFactoryOption = (ifoJsonAsExtended, ifoDontStoreVoidJSON); /// defines how TInterfacedObjectFromFactory will perform its execution TInterfacedObjectFromFactoryOptions = set of TInterfacedObjectFromFactoryOption; /// abstract class handling a generic interface implementation class TInterfacedObjectFromFactory = class(TInterfacedObject) protected fFactory: TInterfaceFactory; fOptions: TInterfacedObjectFromFactoryOptions; fInvoke: TOnFakeInstanceInvoke; fNotifyDestroy: TOnFakeInstanceDestroy; fClientDrivenID: Cardinal; public /// create an instance, using the specified interface constructor Create(aFactory: TInterfaceFactory; aOptions: TInterfacedObjectFromFactoryOptions; aInvoke: TOnFakeInstanceInvoke; aNotifyDestroy: TOnFakeInstanceDestroy); /// release the remote server instance (in sicClientDriven mode); destructor Destroy; override; published /// the associated interface factory class property Factory: TInterfaceFactory read fFactory; /// the ID used in sicClientDriven mode property ClientDrivenID: Cardinal read fClientDrivenID; end; {$M-} /// class handling interface implementation generated from source // - this class targets FPC, which does not generate the expected RTTI - see // http://bugs.freepascal.org/view.php?id=26774 // - mORMotWrapper.pas will generate a new inherited class, overriding abstract // AddMethodsFromTypeInfo() to define the interface methods TInterfaceFactoryGenerated = class(TInterfaceFactory) protected fTempStrings: TRawUTF8DynArray; /// the overriden AddMethodsFromTypeInfo() method will call e.g. as // ! AddMethod('Add',[ // ! 0,'n1',TypeInfo(Integer), // ! 0,'n2',TypeInfo(Integer), // ! 3,'Result',TypeInfo(Integer)]); // with 0=ord(smdConst) and 3=ord(smdResult) procedure AddMethod(const aName: RawUTF8; const aParams: array of const); virtual; public /// register one interface type definition from the current class // - will be called by mORMotWrapper.pas generated code, in initialization // section, so that the needed type information will be available class procedure RegisterInterface(aInterface: PTypeInfo); virtual; end; /// abstract parameters used by TInterfaceStub.Executes() events callbacks TOnInterfaceStubExecuteParamsAbstract = class protected fSender: TInterfaceStub; fMethod: PServiceMethod; fParams: RawUTF8; fEventParams: RawUTF8; fResult: RawUTF8; fFailed: boolean; function GetSenderAsMockTestCase: TSynTestCase; public /// constructor of one parameters marshalling instance constructor Create(aSender: TInterfaceStub; aMethod: PServiceMethod; const aParams,aEventParams: RawUTF8); virtual; /// call this method if the callback implementation failed procedure Error(const aErrorMessage: RawUTF8); overload; /// call this method if the callback implementation failed procedure Error(const Format: RawUTF8; const Args: array of const); overload; /// the stubbing / mocking generator property Sender: TInterfaceStub read fSender; /// the mocking generator associated test case // - will raise an exception if the associated Sender generator is not // a TInterfaceMock property TestCase: TSynTestCase read GetSenderAsMockTestCase; /// pointer to the method which is to be executed property Method: PServiceMethod read fMethod; /// a custom message, defined at TInterfaceStub.Executes() definition property EventParams: RawUTF8 read fEventParams; /// outgoing values array encoded as JSON // - every var, out parameter or the function result shall be encoded as // a JSON array into this variable, in the same order than the stubbed // method declaration // - use Returns() method to create the JSON array directly, from an array // of values property Result: RawUTF8 read fResult; /// low-level flag, set to TRUE if one of the Error() method was called property Failed: boolean read fFailed; end; {$ifndef NOVARIANTS} /// parameters used by TInterfaceStub.Executes() events callbacks as Variant // - this class will expect input and output parameters to specified as // variant arrays properties, so is easier (and a bit slower) than the // TOnInterfaceStubExecuteParamsJSON class TOnInterfaceStubExecuteParamsVariant = class(TOnInterfaceStubExecuteParamsAbstract) protected fInput: TVariantDynArray; fOutput: TVariantDynArray; function GetInput(Index: Integer): variant; procedure SetOutput(Index: Integer; const Value: variant); function GetInNamed(const aParamName: RawUTF8): variant; procedure SetOutNamed(const aParamName: RawUTF8; const Value: variant); function GetInUTF8(const ParamName: RawUTF8): RawUTF8; procedure SetResultFromOutput; public /// constructor of one parameters marshalling instance constructor Create(aSender: TInterfaceStub; aMethod: PServiceMethod; const aParams,aEventParams: RawUTF8); override; /// returns the input parameters as a TDocVariant object or array function InputAsDocVariant(Kind: TServiceMethodParamsDocVariantKind; Options: TDocVariantOptions=[dvoReturnNullForUnknownProperty,dvoValueCopiedByReference]): variant; /// returns the output parameters as a TDocVariant object or array function OutputAsDocVariant(Kind: TServiceMethodParamsDocVariantKind; Options: TDocVariantOptions=[dvoReturnNullForUnknownProperty,dvoValueCopiedByReference]): variant; {$ifdef WITHLOG} /// log the input or output parameters to a log instance procedure AddLog(aLog: TSynLogClass; aOutput: boolean; aLevel: TSynLogInfo=sllTrace); {$endif} /// input parameters when calling the method // - order shall follow the method const and var parameters // ! Stub.Add(10,20) -> Input[0]=10, Input[1]=20 // - if the supplied Index is out of range, an EInterfaceStub will be raised property Input[Index: Integer]: variant read GetInput; /// output parameters returned after method process // - order shall follow the method var, out parameters and the function // result (if method is not a procedure) // - if the supplied Index is out of range, an EInterfaceStub will be raised // - can be used as such: // ! procedure TFooTestCase.ExecuteBar(Ctxt: TOnInterfaceStubExecuteParamsVariant); // ! begin // Input[0]=i // ! Ctxt.Output[0] := Ctxt.Input[0]+1; // i := i+1; // ! Ctxt.Output[1] := 42; // result := 42; // ! end; // Output|0]=i, Output[1]=result // to emulate this native implementation: // ! function Bar(var i: Integer): Integer; // ! begin // ! inc(i); // ! result := 42; // ! end; // - consider using the safest Named[] property, to avoid parameters // index matching issue // - if an Output[]/Named[] item is not set, a default value will be used property Output[Index: Integer]: variant write SetOutput; /// access to input/output parameters when calling the method // - if the supplied name is incorrect, an EInterfaceStub will be raised // - is a bit slower than Input[]/Output[] indexed properties, but easier // to work with, and safer in case of method signature change (like parameter // add or rename) // - marked as default property, so you can use it e.g. as such: // ! procedure TFooTestCase.ExecuteBar(Ctxt: TOnInterfaceStubExecuteParamsVariant); // ! begin // ! Ctxt['i'] := Ctxt['i']+1; // i := i+1; // ! Ctxt['result'] := 42; // result := 42; // ! end; // to emulate this native implementation: // ! function Bar(var i: Integer): Integer; // ! begin // ! inc(i); // ! result := 42; // ! end; // - using this default Named[] property is recommended over the index-based // Output[] property // - if an Output[]/Named[] item is not set, a default value will be used property Named[const ParamName: RawUTF8]: variant read GetInNamed write SetOutNamed; default; /// access to UTF-8 input parameters when calling the method // - if the supplied name is incorrect, an EInterfaceStub will be raised // - is a bit slower than Input[]/Output[] indexed properties, but easier // to work with, and safer in case of method signature change (like parameter // add or rename) // - slightly easier to use Ctxt.UTF8['str'] than ToUTF8(Ctxt.Named['str']) property UTF8[const ParamName: RawUTF8]: RawUTF8 read GetInUTF8; end; {$endif NOVARIANTS} /// parameters used by TInterfaceStub.Executes() events callbacks as JSON // - this class will expect input and output parameters to be encoded as // JSON arrays, so is faster than TOnInterfaceStubExecuteParamsVariant TOnInterfaceStubExecuteParamsJSON = class(TOnInterfaceStubExecuteParamsAbstract) public /// a method to return an array of values into Result // - just a wrapper around JSONEncodeArrayOfConst([...]) // - can be used as such: // ! procedure TFooTestCase.ExecuteBar(var Ctxt: TOnInterfaceStubExecuteParamsJSON); // ! begin // Ctxt.Params := '[i]' -> Ctxt.Result := '[i+1,42]' // ! Ctxt.Returns([GetInteger(pointer(Ctxt.Params))+1,42]); // ! end; // to emulate this native implementation: // ! function Bar(var i: Integer): Integer; // ! begin // ! inc(i); // ! result := 42; // ! end; procedure Returns(const Values: array of const); overload; /// a method to return a JSON array of values into Result // - expected format is e.g. '[43,42]' procedure Returns(const ValuesJsonArray: RawUTF8); overload; /// incoming parameters array encoded as JSON array without braces // - order follows the method const and var parameters // ! Stub.Add(10,20) -> Params = '10,20'; property Params: RawUTF8 read fParams; end; {$ifndef NOVARIANTS} /// event called by the TInterfaceStub.Executes() fluent method for variant process // - by default Ctxt.Result shall contain the default JSON array result for // this method - use Ctxt.Named[] default properties, e.g. as // ! Ctxt['result'] := Ctxt['n1']-Ctxt['n2']; // or with Input[] / Output[] properties: // ! with Ctxt do Output[0] := Input[0]-Input[1]; // - you can call Ctxt.Error() to notify the caller for an execution error TOnInterfaceStubExecuteVariant = procedure(Ctxt: TOnInterfaceStubExecuteParamsVariant) of object; {$endif NOVARIANTS} /// event called by the TInterfaceStub.Executes() fluent method for JSON process // - by default Ctxt.Result shall contain the default JSON array result for // this method - use Ctxt.Named[] default properties, e.g. as // ! P := pointer(Ctxt.Params); // ! Ctxt.Returns([GetNextItemDouble(P)-GetNextItemDouble(P)]); // - you can call Ctxt.Error() to notify the caller for an execution error TOnInterfaceStubExecuteJSON = procedure(Ctxt: TOnInterfaceStubExecuteParamsJSON) of object; /// diverse types of stubbing / mocking rules // - isUndefined is the first, since it will be a ExpectsCount() weak rule // which may be overwritten by the other real run-time rules TInterfaceStubRuleKind = (isUndefined, isExecutesJSON, {$ifndef NOVARIANTS}isExecutesVariant, {$endif} isRaises, isReturns, isFails); /// define a mocking / stubing rule used internaly by TInterfaceStub TInterfaceStubRule = record /// optional expected parameters, serialized as a JSON array // - if equals '', the rule is not parametrized - i.e. it will be the // default for this method Params: RawUTF8; /// values associated to the rule // - for TInterfaceStub.Executes(), is the aEventParams parameter transmitted // to Execute event handler (could be used to e.g. customize the handler) // - for TInterfaceStub.Raises(), is the Exception.Message associated // to one ExceptionClass // - for TInterfaceStub.Returns(), is the returned result, serialized as a // JSON array (including var / out parameters then any function result) // - for TInterfaceStub.Fails() is the returned error message for // TInterfaceStub exception or TInterfaceMock associated test case Values: RawUTF8; /// the type of this rule // - isUndefined is used for a TInterfaceStub.ExpectsCount() weak rule Kind: TInterfaceStubRuleKind; /// the event handler to be executed // - for TInterfaceStub.Executes(), Values is transmitted as aResult parameter // - either a TOnInterfaceStubExecuteJSON, or a TOnInterfaceStubExecuteVariant Execute: TMethod; /// the exception class to be raised // - for TInterfaceStub.Raises(), Values contains Exception.Message ExceptionClass: ExceptClass; /// the number of times this rule has been executed RulePassCount: cardinal; /// comparison operator set by TInterfaceStub.ExpectsCount() // - only qoEqualTo..qoGreaterThanOrEqualTo are relevant here ExpectedPassCountOperator: TSQLQueryOperator; /// expected pass count value set by TInterfaceStub.ExpectsCount() // - value to be compared to the number of times this rule has been executed // - TInterfaceStub/TInterfaceMock will check it in their Destroy destructor, // using the comparison stated by ExpectedPassCountOperator ExpectedPassCount: cardinal; /// log trace value set by TInterfaceStub.ExpectsTrace() // - value to be compared to the Hash32() value of the execution log trace // - TInterfaceStub/TInterfaceMock will check it in their Destroy destructor, // using the fLogs[] content ExpectedTraceHash: cardinal; end; /// define the rules for a given method as used internaly by TInterfaceStub {$ifdef USERECORDWITHMETHODS}TInterfaceStubRules = record {$else}TInterfaceStubRules = object{$endif} public /// the mocking / stubing rules associated to this method Rules: array of TInterfaceStubRule; /// index in Rules[] of the default rule, i.e. the one with Params='' DefaultRule: integer; /// the number of times this method has been executed MethodPassCount: cardinal; /// find a rule index from its Params content function FindRuleIndex(const aParams: RawUTF8): integer; /// find a strong rule index from its Params content function FindStrongRuleIndex(const aParams: RawUTF8): integer; /// register a rule procedure AddRule(Sender: TInterfaceStub; aKind: TInterfaceStubRuleKind; const aParams, aValues: RawUTF8; const aEvent: TNotifyEvent=nil; aExceptionClass: ExceptClass=nil; aExpectedPassCountOperator: TSQLQueryOperator=qoNone; aValue: cardinal=0); end; /// diverse options available to TInterfaceStub // - by default, method execution stack is not recorded - include // imoLogMethodCallsAndResults in the options to track all method calls // and the returned values; note that ExpectsTrace() method will set it // - by default, TInterfaceStub will be released when the stubed/mocked // interface is released - include imoFakeInstanceWontReleaseTInterfaceStub // in the options to force manual memory handling of TInterfaceStubs // - by default, all interfaces will return some default values, unless // imoRaiseExceptionIfNoRuleDefined or imoReturnErrorIfNoRuleDefined is // included in the options // - by default, any TInterfaceMock.Fails() rule execution will notify the // TSynTestCase, unless imoMockFailsWillPassTestCase which will let test pass TInterfaceStubOption = ( imoLogMethodCallsAndResults, imoFakeInstanceWontReleaseTInterfaceStub, imoRaiseExceptionIfNoRuleDefined, imoReturnErrorIfNoRuleDefined, imoMockFailsWillPassTestCase); /// set of options available to TInterfaceStub TInterfaceStubOptions = set of TInterfaceStubOption; /// every potential part of TInterfaceStubLog.AddAsText() log entry TInterfaceStubLogLayout = (wName, wParams, wResults); /// set the output layout of TInterfaceStubLog.AddAsText() log entry TInterfaceStubLogLayouts = set of TInterfaceStubLogLayout; /// used to keep track of one stubbed method call {$ifdef USERECORDWITHMETHODS}TInterfaceStubLog = record {$else}TInterfaceStubLog = object{$endif} public /// call timestamp, in milliseconds // - is filled with GetTickCount64() API returned value Timestamp64: Int64; /// set to TRUE if this calls failed // - i.e. if EInterfaceFactoryException was raised for TInterfaceStub, or // if TInterfaceMock did notify its associated TSynTestCase via a Check() // - CustomResults/Results will contain the error message WasError: boolean; /// the method called // - a pointer to the existing information in shared TInterfaceFactory Method: PServiceMethod; /// the parameters at execution call, as JSON CSV (i.e. array without [ ]) Params: RawUTF8; /// any non default result returned after execution // - if not set (i.e. if equals ''), Method^.DefaultResult has been returned // - if WasError is TRUE, always contain the error message CustomResults: RawUTF8; /// the result returned after execution // - this method will return Method^.DefaultResult if CustomResults='' function Results: RawUTF8; /// append the log in textual format // - typical output is as such: // $ Add(10,20)=[30], // or, if WasError is TRUE: // $ Divide(20,0) error "divide by zero", procedure AddAsText(WR: TTextWriter; aScope: TInterfaceStubLogLayouts; SepChar: AnsiChar=','); end; /// used to keep track of all stubbed methods calls TInterfaceStubLogDynArray = array of TInterfaceStubLog; /// used to stub an interface implementation // - define the expected workflow in a fluent interface using Executes / // Fails / Returns / Raises // - this class will be inherited by TInterfaceMock which will contain some // additional methods dedicated to mocking behavior (e.g. including in tests) // - each instance of this class will be owned by its generated fake // implementation class (retrieved at constructor out parameter): when the // stubed/mocked interface is freed, its associated TInterfaceStub will be // freed - so you do not need to protect TInterfaceStub.Create with a // try..finally clause, since it will be released when no more needed // - inherits from TInterfaceResolver so match TInjectableObject expectations TInterfaceStub = class(TInterfaceResolver) protected fInterface: TInterfaceFactory; fRules: array of TInterfaceStubRules; fOptions: TInterfaceStubOptions; fHasExpects: set of (eCount,eTrace); fLogs: TInterfaceStubLogDynArray; fLog: TDynArray; fLogCount: integer; fInterfaceExpectedTraceHash: cardinal; fLastInterfacedObjectFake: TInterfacedObject; function TryResolve(aInterface: PTypeInfo; out Obj): boolean; override; function Implements(aInterface: PTypeInfo): boolean; override; procedure InternalGetInstance(out aStubbedInterface); virtual; function InternalCheck(aValid,aExpectationFailed: boolean; const aErrorMsgFmt: RawUTF8; const aErrorMsgArgs: array of const): boolean; virtual; // match TOnFakeInstanceInvoke callback signature function Invoke(const aMethod: TServiceMethod; const aParams: RawUTF8; aResult, aErrorMsg: PRawUTF8; aClientDrivenID: PCardinal; aServiceCustomAnswer: PServiceCustomAnswer): boolean; // will launch InternalCheck() process if some expectations defined by // ExpectsCount() are not met, i.e. raise an exception for TInterfaceStub // or notify the associated test case for TInterfaceMock procedure InstanceDestroyed(aClientDrivenID: cardinal); procedure IntSetOptions(Options: TInterfaceStubOptions); virtual; procedure IntCheckCount(aMethodIndex, aComputed: cardinal; aOperator: TSQLQueryOperator; aCount: cardinal); function IntGetLogAsText(asmndx: integer; const aParams: RawUTF8; aScope: TInterfaceStubLogLayouts; SepChar: AnsiChar): RawUTF8; function GetLogHash: cardinal; procedure OnExecuteToLog(Ctxt: TOnInterfaceStubExecuteParamsVariant); public /// low-level internal constructor // - you should not call this method, but the overloaded alternatives constructor Create(aFactory: TInterfaceFactory; const aInterfaceName: RawUTF8); reintroduce; overload; virtual; /// initialize an interface stub from TypeInfo(IMyInterface) // - assign the fake class instance to a stubbed interface variable: // !var I: ICalculator; // ! TInterfaceStub.Create(TypeInfo(ICalculator),I); // ! Check(I.Add(10,20)=0,'Default result'); constructor Create(aInterface: PTypeInfo; out aStubbedInterface); reintroduce; overload; /// initialize an interface stub from an interface GUID // - you shall have registered the interface by a previous call to // ! TInterfaceFactory.RegisterInterfaces([TypeInfo(IMyInterface),...]) // - once registered, create and use the fake class instance as such: // !var I: ICalculator; // ! TInterfaceStub.Create(ICalculator,I); // ! Check(I.Add(10,20)=0,'Default result'); // - if the supplied TGUID has not been previously registered, raise an Exception constructor Create(const aGUID: TGUID; out aStubbedInterface); reintroduce; overload; /// initialize an interface stub from an interface name (e.g. 'IMyInterface') // - you shall have registered the interface by a previous call to // TInterfaceFactory.Get(TypeInfo(IMyInterface)) or RegisterInterfaces([]) // - if the supplied name has not been previously registered, raise an Exception constructor Create(const aInterfaceName: RawUTF8; out aStubbedInterface); reintroduce; overload; /// prepare an interface stub from TypeInfo(IMyInterface) for later injection // - create several TInterfaceStub instances for a given TInjectableObject // ! procedure TMyTestCase.OneTestCaseMethod; // ! var Test: IServiceToBeTested; // ! begin // ! Test := TServiceToBeTested.CreateInjected([], // ! TInterfaceStub.Create(TypeInfo(ICalculator)), // ! TInterfaceMock.Create(TypeInfo(IPersistence),self). // ! ExpectsCount('SaveItem',qoEqualTo,1)]); constructor Create(aInterface: PTypeInfo); reintroduce; overload; /// prepare an interface stub from a given TGUID for later injection // - you shall have registered the interface by a previous call to // ! TInterfaceFactory.RegisterInterfaces([TypeInfo(IMyInterface),...]) // - then create TInterfaceStub instances for a given TInjectableObject: // ! procedure TMyTestCase.OneTestCaseMethod; // ! var Test: IServiceToBeTested; // ! begin // ! Test := TServiceToBeTested.CreateInjected( // ! [IMyInterface], // ! TInterfaceMock.Create(IPersistence,self). // ! ExpectsCount('SaveItem',qoEqualTo,1)]); constructor Create(const aGUID: TGUID); reintroduce; overload; /// add an execution rule for a given method, with JSON marshalling // - optional aEventParams parameter will be transmitted to aEvent handler // - raise an Exception if the method name does not exist for this interface function Executes(const aMethodName: RawUTF8; aEvent: TOnInterfaceStubExecuteJSON; const aEventParams: RawUTF8=''): TInterfaceStub; overload; /// add an execution rule for a given method and a set of parameters, // with JSON marshalling // - if execution context matches the supplied aParams value, aEvent is triggered // - optional aEventParams parameter will be transmitted to aEvent handler // - raise an Exception if the method name does not exist for this interface function Executes(const aMethodName, aParams: RawUTF8; aEvent: TOnInterfaceStubExecuteJSON; const aEventParams: RawUTF8=''): TInterfaceStub; overload; /// add an execution rule for a given method and a set of parameters, // with JSON marshalling // - if execution context matches the supplied aParams value, aEvent is triggered // - optional aEventParams parameter will be transmitted to aEvent handler // - raise an Exception if the method name does not exist for this interface function Executes(const aMethodName: RawUTF8; const aParams: array of const; aEvent: TOnInterfaceStubExecuteJSON; const aEventParams: RawUTF8=''): TInterfaceStub; overload; {$ifndef NOVARIANTS} /// add an execution rule for a given method, with Variant marshalling // - optional aEventParams parameter will be transmitted to aEvent handler // - raise an Exception if the method name does not exist for this interface function Executes(const aMethodName: RawUTF8; aEvent: TOnInterfaceStubExecuteVariant; const aEventParams: RawUTF8=''): TInterfaceStub; overload; /// add an execution rule for a given method and a set of parameters, // with Variant marshalling // - if execution context matches the supplied aParams value, aEvent is triggered // - optional aEventParams parameter will be transmitted to aEvent handler // - raise an Exception if the method name does not exist for this interface function Executes(const aMethodName, aParams: RawUTF8; aEvent: TOnInterfaceStubExecuteVariant; const aEventParams: RawUTF8=''): TInterfaceStub; overload; /// add an execution rule for a given method and a set of parameters, // with Variant marshalling // - if execution context matches the supplied aParams value, aEvent is triggered // - optional aEventParams parameter will be transmitted to aEvent handler // - raise an Exception if the method name does not exist for this interface function Executes(const aMethodName: RawUTF8; const aParams: array of const; aEvent: TOnInterfaceStubExecuteVariant; const aEventParams: RawUTF8=''): TInterfaceStub; overload; /// add an execution rule for all methods, with Variant marshalling // - optional aEventParams parameter will be transmitted to aEvent handler // - callback's Ctxt: TOnInterfaceStubExecuteParamsVariant's Method field // will identify the executed method function Executes(aEvent: TOnInterfaceStubExecuteVariant; const aEventParams: RawUTF8=''): TInterfaceStub; overload; /// will add execution rules for all methods to log the input parameters // - aKind will define how the input parameters are serialized in JSON function Executes(aLog: TSynLogClass; aLogLevel: TSynLogInfo; aKind: TServiceMethodParamsDocVariantKind): TInterfaceStub; overload; {$endif} /// add an exception rule for a given method // - will create and raise the specified exception for this method // - raise an Exception if the method name does not exist for this interface function Raises(const aMethodName: RawUTF8; aException: ExceptClass; const aMessage: string): TInterfaceStub; overload; /// add an exception rule for a given method and a set of parameters // - will create and raise the specified exception for this method, if the // execution context matches the supplied aParams value // - raise an Exception if the method name does not exist for this interface function Raises(const aMethodName, aParams: RawUTF8; aException: ExceptClass; const aMessage: string): TInterfaceStub; overload; /// add an exception rule for a given method and a set of parameters // - will create and raise the specified exception for this method, if the // execution context matches the supplied aParams value // - raise an Exception if the method name does not exist for this interface function Raises(const aMethodName: RawUTF8; const aParams: array of const; aException: ExceptClass; const aMessage: string): TInterfaceStub; overload; /// add an evaluation rule for a given method // - aExpectedResults JSON array will be returned to the caller // - raise an Exception if the method name does not exist for this interface function Returns(const aMethodName, aExpectedResults: RawUTF8): TInterfaceStub; overload; /// add an evaluation rule for a given method // - aExpectedResults will be returned to the caller after conversion to // a JSON array // - raise an Exception if the method name does not exist for this interface function Returns(const aMethodName: RawUTF8; const aExpectedResults: array of const): TInterfaceStub; overload; /// add an evaluation rule for a given method and a set of parameters // - aExpectedResults JSON array will be returned to the caller // - raise an Exception if the method name does not exist for this interface function Returns(const aMethodName, aParams, aExpectedResults: RawUTF8): TInterfaceStub; overload; /// add an evaluation rule for a given method and a set of parameters // - aExpectedResults JSON array will be returned to the caller // - raise an Exception if the method name does not exist for this interface function Returns(const aMethodName: RawUTF8; const aParams, aExpectedResults: array of const): TInterfaceStub; overload; /// add an error rule for a given method // - an error will be returned to the caller, with aErrorMsg as message // - it will raise EInterfaceFactoryException for TInterfaceStub, but // TInterfaceMock will push the failure to the associated test case // - raise an Exception if the method name does not exist for this interface function Fails(const aMethodName, aErrorMsg: RawUTF8): TInterfaceStub; overload; /// add an error rule for a given method and a set of parameters // - an error will be returned to the caller, with aErrorMsg as message // - it will raise EInterfaceFactoryException for TInterfaceStub, but // TInterfaceMock will push the failure to the associated test case // - raise an Exception if the method name does not exist for this interface function Fails(const aMethodName, aParams, aErrorMsg: RawUTF8): TInterfaceStub; overload; /// add an error rule for a given method and a set of parameters // - an error will be returned to the caller, with aErrorMsg as message // - it will raise EInterfaceFactoryException for TInterfaceStub, but // TInterfaceMock will push the failure to the associated test case // - raise an Exception if the method name does not exist for this interface function Fails(const aMethodName: RawUTF8; const aParams: array of const; const aErrorMsg: RawUTF8): TInterfaceStub; overload; /// add a pass count expectation rule for a given method // - those rules will be evaluated at Destroy execution // - only qoEqualTo..qoGreaterThanOrEqualTo are relevant here // - it will raise EInterfaceFactoryException for TInterfaceStub, but // TInterfaceMock will push the failure to the associated test case // - raise an Exception if the method name does not exist for this interface function ExpectsCount(const aMethodName: RawUTF8; aOperator: TSQLQueryOperator; aValue: cardinal): TInterfaceStub; overload; /// add a pass count expectation rule for a given method and a set of parameters // - those rules will be evaluated at Destroy execution // - only qoEqualTo..qoGreaterThanOrEqualTo are relevant here // - it will raise EInterfaceFactoryException for TInterfaceStub, but // TInterfaceMock will push the failure to the associated test case // - raise an Exception if the method name does not exist for this interface function ExpectsCount(const aMethodName, aParams: RawUTF8; aOperator: TSQLQueryOperator; aValue: cardinal): TInterfaceStub; overload; /// add a pass count expectation rule for a given method and a set of parameters // - those rules will be evaluated at Destroy execution // - only qoEqualTo..qoGreaterThanOrEqualTo are relevant here // - it will raise EInterfaceFactoryException for TInterfaceStub, but // TInterfaceMock will push the failure to the associated test case // - raise an Exception if the method name does not exist for this interface function ExpectsCount(const aMethodName: RawUTF8; const aParams: array of const; aOperator: TSQLQueryOperator; aValue: cardinal): TInterfaceStub; overload; /// add a hash-based execution expectation rule for the whole interface // - those rules will be evaluated at Destroy execution // - supplied aValue is a Hash32() of the trace in LogAsText format // - it will raise EInterfaceFactoryException for TInterfaceStub, but // TInterfaceMock will push the failure to the associated test case function ExpectsTrace(aValue: cardinal): TInterfaceStub; overload; /// add a hash-based execution expectation rule for a given method // - those rules will be evaluated at Destroy execution // - supplied aValue is a Hash32() of the trace in LogAsText format // - it will raise EInterfaceFactoryException for TInterfaceStub, but // TInterfaceMock will push the failure to the associated test case // - raise an Exception if the method name does not exist for this interface function ExpectsTrace(const aMethodName: RawUTF8; aValue: cardinal): TInterfaceStub; overload; /// add a hash-based execution expectation rule for a given method // and a set of parameters // - those rules will be evaluated at Destroy execution // - supplied aValue is a Hash32() of the trace in LogAsText format // - it will raise EInterfaceFactoryException for TInterfaceStub, but // TInterfaceMock will push the failure to the associated test case // - raise an Exception if the method name does not exist for this interface function ExpectsTrace(const aMethodName, aParams: RawUTF8; aValue: cardinal): TInterfaceStub; overload; /// add a hash-based execution expectation rule for a given method // and a set of parameters // - those rules will be evaluated at Destroy execution // - supplied aValue is a Hash32() of the trace in LogAsText format // - it will raise EInterfaceFactoryException for TInterfaceStub, but // TInterfaceMock will push the failure to the associated test case // - raise an Exception if the method name does not exist for this interface function ExpectsTrace(const aMethodName: RawUTF8; const aParams: array of const; aValue: cardinal): TInterfaceStub; overload; /// add a JSON-based execution expectation rule for the whole interface // - those rules will be evaluated at Destroy execution // - supplied aValue is the trace in LogAsText format // - it will raise EInterfaceFactoryException for TInterfaceStub, but // TInterfaceMock will push the failure to the associated test case function ExpectsTrace(const aValue: RawUTF8): TInterfaceStub; overload; /// add a JSON-based execution expectation rule for a given method // - those rules will be evaluated at Destroy execution // - supplied aValue is the trace in LogAsText format // - it will raise EInterfaceFactoryException for TInterfaceStub, but // TInterfaceMock will push the failure to the associated test case // - raise an Exception if the method name does not exist for this interface function ExpectsTrace(const aMethodName, aValue: RawUTF8): TInterfaceStub; overload; /// add a JSON-based execution expectation rule for a given method // and a set of parameters // - those rules will be evaluated at Destroy execution // - supplied aValue is the trace in LogAsText format // - it will raise EInterfaceFactoryException for TInterfaceStub, but // TInterfaceMock will push the failure to the associated test case // - raise an Exception if the method name does not exist for this interface function ExpectsTrace(const aMethodName, aParams, aValue: RawUTF8): TInterfaceStub; overload; /// add a JSON-based execution expectation rule for a given method // and a set of parameters // - those rules will be evaluated at Destroy execution // - supplied aValue is the trace in LogAsText format // - it will raise EInterfaceFactoryException for TInterfaceStub, but // TInterfaceMock will push the failure to the associated test case // - raise an Exception if the method name does not exist for this interface function ExpectsTrace(const aMethodName: RawUTF8; const aParams: array of const; const aValue: RawUTF8): TInterfaceStub; overload; /// set the optional stubing/mocking options // - same as the Options property, but in a fluent-style interface function SetOptions(Options: TInterfaceStubOptions): TInterfaceStub; /// reset the internal trace // - Log, LogAsText, LogHash and LogCount will be initialized procedure ClearLog; /// the stubbed method execution trace items property Log: TInterfaceStubLogDynArray read fLogs; /// the stubbed method execution trace converted as text // - typical output is a list of calls separated by commas: // $ Add(10,20)=[30],Divide(20,0) error "divide by zero" function LogAsText(SepChar: AnsiChar=','): RawUTF8; /// returns the last created TInterfacedObject instance // - e.g. corresponding to the out aStubbedInterface parameter of Create() property LastInterfacedObjectFake: TInterfacedObject read fLastInterfacedObjectFake; published /// access to the registered Interface RTTI information property InterfaceFactory: TInterfaceFactory read fInterface; /// optional stubing/mocking options // - you can use the SetOptions() method in a fluent-style interface property Options: TInterfaceStubOptions read fOptions write IntSetOptions; /// the stubbed method execution trace number of items property LogCount: Integer read fLogCount; /// the stubbed method execution trace converted as one numerical hash // - returns Hash32(LogAsText) property LogHash: cardinal read GetLogHash; end; /// used to mock an interface implementation via expect-run-verify pattern // - TInterfaceStub will raise an exception on Fails(), ExpectsCount() or // ExpectsTrace() rule activation, but TInterfaceMock will call // TSynTestCase.Check() with no exception with such rules, as expected by // a mocked interface // - this class will follow the expect-run-verify pattern, i.e. expectations // are defined before running the test, and verification is performed // when the instance is released - use TInterfaceMockSpy if you prefer the // more explicit run-verify pattern TInterfaceMock = class(TInterfaceStub) protected fTestCase: TSynTestCase; function InternalCheck(aValid,aExpectationFailed: boolean; const aErrorMsgFmt: RawUTF8; const aErrorMsgArgs: array of const): boolean; override; public /// initialize an interface mock from TypeInfo(IMyInterface) // - aTestCase.Check() will be called in case of mocking failure // ! procedure TMyTestCase.OneTestCaseMethod; // ! var Persist: IPersistence; // ! ... // ! TInterfaceMock.Create(TypeInfo(IPersistence),Persist,self). // ! ExpectsCount('SaveItem',qoEqualTo,1)]); constructor Create(aInterface: PTypeInfo; out aMockedInterface; aTestCase: TSynTestCase); reintroduce; overload; /// initialize an interface mock from an interface TGUID // - aTestCase.Check() will be called during validation of all Expects*() // - you shall have registered the interface by a previous call to // ! TInterfaceFactory.RegisterInterfaces([TypeInfo(IPersistence),...]) // - once registered, create and use the fake class instance as such: // !procedure TMyTestCase.OneTestCaseMethod; // !var Persist: IPersistence; // ! ... // ! TInterfaceMock.Create(IPersistence,Persist,self). // ! ExpectsCount('SaveItem',qoEqualTo,1)]); // - if the supplied TGUID has not been previously registered, raise an Exception constructor Create(const aGUID: TGUID; out aMockedInterface; aTestCase: TSynTestCase); reintroduce; overload; /// initialize an interface mock from an interface name (e.g. 'IMyInterface') // - aTestCase.Check() will be called in case of mocking failure // - you shall have registered the interface by a previous call to // TInterfaceFactory.Get(TypeInfo(IMyInterface)) or RegisterInterfaces() // - if the supplied name has not been previously registered, raise an Exception constructor Create(const aInterfaceName: RawUTF8; out aMockedInterface; aTestCase: TSynTestCase); reintroduce; overload; /// initialize an interface mock from TypeInfo(IMyInterface) for later injection // - aTestCase.Check() will be called in case of mocking failure constructor Create(aInterface: PTypeInfo; aTestCase: TSynTestCase); reintroduce; overload; /// initialize an interface mock from TypeInfo(IMyInterface) for later injection // - aTestCase.Check() will be called in case of mocking failure constructor Create(const aGUID: TGUID; aTestCase: TSynTestCase); reintroduce; overload; /// the associated test case property TestCase: TSynTestCase read fTestCase; end; /// how TInterfaceMockSpy.Verify() shall generate the calls trace TInterfaceMockSpyCheck = (chkName, chkNameParams, chkNameParamsResults); /// used to mock an interface implementation via run-verify pattern // - this class will implement a so called "test-spy" mocking pattern, i.e. // no expectation is to be declared at first, but all calls are internally // logged (i.e. it force imoLogMethodCallsAndResults option to be defined), // and can afterwards been check via Verify() calls TInterfaceMockSpy = class(TInterfaceMock) protected procedure IntSetOptions(Options: TInterfaceStubOptions); override; public /// this will set and force imoLogMethodCallsAndResults option as needed // - you should not call this method, but the overloaded alternatives constructor Create(aFactory: TInterfaceFactory; const aInterfaceName: RawUTF8); override; /// check that a method has been called a specify number of times procedure Verify(const aMethodName: RawUTF8; aOperator: TSQLQueryOperator=qoGreaterThan; aCount: cardinal=0); overload; /// check a method calls count with a set of parameters // - parameters shall be defined as a JSON array of values procedure Verify(const aMethodName, aParams: RawUTF8; aOperator: TSQLQueryOperator=qoGreaterThan; aCount: cardinal=0); overload; /// check a method calls count with a set of parameters // - parameters shall be defined as a JSON array of values procedure Verify(const aMethodName: RawUTF8; const aParams: array of const; aOperator: TSQLQueryOperator=qoGreaterThan; aCount: cardinal=0); overload; /// check an execution trace for the global interface // - text trace format shall follow method calls, e.g. // ! Verify('Multiply,Add',chkName); // or may include parameters: // ! Verify('Multiply(10,30),Add(2,35)',chkNameParams); // or include parameters and function results: // ! Verify('Multiply(10,30)=[300],Add(2,35)=[37]',chkNameParamsResults); procedure Verify(const aTrace: RawUTF8; aScope: TInterfaceMockSpyCheck); overload; /// check an execution trace for a specified method // - text trace format will follow specified scope, e.g. // ! Verify('Add','(10,30),(2,35)',chkNameParams); // or include parameters and function results: // ! Verify('Add','(10,30)=[300],(2,35)=[37]',chkNameParamsResults); // - if aMethodName does not exists or aScope=chkName, will raise an exception procedure Verify(const aMethodName, aTrace: RawUTF8; aScope: TInterfaceMockSpyCheck); overload; /// check an execution trace for a specified method and parameters // - text trace format shall contain only results, e.g. // ! Verify('Add','2,35','[37]'); procedure Verify(const aMethodName, aParams, aTrace: RawUTF8); overload; /// check an execution trace for a specified method and parameters // - text trace format shall contain only results, e.g. // ! Verify('Add',[2,35],'[37]'); procedure Verify(const aMethodName: RawUTF8; const aParams: array of const; const aTrace: RawUTF8); overload; end; {$M+} /// an abstract service provider, as registered in TServiceContainer // - each registered interface has its own TServiceFactory instance, available // as one TSQLServiceContainer item from TSQLRest.Services property // - this will be either implemented by a registered TInterfacedObject on the // server, or by a on-the-fly generated fake TInterfacedObject class // communicating via JSON on a client // - TSQLRestServer will have to register an interface implementation as: // ! Server.ServiceRegister(TServiceCalculator,[TypeInfo(ICalculator)],sicShared); // - TSQLRestClientURI will have to register an interface remote access as: // ! Client.ServiceRegister([TypeInfo(ICalculator)],sicShared)); // note that the implementation (TServiceCalculator) remain on the server side // only: the client only needs the ICalculator interface // - then TSQLRestServer and TSQLRestClientURI will both have access to the // service, via their Services property, e.g. as: // !var I: ICalculator; // !... // ! if Services.Info(ICalculator).Get(I) then // ! result := I.Add(10,20); // which is in practice to be used with the faster wrapper method: // ! if Services.Resolve(ICalculator,I) then // ! result := I.Add(10,20); TServiceFactory = class protected fInterface: TInterfaceFactory; fInterfaceURI: RawUTF8; fInterfaceMangledURI: RawUTF8; fInstanceCreation: TServiceInstanceImplementation; fRest: TSQLRest; fSharedInstance: TInterfacedObject; fContract: RawUTF8; fContractHash: RawUTF8; fContractExpected: RawUTF8; // per-method execution rights fExecution: array of TServiceFactoryExecution; /// union of all fExecution[].Options fAnyOptions: TServiceMethodOptions; procedure ExecutionAction(const aMethod: array of RawUTF8; aOptions: TServiceMethodOptions; aAction: TServiceMethodOptionsAction); function GetInterfaceTypeInfo: PTypeInfo; {$ifdef HASINLINE}inline;{$endif} function GetInterfaceIID: TGUID; {$ifdef HASINLINE}inline;{$endif} public /// initialize the service provider parameters // - it will check and retrieve all methods of the supplied interface, // and prepare all internal structures for its serialized execution constructor Create(aRest: TSQLRest; aInterface: PTypeInfo; aInstanceCreation: TServiceInstanceImplementation; const aContractExpected: RawUTF8); /// retrieve an instance of this interface // - this virtual method will be overridden to reflect the expected // behavior of client or server side // - can be used as such to resolve an I: ICalculator interface: // ! var I: ICalculator; // ! begin // ! if fClient.Services.Info(TypeInfo(ICalculator)).Get(I) then // ! ... use I function Get(out Obj): Boolean; virtual; abstract; /// retrieve the published signature of this interface // - is always available on TServiceFactoryServer, but TServiceFactoryClient // will be able to retrieve it only if TServiceContainerServer.PublishSignature // is set to TRUE (which is not the default setting, for security reasons) function RetrieveSignature: RawUTF8; virtual; abstract; /// the associated RESTful instance property Rest: TSQLRest read fRest; /// access to the registered Interface RTTI information property InterfaceFactory: TInterfaceFactory read fInterface; /// the registered Interface low-level Delphi RTTI type // - just maps InterfaceFactory.InterfaceTypeInfo property InterfaceTypeInfo: PTypeInfo read GetInterfaceTypeInfo; /// the registered Interface GUID // - just maps InterfaceFactory.InterfaceIID property InterfaceIID: TGUID read GetInterfaceIID; (*/ the service contract, serialized as a JSON object - a "contract" is in fact the used interface signature, i.e. its implementation mode (InstanceCreation) and all its methods definitions - a possible value for a one-method interface defined as such: ! function ICalculator.Add(n1,n2: integer): integer; may be returned as the following JSON object: $ {"contract":"Calculator","implementation":"shared", $ "methods":[{"method":"Add", $ "arguments":[{"argument":"Self","direction":"in","type":"self"}, $ {"argument":"n1","direction":"in","type":"integer"}, $ {"argument":"n2","direction":"in","type":"integer"}, $ {"argument":"Result","direction":"out","type":"integer"} $ ]}]} *) property Contract: RawUTF8 read fContract; /// the published service contract, as expected by both client and server // - by default, will contain ContractHash property value (for security) // - but you can override this value using plain Contract or any custom // value (e.g. a custom version number) - in this case, both TServiceFactoryClient // and TServiceFactoryServer instances must have a matching ContractExpected // - this value is returned by a '_contract_' pseudo-method name, with the URI: // $ POST /root/Interface._contract_ // or (if TSQLRestRoutingJSON_RPC is used): // $ POST /root/Interface // $ (...) // $ {"method":"_contract_","params":[]} // (e.g. to be checked in TServiceFactoryClient.Create constructor) // - if set to SERVICE_CONTRACT_NONE_EXPECTED (i.e. '*'), the client won't // check and ask the server contract for consistency: it may be used e.g. // for accessing a plain REST HTTP server which is not based on mORMot, // so may not implement POST /root/Interface._contract_ property ContractExpected: RawUTF8 read fContractExpected write fContractExpected; published /// the registered Interface URI // - in fact this is the Interface name without the initial 'I', e.g. // 'Calculator' for ICalculator property InterfaceURI: RawUTF8 read fInterfaceURI; /// the registered Interface mangled URI // - in fact this is encoding the GUID using BinToBase64URI(), e.g. // ! ['{c9a646d3-9c61-4cb7-bfcd-ee2522c8f633}'] into '00amyWGct0y_ze4lIsj2Mw' // - can be substituted to the clear InterfaceURI name property InterfaceMangledURI: RawUTF8 read fInterfaceMangledURI; /// how each class instance is to be created // - only relevant on the server side; on the client side, this class will // be accessed only to retrieve a remote access instance, i.e. sicSingle property InstanceCreation: TServiceInstanceImplementation read fInstanceCreation; /// a hash of the service contract, serialized as a JSON string // - this may be used instead of the JSON signature, to enhance security // (i.e. if you do not want to publish the available methods, but want // to check for the proper synchronization of both client and server) // - a possible value may be: "C351335A7406374C" property ContractHash: RawUTF8 read fContractHash; end; {$M-} /// server-side service provider uses this to store one internal instance // - used by TServiceFactoryServer in sicClientDriven, sicPerSession, // sicPerUser or sicPerGroup mode {$ifdef USERECORDWITHMETHODS}TServiceFactoryServerInstance = record {$else}TServiceFactoryServerInstance = object{$endif} public /// the internal Instance ID, as remotely sent in "id":1 // - is set to 0 when an entry in the array is free InstanceID: PtrUInt; /// GetTickCount64() timestamp corresponding to the last access of // this instance LastAccess64: Int64; /// the associated client session Session: cardinal; /// the implementation instance itself Instance: TInterfacedObject; /// used to release the implementation instance // - direct FreeAndNil(Instance) may lead to A/V if self has been assigned // to an interface to any sub-method on the server side -> dec(RefCount) procedure SafeFreeInstance(Factory: TServiceFactoryServer); end; /// server-side service provider uses this to store its internal instances // - used by TServiceFactoryServer in sicClientDriven, sicPerSession, // sicPerUser or sicPerGroup mode TServiceFactoryServerInstanceDynArray = array of TServiceFactoryServerInstance; /// callback called before any interface-method service execution to allow // its execution // - see Ctxt.Service, Ctxt.ServiceMethodIndex and Ctxt.ServiceParameters // to identify the executed method context // - Method parameter will help identify easily the corresponding method, and // will contain in fact PServiceMethod(Ctxt.ServiceMethod)^ // - should return TRUE if the method can be executed // - should return FALSE if the method should not be executed, and set the // corresponding error to the supplied context e.g. // ! Ctxt.Error('Unauthorized method',HTTP_NOTALLOWED); // - i.e. called by TSQLRestServerURIContext.InternalExecuteSOAByInterface TOnServiceCanExecute = function(Ctxt: TSQLRestServerURIContext; const Method: TServiceMethod): boolean of object; /// callbacked used by TServiceFactoryServer.RunOnAllInstances method TOnServiceFactoryServerOne = function(Sender: TServiceFactoryServer; var Instance: TServiceFactoryServerInstance; var Opaque): integer of object; /// a service provider implemented on the server side // - each registered interface has its own TServiceFactoryServer instance, // available as one TSQLServiceContainerServer item from TSQLRest.Services property // - will handle the implementation class instances of a given interface // - by default, all methods are allowed to execution: you can call AllowAll, // DenyAll, Allow or Deny in order to specify your exact security policy TServiceFactoryServer = class(TServiceFactory) protected fInstances: TServiceFactoryServerInstanceDynArray; fInstance: TDynArray; fInstanceCapacity: integer; // some void entries may have P^.InstanceID=0 fInstanceCount: integer; fInstanceCurrentID: TID; fInstanceTimeOut: cardinal; fInstanceLock: TRTLCriticalSection; fStats: TSynMonitorInputOutputObjArray; fImplementationClass: TInterfacedClass; fImplementationClassKind: (ickBlank, ickWithCustomCreate, ickInjectable, ickInjectableRest, ickFromInjectedResolver, ickFake); fImplementationClassInterfaceEntry: PInterfaceEntry; fSharedInterface: IInterface; fByPassAuthentication: boolean; fResultAsJSONObject: boolean; fResultAsJSONObjectWithoutResult: boolean; fResultAsXMLObject: boolean; fResultAsJSONObjectIfAccept: boolean; fResultAsXMLObjectNameSpace: RawUTF8; fExcludeServiceLogCustomAnswer: boolean; fBackgroundThread: TSynBackgroundThreadMethod; fOnMethodExecute: TOnServiceCanExecute; fOnExecute: array of TServiceMethodExecuteEvent; procedure SetServiceLogByIndex(const aMethods: TInterfaceFactoryMethodBits; aLogRest: TSQLRest; aLogClass: TSQLRecordServiceLogClass); procedure SetTimeoutSecInt(value: cardinal); function GetTimeoutSec: cardinal; function GetStat(const aMethod: RawUTF8): TSynMonitorInputOutput; // from client CacheFlush/_ping_ function RenewSession(aSession: cardinal): integer; /// get an implementation Inst.Instance for the given Inst.InstanceID // - is called by ExecuteMethod() in sicClientDrive mode // - returns -1 on error, or aMethodIndex for successfull execution, // e.g. 0 after {"method":"_free_".. call // - otherwise, fill Inst.Instance with the matching implementation (or nil) function InternalInstanceRetrieve(var Inst: TServiceFactoryServerInstance; aMethodIndex,aSession: integer): integer; /// call a given method of this service provider // - here Ctxt.ServiceMethod points to the corresponding fInterface.Methods[] // (i.e. excluding _free_/_contract_/_signature_ pseudo-methods) // - Ctxt.ServiceMethodIndex=0=ord(imFree) will free/release // the corresponding aInstanceID - as called e.g. from // $ {"method":"_free_", "params":[], "id":1234} // - Ctxt.ServiceParameters is e.g. '[1,2]' i.e. a true JSON array, which // will contain the incoming parameters in the same exact order than the // corresponding implemented interface method // - Ctxt.ID is an optional number, to be used in case of sicClientDriven // kind of Instance creation to identify the corresponding client session // - returns 200/HTTP_SUCCESS on success, or an HTTP error status, with an // optional error message in aErrorMsg // - on success, Ctxt.Call.OutBody shall contain a serialized JSON object // with one nested result property, which may be a JSON array, containing // all "var" or "out" parameters values, and then the method main result - // for instance, ExecuteMethod(..,'[1,2]') over ICalculator.Add will return: // $ {"result":[3],"id":0} // the returned "id" number is the Instance identifier to be used for any later // sicClientDriven remote call - or just 0 in case of sicSingle or sicShared procedure ExecuteMethod(Ctxt: TSQLRestServerURIContext); /// called by ExecuteMethod to append input/output params to Sender.TempTextWriter procedure OnLogRestExecuteMethod(Sender: TServiceMethodExecute; Step: TServiceMethodExecuteEventStep); /// this method will create an implementation instance // - reference count will be set to one, in order to allow safe passing // of the instance into an interface, if AndIncreaseRefCount is TRUE // - will handle TInterfacedObjectWithCustomCreate and TInjectableObject // as expected, if necessary function CreateInstance(AndIncreaseRefCount: boolean): TInterfacedObject; public /// initialize the service provider on the server side // - expect an direct server-side implementation class, which may inherit // from plain TInterfacedClass, TInterfacedObjectWithCustomCreate if you // need an overridden constructor, or TInjectableObject to support DI/IoC // - for sicClientDriven, sicPerSession, sicPerUser or sicPerGroup modes, // a time out (in seconds) can be defined (default is 30 minutes) - if the // specified aTimeOutSec is 0, interface will be forced in sicSingle mode // - you should usualy have to call the TSQLRestServer.ServiceRegister() // method instead of calling this constructor directly constructor Create(aRestServer: TSQLRestServer; aInterface: PTypeInfo; aInstanceCreation: TServiceInstanceImplementation; aImplementationClass: TInterfacedClass; const aContractExpected: RawUTF8; aTimeOutSec: cardinal; aSharedInstance: TInterfacedObject); reintroduce; /// release all used memory // - e.g. any internal TServiceFactoryServerInstance instances (any shared // instance, and all still living instances in sicClientDrive mode) destructor Destroy; override; /// allow all methods execution for all TSQLAuthGroup // - all Groups will be affected by this method (on both client and server sides) // - this method returns self in order to allow direct chaining of security // calls, in a fluent interface function AllowAll: TServiceFactoryServer; /// allow all methods execution for the specified TSQLAuthGroup ID(s) // - the specified group ID(s) will be used to authorize remote service // calls from the client side // - you can retrieve a TSQLAuthGroup ID from its identifier, as such: // ! UserGroupID := fServer.MainFieldID(TSQLAuthGroup,'User'); // - this method returns self in order to allow direct chaining of security // calls, in a fluent interface function AllowAllByID(const aGroupID: array of TID): TServiceFactoryServer; /// allow all methods execution for the specified TSQLAuthGroup names // - is just a wrapper around the other AllowAllByID() method, retrieving the // Group ID from its main field // - this method returns self in order to allow direct chaining of security // calls, in a fluent interface function AllowAllByName(const aGroup: array of RawUTF8): TServiceFactoryServer; /// deny all methods execution for all TSQLAuthGroup // - all Groups will be affected by this method (on both client and server sides) // - this method returns self in order to allow direct chaining of security // calls, in a fluent interface function DenyAll: TServiceFactoryServer; /// deny all methods execution for the specified TSQLAuthGroup ID(s) // - the specified group ID(s) will be used to authorize remote service // calls from the client side // - you can retrieve a TSQLAuthGroup ID from its identifier, as such: // ! UserGroupID := fServer.MainFieldID(TSQLAuthGroup,'User'); // - this method returns self in order to allow direct chaining of security // calls, in a fluent interface function DenyAllByID(const aGroupID: array of TID): TServiceFactoryServer; /// dent all methods execution for the specified TSQLAuthGroup names // - is just a wrapper around the other DenyAllByID() method, retrieving the // Group ID from its main field // - this method returns self in order to allow direct chaining of security // calls, in a fluent interface function DenyAllByName(const aGroup: array of RawUTF8): TServiceFactoryServer; /// allow specific methods execution for the all TSQLAuthGroup // - methods names should be specified as an array (e.g. ['Add','Multiply']) // - all Groups will be affected by this method (on both client and server sides) // - this method returns self in order to allow direct chaining of security // calls, in a fluent interface function Allow(const aMethod: array of RawUTF8): TServiceFactoryServer; /// allow specific methods execution for the specified TSQLAuthGroup ID(s) // - methods names should be specified as an array (e.g. ['Add','Multiply']) // - the specified group ID(s) will be used to authorize remote service // calls from the client side // - you can retrieve a TSQLAuthGroup ID from its identifier, as such: // ! UserGroupID := fServer.MainFieldID(TSQLAuthGroup,'User'); // - this method returns self in order to allow direct chaining of security // calls, in a fluent interface function AllowByID(const aMethod: array of RawUTF8; const aGroupID: array of TID): TServiceFactoryServer; /// allow specific methods execution for the specified TSQLAuthGroup name(s) // - is just a wrapper around the other AllowByID() method, retrieving the // Group ID from its main field // - methods names should be specified as an array (e.g. ['Add','Multiply']) // - this method returns self in order to allow direct chaining of security // calls, in a fluent interface function AllowByName(const aMethod: array of RawUTF8; const aGroup: array of RawUTF8): TServiceFactoryServer; /// deny specific methods execution for the all TSQLAuthGroup // - methods names should be specified as an array (e.g. ['Add','Multiply']) // - all Groups will be affected by this method (on both client and server sides) // - this method returns self in order to allow direct chaining of security // calls, in a fluent interface function Deny(const aMethod: array of RawUTF8): TServiceFactoryServer; /// deny specific methods execution for the specified TSQLAuthGroup ID(s) // - methods names should be specified as an array (e.g. ['Add','Multiply']) // - the specified group ID(s) will be used to unauthorize remote service // calls from the client side // - you can retrieve a TSQLAuthGroup ID from its identifier, as such: // ! UserGroupID := fServer.MainFieldID(TSQLAuthGroup,'User'); // - this method returns self in order to allow direct chaining of security // calls, in a fluent interface function DenyByID(const aMethod: array of RawUTF8; const aGroupID: array of TID): TServiceFactoryServer; overload; /// deny specific methods execution for the specified TSQLAuthGroup name(s) // - is just a wrapper around the other DenyByID() method, retrieving the // Group ID from its main field // - methods names should be specified as an array (e.g. ['Add','Multiply']) // - this method returns self in order to allow direct chaining of security // calls, in a fluent interface function DenyByName(const aMethod: array of RawUTF8; const aGroup: array of RawUTF8): TServiceFactoryServer; /// define execution options for a given set of methods // - methods names should be specified as an array (e.g. ['Add','Multiply']) // - if no method name is given (i.e. []), option will be set for all methods // - include optExecInMainThread will force the method(s) to be called within // a RunningThread.Synchronize() call - slower, but thread-safe // - this method returns self in order to allow direct chaining of security // calls, in a fluent interface function SetOptions(const aMethod: array of RawUTF8; aOptions: TServiceMethodOptions; aAction: TServiceMethodOptionsAction=moaReplace): TServiceFactoryServer; /// define the the instance life time-out, in seconds // - for sicClientDriven, sicPerSession, sicPerUser or sicPerGroup modes // - raise an exception for other kind of execution // - this method returns self in order to allow direct chaining of setting // calls for the service, in a fluent interface function SetTimeoutSec(value: cardinal): TServiceFactoryServer; /// log method execution information to a TSQLRecordServiceLog table // - methods names should be specified as an array (e.g. ['Add','Multiply']) // - if no method name is given (i.e. []), option will be set for all methods // - will write to the specified aLogRest instance, and will disable // writing if aLogRest is nil // - will write to a (inherited) TSQLRecordServiceLog table, as available in // TSQLRest's model, unless a dedicated table is specified as aLogClass // - this method returns self in order to allow direct chaining of security // calls, in a fluent interface function SetServiceLog(const aMethod: array of RawUTF8; aLogRest: TSQLRest; aLogClass: TSQLRecordServiceLogClass=nil): TServiceFactoryServer; /// you can define here an event to allow/deny execution of any method // of this service, at runtime property OnMethodExecute: TOnServiceCanExecute read fOnMethodExecute write fOnMethodExecute; /// allow to hook the methods execution // - several events could be registered, and will be called directly // before and after method execution // - if optInterceptInputOutput is defined in Options, then Sender.Input/Output // fields will contain the execution data context when Hook is called // - see OnMethodExecute if you want to implement security features procedure AddInterceptor(const Hook: TServiceMethodExecuteEvent); /// retrieve an instance of this interface from the server side // - sicShared mode will retrieve the shared instance // - sicPerThread mode will retrieve the instance corresponding to the // current running thread // - all other kind of instance creation will behave the same as sicSingle // when accessed directly from this method, i.e. from server side: in fact, // on the server side, there is no notion of client, session, user nor group // - if ServiceContext.Factory is nil (i.e. if there is no other // service context currently associated), this method will also update // ServiceContext.Factory, so that the implementation method will be able // to access the associated TSQLRestServer instance if needed function Get(out Obj): Boolean; override; /// retrieve the published signature of this interface // - is always available on TServiceFactoryServer, but TServiceFactoryClient // will be able to retrieve it only if TServiceContainerServer.PublishSignature // is set to TRUE (which is not the default setting, for security reasons) function RetrieveSignature: RawUTF8; override; /// call the supplied aEvent callback for all class instances implementing // this service function RunOnAllInstances(const aEvent: TOnServiceFactoryServerOne; var aOpaque): integer; /// just type-cast the associated TSQLRest instance to a true TSQLRestServer function RestServer: TSQLRestServer; {$ifdef HASINLINE}inline;{$endif} /// direct access to per-method detailed process statistics // - this Stats[] array follows Interface.Methods[] order // - see Stat[] property to retrieve information about a method by name property Stats: TSynMonitorInputOutputObjArray read fStats; /// retrieve detailed statistics about a method use // - will return a reference to the actual item in Stats[]: caller should // not free the returned instance property Stat[const aMethod: RawUTF8]: TSynMonitorInputOutput read GetStat; published /// the class type used to implement this interface property ImplementationClass: TInterfacedClass read fImplementationClass; /// the instance life time-out, in seconds // - for sicClientDriven, sicPerSession, sicPerUser or sicPerGroup modes // - raise an exception for other kind of execution // - you can also use the SetTimeOutSec() fluent function instead property TimeoutSec: cardinal read GetTimeoutSec write SetTimeoutSecInt; /// set to TRUE disable Authentication method check for the whole interface // - by default (FALSE), all interface-based services will require valid // RESTful authentication (if enabled on the server side); setting TRUE will // disable authentication for all methods of this interface // (e.g. for returning some HTML content from a public URI, or to implement // a public service catalog) property ByPassAuthentication: boolean read fByPassAuthentication write fByPassAuthentication; /// set to TRUE to return the interface's methods result as JSON object // - by default (FALSE), any method execution will return a JSON array with // all VAR/OUT parameters, in order // - TRUE will generate a JSON object instead, with the VAR/OUT parameter // names as field names (and "Result" for any function result) - may be // useful e.g. when working with JavaScript clients // - Delphi clients (i.e. TServiceFactoryClient/TInterfacedObjectFake) will // transparently handle both formats // - this value can be overridden by setting ForceServiceResultAsJSONObject // for a given TSQLRestServerURIContext (e.g. for server-side JavaScript work) property ResultAsJSONObject: boolean read fResultAsJSONObject write fResultAsJSONObject; /// set to TRUE to return the interface's methods result as JSON object // with no '{"result":{...}}' nesting // - could be used e.g. for plain non mORMot REST Client with in sicSingle // or sicShared mode kind of services // - on client side, consider using TSQLRestClientURI.ServiceDefineSharedAPI property ResultAsJSONObjectWithoutResult: boolean read fResultAsJSONObjectWithoutResult write fResultAsJSONObjectWithoutResult; /// set to TRUE to return the interface's methods result as XML object // - by default (FALSE), method execution will return a JSON array with // all VAR/OUT parameters, or a JSON object if ResultAsJSONObject is TRUE // - TRUE will generate a XML object instead, with the VAR/OUT parameter // names as field names (and "Result" for any function result) - may be // useful e.g. when working with some XML-only clients // - Delphi clients (i.e. TServiceFactoryClient/TInterfacedObjectFake) does // NOT handle this XML format yet // - this value can be overridden by setting ForceServiceResultAsXMLObject // for a given TSQLRestServerURIContext instance property ResultAsXMLObject: boolean read fResultAsXMLObject write fResultAsXMLObject; /// set to TRUE to return XML objects for the interface's methods result // if the Accept: HTTP header is exactly 'application/xml' or 'text/xml' // - the header should be exactly 'Accept: application/xml' or // 'Accept: text/xml' (and no other value) // - in this case, ForceServiceResultAsXMLObject will be set for this // particular TSQLRestServerURIContext instance, and result returned as XML // - using this method allows to mix standard JSON requests (from JSON // or AJAX clients) and XML requests (from XML-only clients) property ResultAsXMLObjectIfAcceptOnlyXML: boolean read fResultAsJSONObjectIfAccept write fResultAsJSONObjectIfAccept; /// specify a custom name space content when returning a XML object // - by default, no name space will be appended - but such rough XML will // have potential validation problems // - you may use e.g. XMLUTF8_NAMESPACE, which will append ... // around the generated XML data property ResultAsXMLObjectNameSpace: RawUTF8 read fResultAsXMLObjectNameSpace write fResultAsXMLObjectNameSpace; /// disable base64-encoded TSQLRecordServiceLog.Output for methods // returning TServiceCustomAnswer record (to reduce storage size) property ExcludeServiceLogCustomAnswer: boolean read fExcludeServiceLogCustomAnswer write fExcludeServiceLogCustomAnswer; end; /// a service provider implemented on the client side // - each registered interface has its own TServiceFactoryClient instance, // available as one TSQLServiceContainerClient item from TSQLRest.Services property // - will emulate "fake" implementation class instance of a given interface // and call remotely the server to process the actual implementation TServiceFactoryClient = class(TServiceFactory) protected fForcedURI: RawUTF8; fClient: TSQLRestClientURI; fParamsAsJSONObject: boolean; fResultAsJSONObject: boolean; fDelayedInstance: boolean; fNonBlockWithoutAnswer: boolean; fSendNotificationsThread: TThread; fSendNotificationsRest: TSQLRest; fSendNotificationsLogClass: TSQLRecordServiceNotificationsClass; function CreateFakeInstance: TInterfacedObject; function InternalInvoke(const aMethod: RawUTF8; const aParams: RawUTF8=''; aResult: PRawUTF8=nil; aErrorMsg: PRawUTF8=nil; aClientDrivenID: PCardinal=nil; aServiceCustomAnswer: PServiceCustomAnswer=nil; aClient: TSQLRestClientURI=nil): boolean; virtual; // match TOnFakeInstanceInvoke callback signature function Invoke(const aMethod: TServiceMethod; const aParams: RawUTF8; aResult: PRawUTF8; aErrorMsg: PRawUTF8; aClientDrivenID: PCardinal; aServiceCustomAnswer: PServiceCustomAnswer): boolean; procedure NotifyInstanceDestroyed(aClientDrivenID: cardinal); virtual; public /// initialize the service provider parameters // - it will check and retrieve all methods of the supplied interface, // and prepare all internal structures for its serialized execution // - also set the inherited TServiceInstanceImplementation property // - initialize fSharedInstance if aInstanceCreation is sicShared // - it will also ensure that the corresponding TServiceFactory.Contract // matches on both client and server sides, either by comparing the default // signature (based on methods and arguments), either by using the supplied // expected contract (which may be a custom version number) constructor Create(aRest: TSQLRest; aInterface: PTypeInfo; aInstanceCreation: TServiceInstanceImplementation; const aContractExpected: RawUTF8=''); /// finalize the service provider used instance // - e.g. the shared fake implementation instance destructor Destroy; override; /// retrieve an instance of this interface from the client side function Get(out Obj): Boolean; override; /// retrieve the published signature of this interface // - TServiceFactoryClient will be able to retrieve it only if // TServiceContainerServer.PublishSignature is set to TRUE (which is not the // default setting, for security reasons) - this function is always available // on TServiceFactoryServer side function RetrieveSignature: RawUTF8; override; /// convert a HTTP error from mORMot's REST/SOA into an English text message // - will recognize the HTTP_UNAVAILABLE, HTTP_NOTIMPLEMENTED, HTTP_NOTFOUND, // HTTP_NOTALLOWED, HTTP_UNAUTHORIZED or HTTP_NOTACCEPTABLE errors, as // generated by the TSQLRestServer side // - is used by TServiceFactoryClient.InternalInvoke, but may be called // on client side for TServiceCustomAnswer.Status <> HTTP_SUCCESS class function GetErrorMessage(status: integer): RawUTF8; /// define execution options for a given set of methods // - methods names should be specified as an array (e.g. ['Add','Multiply']) // - if no method name is given (i.e. []), option will be set for all methods // - only supports optNoLogInput and optNoLogOutput on the client side, // by design of "fake" interface remote execution procedure SetOptions(const aMethod: array of RawUTF8; aOptions: TServiceMethodOptions; aAction: TServiceMethodOptionsAction=moaReplace); /// persist all service calls into a database instead of calling the client // - expect a REST instance, which will store all methods without any // results (i.e. procedure without any var/out parameters) on the // associated TSQLRecordServiceNotifications class // - once set, regular fClient.URI() won't be called but a new aLogClass // entry will be stored in aRest // - to disable this redirection, set aRest and aLogClass to nil procedure StoreNotifications(aRest: TSQLRest; aLogClass: TSQLRecordServiceNotificationsClass); /// allow background process of method with no results, via a temporary // database, to be used e.g. for safe notifications transmission // - will call StoreNotifications() and start background notification // - expect a REST instance, which will store all methods without any // results (i.e. procedure without any var/out parameters) on the // associated TSQLRecordServiceNotifications class // - a background thread will be used to check for pending notifications, // and send them to the supplied aRemote TSQLRestClient instance, or // to the main TServiceFactoryClient.fClient instance // - if the remote client is not reachable, will retry after the specified // period of time, in seconds // - this method is not blocking, and will write the pending calls to // the aRest/aLogClass table, which will be retrieved asynchronously // by the background thread procedure SendNotifications(aRest: TSQLRest; aLogClass: TSQLRecordServiceNotificationsClass; aRetryPeriodSeconds: Integer=30; aRemote: TSQLRestClientURI=nil); /// compute how many pending notifications are waiting for background process // initiated by SendNotifications() method function SendNotificationsPending: integer; /// wait for all pending notifications to be sent // - you can supply a time out period after which no wait will take place procedure SendNotificationsWait(aTimeOutSeconds: integer); published /// could be used to force the remote URI to access the service // - by default, the URI will be Root/Calculator or Root/InterfaceMangledURI // but you may use this property to use another value, e.g. if you are // accessign a non mORMot REST server (probably with aContractExpected set // to SERVICE_CONTRACT_NONE_EXPECTED, and running // Client.ServerTimestamp := TimeLogNowUTC to avoid an unsupported // ServerTimestampSynchronize call) property ForcedURI: RawUTF8 read fForcedURI write fForcedURI; /// set to TRUE to send the interface's methods parameters as JSON object // - by default (FALSE), any method execution will send a JSON array with // all CONST/VAR parameters, in order // - TRUE will generate a JSON object instead, with the CONST/VAR parameter // names as field names - may be useful e.g. when working with a non // mORMot server, or when the mORMot server exposes a public API // - defined e.g. by TSQLRestClientURI.ServiceDefineSharedAPI() method property ParamsAsJSONObject: boolean read fParamsAsJSONObject write fParamsAsJSONObject; /// set to TRUE if the interface's methods result is expected to be a JSON object // without the {"result":... } nesting // - by default (FALSE), any method execution will return a JSON array with // all VAR/OUT parameters, within a {"result":...,"id":...} layout // - TRUE will expect a simple JSON object instead, with the VAR/OUT parameter // names as field names (and "Result" for any function result) - may be // useful e.g. when working with JavaScript clients or any public API // - this value can be overridden by setting ForceServiceResultAsJSONObject // for a given TSQLRestServerURIContext (e.g. for server-side JavaScript work) // - defined e.g. by TSQLRestClientURI.ServiceDefineSharedAPI() method property ResultAsJSONObjectWithoutResult: boolean read fResultAsJSONObject write fResultAsJSONObject; /// delay the sicClientDriven server-side instance to the first method call // - by default, CreateFakeInstance will call _instance_ server pseudo-method // to ensure a fClientDrivenID is safely and properly initialized // - if you are sure that your client's interface variables will be thread-safe, // you may define this property to TRUE so that the "id" field as returned // at first method call will be used - makes sense only if a lot of short-live // interface instances are expected to be generated by the client property DelayedInstance: boolean read fDelayedInstance write fDelayedInstance; /// if methods expecting no result (i.e. plain procedure without var/out // parameters) should not block the client waiting for answer // - may be handy e.g. when consuming an event-driven asynchronous service // - will call CallbackNonBlockingSetHeader, currently implemented only in // TSQLHttpClientWebsockets, with frame gathering property NonBlockWithoutAnswer: boolean read fNonBlockWithoutAnswer write fNonBlockWithoutAnswer; end; /// class-reference type (metaclass) of a TServiceFactoryClient kind TServiceFactoryClientClass = class of TServiceFactoryClient; /// used to lookup one service in a global list of interface-based services TServiceContainerInterface = record /// one 'service' item, as set at URI, e.g. 'Calculator' InterfaceName: RawUTF8; /// the associated service provider Service: TServiceFactory; end; /// pointer to one lookup in a global list of interface-based services PServiceContainerInterface = ^TServiceContainerInterface; /// used to store all s in a global list of interface-based services TServiceContainerInterfaces = array of TServiceContainerInterface; /// used to lookup one method in a global list of interface-based services TServiceContainerInterfaceMethod = record /// one 'service.method' item, as set at URI // - e.g.'Calculator.Add','Calculator.Multiply'... InterfaceDotMethodName: RawUTF8; /// the associated service provider InterfaceService: TServiceFactory; /// the index of the method for the given service // - 0..2 indicates _free_/_contract_/_signature_ pseudo-methods // - then points to InterfaceService.Interface.Methods[InterfaceMethodIndex-3] InterfaceMethodIndex: integer; end; /// pointer to one method lookup in a global list of interface-based services PServiceContainerInterfaceMethod = ^TServiceContainerInterfaceMethod; /// used to store all methods in a global list of interface-based services TServiceContainerInterfaceMethods = array of TServiceContainerInterfaceMethod; /// used in TServiceContainer to identify fListInterfaceMethod[] entries TServiceContainerInterfaceMethodBits = set of 0..255; /// a global services provider class // - used to maintain a list of interfaces implementation // - inherits from TInterfaceResolverInjected and its Resolve() methods, // compatible with TInjectableObject TServiceContainer = class(TInterfaceResolverInjected) protected fRest: TSQLRest; // list of services ['Calculator',...] fInterface: TServiceContainerInterfaces; fInterfaces: TDynArrayHashed; // list of service.method ['Calculator.Add','Calculator.Multiply',...] fInterfaceMethod: TServiceContainerInterfaceMethods; fInterfaceMethods: TDynArrayHashed; fExpectMangledURI: boolean; fServicesFactoryClients: TServiceFactoryClientClass; procedure SetExpectMangledURI(aValue: Boolean); procedure SetInterfaceMethodBits(MethodNamesCSV: PUTF8Char; IncludePseudoMethods: boolean; out bits: TServiceContainerInterfaceMethodBits); function GetMethodName(ListInterfaceMethodIndex: integer): RawUTF8; procedure CheckInterface(const aInterfaces: array of PTypeInfo); function AddServiceInternal(aService: TServiceFactory): integer; function TryResolve(aInterface: PTypeInfo; out Obj): boolean; override; /// retrieve a service provider from its URI function GetService(const aURI: RawUTF8): TServiceFactory; public /// initialize the list constructor Create(aRest: TSQLRest); virtual; /// release all registered services destructor Destroy; override; /// release all services of a TSQLRest instance before shutdown // - will allow to properly release any pending callbacks // - TSQLRest.Services.Release will call FreeAndNil(fServices) procedure Release; /// return the number of registered service interfaces function Count: integer; {$ifdef HASINLINE}inline;{$endif} /// method called on the client side to register a service via its interface(s) // - will add a TServiceFactoryClient instance to the internal list // - is called e.g. by TSQLRestClientURI.ServiceRegister or even by // TSQLRestServer.ServiceRegister(aClient: TSQLRest...) for a remote access - // use TServiceContainerServer.AddImplementation() instead for normal // server side implementation // - will raise an exception on error // - will return true if some interfaces have been added // - will check for the availability of the interfaces on the server side, // with an optional custom contract to be used instead of methods signature // (only for the first interface) function AddInterface(const aInterfaces: array of PTypeInfo; aInstanceCreation: TServiceInstanceImplementation; aContractExpected: RawUTF8=''): boolean; overload; /// method called on the client side to register a service via one interface // - overloaded method returning the corresponding service factory client, // or nil on error function AddInterface(aInterface: PTypeInfo; aInstanceCreation: TServiceInstanceImplementation; const aContractExpected: RawUTF8=''): TServiceFactoryClient; overload; /// retrieve a service provider from its index in the list // - returns nil if out of range index function Index(aIndex: integer): TServiceFactory; overload; {$ifdef HASINLINE}inline;{$endif} /// retrieve a service provider from its GUID / Interface type // - you shall have registered the interface by a previous call to // ! TInterfaceFactory.RegisterInterfaces([TypeInfo(IMyInterface),...]) // - on match, it will return the service the corresponding interface factory // - returns nil if the GUID does not match any registered interface // - can be used as such to resolve an I: ICalculator interface // ! if fClient.Services.Info(ICalculator).Get(I) then // ! ... use I function Info(const aGUID: TGUID): TServiceFactory; overload; /// retrieve a service provider from its type information // - on match, it will return the service the corresponding interface factory // - returns nil if the type information does not match any registered interface // - can be used as such to resolve an I: ICalculator interface // ! if fClient.Services.Info(TypeInfo(ICalculator)).Get(I) then // ! ... use I // - is defined as virtual so that e.g. TServiceContainerClient will // automatically register the interface, if it was not already done function Info(aTypeInfo: PTypeInfo): TServiceFactory; overload; virtual; /// notify the other side that the given Callback event interface is released // - this default implementation will do nothing function CallBackUnRegister(const Callback: IInvokable): boolean; virtual; /// retrieve all registered Services TGUID procedure SetGUIDs(out Services: TGUIDDynArray); /// retrieve all registered Services names // - i.e. all interface names without the initial 'I', e.g. 'Calculator' for // ICalculator procedure SetInterfaceNames(out Names: TRawUTF8DynArray); /// retrieve all registered Services contracts as a JSON array // - i.e. a JSON array of TServiceFactory.Contract JSON objects function AsJson: RawJSON; /// retrieve a service provider from its URI // - it expects the supplied URI variable to be e.g. '00amyWGct0y_ze4lIsj2Mw' // or 'Calculator', depending on the ExpectMangledURI property // - on match, it will return the service the corresponding interface factory // - returns nil if the URI does not match any registered interface property Services[const aURI: RawUTF8]: TServiceFactory read GetService; default; /// the associated RESTful instance property Rest: TSQLRest read fRest; /// set if the URI is expected to be mangled from the GUID // - by default (FALSE), the clear service name is expected to be supplied at // the URI level (e.g. 'Calculator') // - if this property is set to TRUE, the mangled URI value will be expected // instead (may enhance security) - e.g. '00amyWGct0y_ze4lIsj2Mw' property ExpectMangledURI: boolean read fExpectMangledURI write SetExpectMangledURI; /// the services factory client classes // - by default, will use TServiceFactoryClient property ServicesFactoryClients: TServiceFactoryClientClass read fServicesFactoryClients write fServicesFactoryClients; end; /// a callback interface used to notify a TSQLRecord modification in real time // - will be used e.g. by TSQLRestServer.RecordVersionSynchronizeSubscribeMaster() // - all methods of this interface will be called asynchronously when // transmitted via our WebSockets implementation, since they are defined as // plain procedures // - each callback instance should be private to a specific TSQLRecord IServiceRecordVersionCallback = interface(IInvokable) ['{8598E6BE-3590-4F76-9449-7AF7AF4241B0}'] /// this event will be raised on any Add on a versioned record // - the supplied JSON object will contain the TRecordVersion field procedure Added(const NewContent: RawJSON); /// this event will be raised on any Update on a versioned record // - the supplied JSON object will contain the TRecordVersion field procedure Updated(const ModifiedContent: RawJSON); /// this event will be raised on any Delete on a versioned record procedure Deleted(const ID: TID; const Revision: TRecordVersion); /// allow to optimize process for WebSockets "jumbo frame" items // - this method may be called with isLast=false before the first method // call of this interface, then with isLast=true after the call of the // last method of the "jumbo frame" // - match TInterfaceFactory.MethodIndexCurrentFrameCallback signature // - allow e.g. to create a temporary TSQLRestBatch for jumbo frames // - if individual frames are received, this method won't be called procedure CurrentFrame(isLast: boolean); end; /// a list of callback interfaces to notify TSQLRecord modifications // - you can use InterfaceArray*() wrapper functions to manage the list IServiceRecordVersionCallbackDynArray = array of IServiceRecordVersionCallback; /// service definition for master/slave replication notifications subscribe // - implemented by TServiceRecordVersion, as used by // TSQLRestServer.RecordVersionSynchronizeMasterStart(), and expected by // TSQLRestServer.RecordVersionSynchronizeSlaveStart() IServiceRecordVersion = interface(IInvokable) ['{06A355CA-19EB-4CC6-9D87-7B48967D1D9F}'] /// will register the supplied callback for the given table function Subscribe(const SQLTableName: RawUTF8; const revision: TRecordVersion; const callback: IServiceRecordVersionCallback): boolean; end; /// service definition with a method which will be called when a callback // interface instance is released on the client side // - may be used to implement safe publish/subscribe mechanism using // interface callbacks, e.g. over WebSockets IServiceWithCallbackReleased = interface(IInvokable) ['{8D518FCB-62C3-42EB-9AE7-96ED322140F7}'] /// will be called when a callback is released on the client side // - this method matches the TInterfaceFactory.MethodIndexCallbackReleased // signature, so that it will be called with the interface instance by // TServiceContainerServer.FakeCallbackRelease // - you may use it as such - see sample Project31ChatServer.dpr: // ! procedure TChatService.CallbackReleased(const callback: IInvokable; // ! const interfaceName: RawUTF8); // ! begin // unsubscribe from fConnected: array of IChatCallback // ! if interfaceName='IChatCallback' then // ! InterfaceArrayDelete(fConnected,callback); // ! end; procedure CallbackReleased(const callback: IInvokable; const interfaceName: RawUTF8); end; /// event signature triggerred when a callback instance is released // - used by TServiceContainerServer.OnCallbackReleasedOnClientSide // and TServiceContainerServer.OnCallbackReleasedOnServerSide event properties // - the supplied Instance will be a TInterfacedObjectFakeServer, and the // Callback will be a pointer to the corresponding interface value // - assigned implementation should be as fast a possible, since this event // will be executed in a global lock for all server-side callbacks TOnCallbackReleased = procedure(Sender: TServiceContainer; Instance: TInterfacedObject; Callback: pointer) of object; /// how TServiceContainerServer will handle SOA callbacks // - by default, a callback released on the client side will log a warning // and continue the execution (relying e.g. on a CallbackReleased() method to // unsubscribe the event), but coRaiseExceptionIfReleasedByClient can be // defined to raise an EInterfaceFactoryException in this case TServiceCallbackOptions = set of ( coRaiseExceptionIfReleasedByClient); /// a services provider class to be used on the server side // - this will maintain a list of true implementation classes TServiceContainerServer = class(TServiceContainer) protected fPublishSignature: boolean; fConnectionID: Int64; fFakeCallbacks: TSynObjectListLocked; // TInterfacedObjectFakeServer instances fOnCallbackReleasedOnClientSide: TOnCallbackReleased; fOnCallbackReleasedOnServerSide: TOnCallbackReleased; fCallbackOptions: TServiceCallbackOptions; fRecordVersionCallback: array of IServiceRecordVersionCallbackDynArray; fSessionTimeout: cardinal; /// make some garbage collection when session is finished procedure OnCloseSession(aSessionID: cardinal); virtual; procedure FakeCallbackAdd(aFakeInstance: TObject); procedure FakeCallbackRemove(aFakeInstance: TObject); procedure FakeCallbackRelease(Ctxt: TSQLRestServerURIContext); procedure RecordVersionCallbackNotify(TableIndex: integer; Occasion: TSQLOccasion; const DeletedID: TID; const DeletedRevision: TRecordVersion; const AddUpdateJson: RawUTF8); public /// class method able to check if a given server-side callback event fake // instance has been released on the client side // - may be used to automatically purge a list of subscribed callbacks, // e.g. before trigerring the interface instance, and avoid an exception // - can optionally append the callback class instance information to // a local shortstring variable, e.g. for logging/debug purposes class function CallbackReleasedOnClientSide(const callback: IInterface; callbacktext: PShortString=nil): boolean; overload; /// method called on the server side to register a service via its // interface(s) and a specified implementation class or a shared // instance (for sicShared mode) // - will add a TServiceFactoryServer instance to the internal list // - will raise an exception on error // - will return the first of the registered TServiceFactoryServer created // on success (i.e. the one corresponding to the first item of the aInterfaces // array), or nil if registration failed (e.g. if any of the supplied interfaces // is not implemented by the given class) // - the same implementation class can be used to handle several interfaces // (just as Delphi allows to do natively) function AddImplementation(aImplementationClass: TInterfacedClass; const aInterfaces: array of PTypeInfo; aInstanceCreation: TServiceInstanceImplementation; aSharedImplementation: TInterfacedObject; const aContractExpected: RawUTF8): TServiceFactoryServer; /// initialize the list constructor Create(aRest: TSQLRest); override; /// finalize the service container destructor Destroy; override; /// register a callback interface which will be called each time a write // operation is performed on a given TSQLRecord with a TRecordVersion field // - called e.g. by TSQLRestServer.RecordVersionSynchronizeSubscribeMaster function RecordVersionSynchronizeSubscribeMaster(TableIndex: integer; RecordVersion: TRecordVersion; const SlaveCallback: IServiceRecordVersionCallback): boolean; /// notify any TRecordVersion callback for a table Add/Update from a // TDocVariant content // - used e.g. by TSQLRestStorageMongoDB.DocFromJSON() procedure RecordVersionNotifyAddUpdate(Occasion: TSQLOccasion; TableIndex: integer; const Document: TDocVariantData); overload; /// notify any TRecordVersion callback for a table Add/Update from a // TJSONObjectDecoder content // - used e.g. by TSQLRestStorageMongoDB.DocFromJSON() procedure RecordVersionNotifyAddUpdate(Occasion: TSQLOccasion; TableIndex: integer; const Decoder: TJSONObjectDecoder); overload; /// notify any TRecordVersion callback for a table Delete procedure RecordVersionNotifyDelete(TableIndex: integer; const ID: TID; const Revision: TRecordVersion); /// log method execution information to a TSQLRecordServiceLog table // - TServiceFactoryServer.SetServiceLog() will be called for all registered // interfaced-based services of this container // - will write to the specified aLogRest instance, and will disable // writing if aLogRest is nil // - will write to a (inherited) TSQLRecordServiceLog table, as available in // TSQLRest's model, unless a dedicated table is specified as aLogClass // - you could specify a CSV list of method names to be excluded from logging // (containing e.g. a password or a credit card number), containing either // the interface name (as 'ICalculator.Add'), or not (as 'Add') procedure SetServiceLog(aLogRest: TSQLRest; aLogClass: TSQLRecordServiceLogClass=nil; const aExcludedMethodNamesCSV: RawUTF8=''); /// defines if the "method":"_signature_" or /root/Interface._signature // pseudo method is available to retrieve the whole interface signature, // encoded as a JSON object // - is set to FALSE by default, for security reasons: only "_contract_" // pseudo method is available - see TServiceContainer.ContractExpected property PublishSignature: boolean read fPublishSignature write fPublishSignature; /// the default TServiceFactoryServer.TimeoutSec value // - default is 30 minutes // - you can customize each service using its corresponding TimeoutSec property property SessionTimeout: cardinal read fSessionTimeout write fSessionTimeout; /// this event will be launched when a callback interface is notified as // relased on the Client side // - as an alternative, you may define the following method on the // registration service interface type, which will be called when a // callback registered via this service is released (e.g. to unsubscribe // the callback from an interface list, via InterfaceArrayDelete): // ! procedure CallbackReleased(const callback: IInvokable; const interfaceName: RawUTF8); property OnCallbackReleasedOnClientSide: TOnCallbackReleased read fOnCallbackReleasedOnClientSide; /// this event will be launched when a callback interface is relased on // the Server side property OnCallbackReleasedOnServerSide: TOnCallbackReleased read fOnCallbackReleasedOnServerSide; /// defines how SOA callbacks will be handled property CallbackOptions: TServiceCallbackOptions read fCallbackOptions write fCallbackOptions; end; /// this class implements a service, which may be called to push notifications // for master/slave replication // - as used by TSQLRestServer.RecordVersionSynchronizeMasterStart(), and // expected by TSQLRestServer.RecordVersionSynchronizeSlaveStart() TServiceRecordVersion = class(TInjectableObjectRest,IServiceRecordVersion) public /// will register the supplied callback for the given table function Subscribe(const SQLTableName: RawUTF8; const revision: TRecordVersion; const callback: IServiceRecordVersionCallback): boolean; end; /// a services provider class to be used on the client side // - this will maintain a list of fake implementation classes, which will // remotely call the server to make the actual process TServiceContainerClient = class(TServiceContainer) protected fDisableAutoRegisterAsClientDriven: boolean; public /// retrieve a service provider from its type information // - this overridden method will register the interface, if was not yet made // - in this case, the interface will be registered with sicClientDriven // implementation method, unless DisableAutoRegisterAsClientDriven is TRUE function Info(aTypeInfo: PTypeInfo): TServiceFactory; overload; override; /// notify the other side that the given Callback event interface is released // - this overriden implementation will check the private fFakeCallbacks list function CallBackUnRegister(const Callback: IInvokable): boolean; override; /// allow to disable the automatic registration as sicClientDriven in Info() property DisableAutoRegisterAsClientDriven: boolean read fDisableAutoRegisterAsClientDriven write fDisableAutoRegisterAsClientDriven; end; /// TInterfacedObject class which will notify a REST server when it is released // - could be used when implementing event callbacks as interfaces, so that // the other side instance will be notified when it is destroyed TInterfacedCallback = class(TInterfacedObjectLocked) protected fRest: TSQLRest; fInterface: TGUID; public /// initialize the instance for a given REST and callback interface constructor Create(aRest: TSQLRest; const aGUID: TGUID); reintroduce; /// notify the associated TSQLRestServer that the callback is disconnnected // - i.e. will call TSQLRestServer's TServiceContainer.CallBackUnRegister() // - this method will process the unsubscription only once procedure CallbackRestUnregister; virtual; /// finalize the instance, and notify the TSQLRestServer that the callback // is now unreachable // - i.e. will call CallbackRestUnregister destructor Destroy; override; /// the associated TSQLRestServer instance, which will be notified // when the callback is released property Rest: TSQLRest read fRest; /// the interface type, implemented by this callback class property RestInterface: TGUID read fInterface write fInterface; end; /// asynchrounous callback to emulate a synchronous/blocking process // - once created, process will block via a WaitFor call, which will be // released when CallbackFinished() is called by the process background thread TBlockingCallback = class(TInterfacedCallback) protected fProcess: TBlockingProcess; function GetEvent: TBlockingEvent; public /// initialize the callback instance // - specify a time out millliseconds period after which blocking execution // should be handled as failure (if 0 is set, default 3000 will be used) // - you can optionally set a REST and callback interface for automatic // notification when this TInterfacedCallback will be released constructor Create(aTimeOutMs: integer; aRest: TSQLRest; const aGUID: TGUID); reintroduce; /// finalize the callback instance destructor Destroy; override; /// called to wait for the callback to be processed, or trigger timeout // - will block until CallbackFinished() is called by the processing thread // - returns the final state of the process, i.e. beRaised or beTimeOut function WaitFor: TBlockingEvent; virtual; /// should be called by the callback when the process is finished // - the caller will then let its WaitFor method return // - if aServerUnregister is TRUE, will also call CallbackRestUnregister to // notify the server that the callback is no longer needed // - will optionally log all published properties values to the log class // of the supplied REST instance procedure CallbackFinished(aRestForLog: TSQLRest; aServerUnregister: boolean=false); virtual; /// just a wrapper to reset the internal Event state to evNone // - may be used to re-use the same TBlockingCallback instance, after // a successfull WaitFor/CallbackFinished process // - returns TRUE on success (i.e. status was not beWaiting) // - if there is a WaitFor currently in progress, returns FALSE function Reset: boolean; virtual; /// the associated blocking process instance property Process: TBlockingProcess read fProcess; published /// the current state of process // - just a wrapper around Process.Event // - use Reset method to re-use this instance after a WaitFor process property Event: TBlockingEvent read GetEvent; end; /// this class implements a callback interface, able to write all remote ORM // notifications to the local DB // - could be supplied as callback parameter, possibly via WebSockets // transmission, to TSQLRestServer.RecordVersionSynchronizeSubscribeMaster() TServiceRecordVersionCallback = class(TInterfacedCallback,IServiceRecordVersionCallback) protected fTable: TSQLRecordClass; fRecordVersionField: TSQLPropInfoRTTIRecordVersion; fBatch: TSQLRestBatch; fSlave: TSQLRestServer; // fRest is master remote access fOnNotify: TOnBatchWrite; // local TSQLRecordTableDeleted.ID follows current Model -> pre-compute offset fTableDeletedIDOffset: Int64; procedure SetCurrentRevision(const Revision: TRecordVersion; Event: TSQLOccasion); public /// initialize the instance able to apply callbacks for a given table on // a local slave REST server from a remote master REST server // - the optional low-level aOnNotify callback will be triggerred for each // incoming notification, to track the object changes in real-time constructor Create(aSlave: TSQLRestServer; aMaster: TSQLRestClientURI; aTable: TSQLRecordClass; aOnNotify: TOnBatchWrite); reintroduce; /// finalize this callback instance destructor Destroy; override; /// this event will be raised on any Add on a versioned record procedure Added(const NewContent: RawJSON); virtual; /// this event will be raised on any Update on a versioned record procedure Updated(const ModifiedContent: RawJSON); virtual; /// this event will be raised on any Delete on a versioned record procedure Deleted(const ID: TID; const Revision: TRecordVersion); virtual; /// match TInterfaceFactory.MethodIndexCurrentFrameCallback signature, // so that TSQLHttpClientWebsockets.CallbackRequest will call it // - it will create a temporary TSQLRestBatch for the whole "jumbo frame" procedure CurrentFrame(isLast: boolean); virtual; /// low-level event handler triggerred by Added/Updated/Deleted methods property OnNotify: TOnBatchWrite read fOnNotify write fOnNotify; end; /// prototype of a class implementing redirection of a given interface // - as returned e.g. by TSQLRest.MultiRedirect method // - can be used as a main callback, then call Redirect() to manage // an internal list of redirections // - when you release this instance, will call Rest.Service.CallbackUnregister // with the associated fake callback generated IMultiCallbackRedirect = interface ['{E803A30A-8C06-4BB9-94E6-EB87EACFE980}'] /// add or remove an interface callback to the internal redirection list // - will register a callback if aSubscribe is true // - will unregister a callback if aSubscribe is false // - supplied aCallback shoud implement the expected interface GUID // - this method will be implemented as thread-safe // - you can specify some method names, or all methods redirection if [] procedure Redirect(const aCallback: IInvokable; const aMethodsNames: array of RawUTF8; aSubscribe: boolean=true); overload; /// add or remove a class instance callback to the internal redirection list // - will register a callback if aSubscribe is true // - will unregister a callback if aSubscribe is false // - supplied aCallback instance should implement the expected interface GUID // - this method will be implemented as thread-safe // - you can specify some method names, or all methods redirection if [] procedure Redirect(const aCallback: TInterfacedObject; const aMethodsNames: array of RawUTF8; aSubscribe: boolean=true); overload; end; /// for TSQLRestCache, stores a table values TSQLRestCacheEntryValue = packed record /// corresponding ID ID: TID; /// GetTickCount64 shr 9 timestamp when this cached value was stored // - resulting time period has therefore a resolution of 512 ms, and // overflows after 70 years without computer reboot // - equals 0 when there is no JSON value cached Timestamp512: cardinal; /// some associated unsigned integer value // - not used by TSQLRestCache, but available at TSQLRestCacheEntry level Tag: cardinal; /// JSON encoded UTF-8 serialization of the record JSON: RawUTF8; end; /// for TSQLRestCache, stores all tables values TSQLRestCacheEntryValueDynArray = array of TSQLRestCacheEntryValue; /// for TSQLRestCache, stores a table settings and values {$ifdef USERECORDWITHMETHODS}TSQLRestCacheEntry = record {$else}TSQLRestCacheEntry = object{$endif} public /// TRUE if this table should use caching // - i.e. if was not set, or worth it for this table (e.g. in-memory table) CacheEnable: boolean; /// the whole specified Table content will be cached CacheAll: boolean; /// time out value (in ms) // - if 0, caching will never expire TimeOutMS: Cardinal; /// the number of entries stored in Values[] Count: integer; /// all cached IDs and JSON content Values: TSQLRestCacheEntryValueDynArray; /// TDynArray wrapper around the Values[] array Value: TDynArray; /// used to lock the table cache for multi thread safety Mutex: TSynLocker; /// initialize this table cache // - will set Value wrapper and Mutex handle - other fields should have // been cleared by caller (is the case for a TSQLRestCacheEntryDynArray) procedure Init; /// reset all settings corresponding to this table cache procedure Clear; /// finalize this table cache entry procedure Done; /// flush cache for a given Value[] index procedure FlushCacheEntry(Index: Integer); /// flush cache for all Value[] procedure FlushCacheAllEntries; /// add the supplied ID to the Value[] array procedure SetCache(aID: TID); /// update/refresh the cached JSON serialization of a given ID procedure SetJSON(aID: TID; const aJSON: RawUTF8; aTag: cardinal=0); overload; /// update/refresh the cached JSON serialization of a supplied Record procedure SetJSON(aRecord: TSQLRecord); overload; /// retrieve a JSON serialization of a given ID from cache function RetrieveJSON(aID: TID; var aJSON: RawUTF8; aTag: PCardinal=nil): boolean; overload; /// unserialize a JSON cached record of a given ID function RetrieveJSON(aID: TID; aValue: TSQLRecord; aTag: PCardinal=nil): boolean; overload; /// compute how much memory stored entries are using // - will also flush outdated entries function CachedMemory(FlushedEntriesCount: PInteger=nil): cardinal; end; /// for TSQLRestCache, stores all table settings and values // - this dynamic array will follow TSQLRest.Model.Tables[] layout, i.e. one // entry per TSQLRecord class in the data model TSQLRestCacheEntryDynArray = array of TSQLRestCacheEntry; /// implement a fast TSQLRecord cache, per ID, at the TSQLRest level // - purpose of this caching mechanism is to speed up retrieval of some common // values at either Client or Server level (like configuration settings) // - only caching synchronization is about the following RESTful basic commands: // RETRIEVE, ADD, DELETION and UPDATE (that is, a complex direct SQL UPDATE // or via TSQLRecordMany pattern won't be taken into account) // - only Simple fields are cached: e.g. the BLOB fields are not stored // - this cache is thread-safe (access is locked per table) // - this caching will be located at the TSQLRest level, that is no automated // synchronization is implemented between TSQLRestClient and TSQLRestServer: // you shall ensure that your code won't fail due to this restriction TSQLRestCache = class protected fRest: TSQLRest; /// fCache[] follows fRest.Model.Tables[] array: one entry per TSQLRecord fCache: TSQLRestCacheEntryDynArray; /// retrieve a record specified by its ID from cache into JSON content // - return '' if the item is not in cache function Retrieve(aTableIndex: integer; aID: TID): RawUTF8; overload; /// fill a record specified by its ID from cache into a new TSQLRecord instance // - return false if the item is not in cache // - this method will call RetrieveJSON method, unserializing the cached // JSON content into the supplied aValue instance function Retrieve(aID: TID; aValue: TSQLRecord): boolean; overload; public /// create a cache instance // - the associated TSQLModel will be used internaly constructor Create(aRest: TSQLRest); reintroduce; /// release the cache instance destructor Destroy; override; /// flush the cache // - this will flush all stored JSON content, but keep the settings // (SetCache/SetTimeOut) as before procedure Flush; overload; /// flush the cache for a given table // - this will flush all stored JSON content, but keep the settings // (SetCache/SetTimeOut) as before for this table procedure Flush(aTable: TSQLRecordClass); overload; /// flush the cache for a given record // - this will flush the stored JSON content for this record (and table // settings will be kept) procedure Flush(aTable: TSQLRecordClass; aID: TID); overload; /// flush the cache for a set of specified records // - this will flush the stored JSON content for these record (and table // settings will be kept) procedure Flush(aTable: TSQLRecordClass; const aIDs: array of TID); overload; /// flush the cache, and destroy all settings // - this will flush all stored JSON content, AND destroy the settings // (SetCache/SetTimeOut) to default (i.e. no cache enabled) procedure Clear; // - will fill the internal JSON cache of a given Table with data coming // from a REST query // - returns the number of TSQLRecord items actually cached // - may be handy to pre-load a set of values (e.g. a lookup table) from a // single REST query, without waiting for each record to be retrieved function FillFromQuery(aTable: TSQLRecordClass; const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const): integer; /// activate the internal caching for a whole Table // - any cached item of this table will be flushed // - return true on success function SetCache(aTable: TSQLRecordClass): boolean; overload; /// activate the internal caching for a given TSQLRecord // - if this item is already cached, do nothing // - return true on success function SetCache(aTable: TSQLRecordClass; aID: TID): boolean; overload; /// activate the internal caching for a set of specified TSQLRecord // - if these items are already cached, do nothing // - return true on success function SetCache(aTable: TSQLRecordClass; const aIDs: array of TID): boolean; overload; /// activate the internal caching for a given TSQLRecord // - will cache the specified aRecord.ID item // - if this item is already cached, do nothing // - return true on success function SetCache(aRecord: TSQLRecord): boolean; overload; /// set the internal caching time out delay (in ms) for a given table // - actual resolution is 512 ms // - time out setting is common to all items of the table // - if aTimeOut is left to its default 0 value, caching will never expire // - return true on success function SetTimeOut(aTable: TSQLRecordClass; aTimeoutMS: cardinal): boolean; /// returns TRUE if the table is part of the current caching policy function IsCached(aTable: TSQLRecordClass): boolean; /// returns the number of JSON serialization records within this cache function CachedEntries: cardinal; /// returns the memory used by JSON serialization records within this cache // - this method will also flush any outdated entries in the cache function CachedMemory(FlushedEntriesCount: PInteger=nil): cardinal; /// read-only access to the associated TSQLRest instance property Rest: TSQLRest read fRest; public { TSQLRest low level methods which are not to be called usualy: } /// TSQLRest instance shall call this method when a record is added or updated // - this overloaded method expects the content to be specified as JSON object procedure Notify(aTable: TSQLRecordClass; aID: TID; const aJSON: RawUTF8; aAction: TSQLOccasion); overload; /// TSQLRest instance shall call this method when a record is retrieved, // added or updated // - this overloaded method expects the content to be specified as JSON object, // and TSQLRecordClass to be specified as its index in Rest.Model.Tables[] procedure Notify(aTableIndex: integer; aID: TID; const aJSON: RawUTF8; aAction: TSQLOccasion); overload; /// TSQLRest instance shall call this method when a record is added or updated // - this overloaded method will call the other Trace method, serializing // the supplied aRecord content as JSON (not in the case of seDelete) procedure Notify(aRecord: TSQLRecord; aAction: TSQLOccasion); overload; /// TSQLRest instance shall call this method when a record is deleted // - this method is dedicated for a record deletion procedure NotifyDeletion(aTable: TSQLRecordClass; aID: TID); overload; /// TSQLRest instance shall call this method when a record is deleted // - this method is dedicated for a record deletion // - TSQLRecordClass to be specified as its index in Rest.Model.Tables[] procedure NotifyDeletion(aTableIndex: integer; aID: TID); overload; /// TSQLRest instance shall call this method when records are deleted // - TSQLRecordClass to be specified as its index in Rest.Model.Tables[] procedure NotifyDeletions(aTableIndex: integer; const aIDs: array of Int64); overload; end; /// how a TSQLRest class may execute read or write operations // - used e.g. for TSQLRestServer.AcquireWriteMode or // TSQLRestServer.AcquireExecutionMode/AcquireExecutionLockedTimeOut TSQLRestServerAcquireMode = ( amUnlocked, amLocked, amBackgroundThread, amBackgroundORMSharedThread {$ifndef LVCL}, amMainThread{$endif}); /// class-reference type (metaclass) of a TSQLRest kind TSQLRestClass = class of TSQLRest; /// a dynamic array of TSQLRest instances TSQLRestDynArray = array of TSQLRest; /// a dynamic array of TSQLRest instances, owniing the instances TSQLRestObjArray = array of TSQLRest; /// used to store the execution parameters for a TSQLRest instance TSQLRestAcquireExecution = class(TSynPersistentLock) public /// how read or write operations will be executed Mode: TSQLRestServerAcquireMode; /// delay before failing to acquire the lock LockedTimeOut: cardinal; /// background thread instance (if any) Thread: TSynBackgroundThreadMethod; /// finalize the memory structure, and the associated background thread destructor Destroy; override; end; /// optionally called after TSQLRest.AsynchRedirect background execution // - to retrieve any output result value, as JSON-encoded content // - as used in TSQLRestBackgroundTimer.AsynchBackgroundExecute protected method TOnAsynchRedirectResult = procedure(const aMethod: TServiceMethod; const aInstance: IInvokable; const aParams, aResult: RawUTF8) of object; /// TThread able to run one or several tasks at a periodic pace, or do // asynchronous interface or batch execution, with proper TSQLRest integration // - used e.g. by TSQLRest.TimerEnable/AsynchRedirect/AsynchBatchStart methods // - TSQLRest.BackgroundTimer will define one instance, but you may create // other dedicated instances to instantiate separated threads TSQLRestBackgroundTimer = class(TSynBackgroundTimer) protected fRest: TSQLRest; fBackgroundBatch: array of TSQLRestBatchLocked; fBackgroundInterning: array of TRawUTF8Interning; fBackgroundInterningMaxRefCount: integer; // used by AsynchRedirect() and AsynchBatch() function AsynchBatchIndex(aTable: TSQLRecordClass): integer; function AsynchBatchLocked(aTable: TSQLRecordClass; out aBatch: TSQLRestBatchLocked): boolean; procedure AsynchBatchUnLock(aBatch: TSQLRestBatchLocked); procedure AsynchBatchExecute(Sender: TSynBackgroundTimer; Event: TWaitResult; const Msg: RawUTF8); procedure AsynchBackgroundExecute(Sender: TSynBackgroundTimer; Event: TWaitResult; const Msg: RawUTF8); procedure AsynchBackgroundInterning(Sender: TSynBackgroundTimer; Event: TWaitResult; const Msg: RawUTF8); public /// initialize the thread for a periodic task processing constructor Create(aRest: TSQLRest; const aThreadName: RawUTF8=''; aStats: TSynMonitorClass=nil); reintroduce; virtual; /// finalize the thread destructor Destroy; override; /// define asynchronous execution of interface methods in a background thread // - this method implements any interface via a fake class, which will // redirect all methods calls into calls of another interface, but as a FIFO // in a background thread, shared with TimerEnable/TimerDisable process // - parameters will be serialized and stored as JSON in the queue // - by design, only procedure methods without any output parameters are // allowed, since their execution will take place asynchronously // - of course, a slight delay is introduced in aDestinationInterface // methods execution, but the main process thread is not delayed any more, // and is free from potential race conditions // - the returned fake aCallbackInterface should be freed before TSQLRest // is destroyed, to release the redirection resources // - it is an elegant resolution to the most difficult implementation // problem of SOA callbacks, which is to avoid race condition on reentrance, // e.g. if a callback is run from a thread, and then the callback code try // to execute something in the context of the initial thread, protected // by a critical section (mutex) procedure AsynchRedirect(const aGUID: TGUID; const aDestinationInterface: IInvokable; out aCallbackInterface; const aOnResult: TOnAsynchRedirectResult=nil); overload; /// define asynchronous execution of interface methods in a background thread // - this method implements any interface via a fake class, which will // redirect all methods calls into calls of another interface, but as a FIFO // in a background thread, shared with TimerEnable/TimerDisable process // - parameters will be serialized and stored as JSON in the queue // - by design, only procedure methods without any output parameters are // allowed, since their execution will take place asynchronously // - of course, a slight delay is introduced in aDestinationInterface // methods execution, but the main process thread is not delayed any more, // and is free from potential race conditions // - the returned fake aCallbackInterface should be freed before TSQLRest // is destroyed, to release the redirection resources // - it is an elegant resolution to the most difficult implementation // problem of SOA callbacks, which is to avoid race condition on reentrance, // e.g. if a callback is run from a thread, and then the callback code try // to execute something in the context of the initial thread, protected // by a critical section (mutex) procedure AsynchRedirect(const aGUID: TGUID; const aDestinationInstance: TInterfacedObject; out aCallbackInterface; const aOnResult: TOnAsynchRedirectResult=nil); overload; /// prepare an asynchronous ORM BATCH process, executed in a background thread // - will initialize a TSQLRestBatch and call TimerEnable to initialize the // background thread, following the given processing period (in seconds), // or the TSQLRestBatch.Count threshold to call BatchSend // - actual REST/CRUD commands will take place via AsynchBatchAdd, // AsynchBatchUpdate and AsynchBatchDelete methods // - only a single AsynchBatch() call per Table is allowed at a time, unless // AsynchBatchStop method is used to flush the current asynchronous BATCH // - using a BATCH in a dedicated thread will allow very fast bacgkround // asynchronous process of ORM methods, sufficient for most use cases function AsynchBatchStart(Table: TSQLRecordClass; SendSeconds: integer; PendingRowThreshold: integer=500; AutomaticTransactionPerRow: integer=1000; Options: TSQLRestBatchOptions=[boExtendedJSON]): boolean; /// finalize asynchronous ORM BATCH process, executed in a background thread // - should have been preceded by a call to AsynchBatch(), or returns false // - Table=nil will release all existing batch instances function AsynchBatchStop(Table: TSQLRecordClass): boolean; /// create a new ORM member in a BATCH to be written in a background thread // - should have been preceded by a call to AsynchBatchStart(), or returns -1 // - is a wrapper around TSQLRestBatch.Add() sent in the Timer thread, // so will return the index in the BATCH rows, not the created TID // - this method is thread-safe function AsynchBatchAdd(Value: TSQLRecord; SendData: boolean; ForceID: boolean=false; const CustomFields: TSQLFieldBits=[]; DoNotAutoComputeFields: boolean=false): integer; /// append some JSON content in a BATCH to be writen in a background thread // - could be used to emulate AsynchBatchAdd() with an already pre-computed // JSON object // - is a wrapper around TSQLRestBatch.RawAdd() sent in the Timer thread, // so will return the index in the BATCH rows, not the created TID // - this method is thread-safe function AsynchBatchRawAdd(Table: TSQLRecordClass; const SentData: RawUTF8): integer; /// append some JSON content in a BATCH to be writen in a background thread // - could be used to emulate AsynchBatchAdd() with an already pre-computed // JSON object, as stored in a TTextWriter instance // - is a wrapper around TSQLRestBatch.RawAppend.AddNoJSONEscape(SentData) // in the Timer thread // - this method is thread-safe procedure AsynchBatchRawAppend(Table: TSQLRecordClass; SentData: TTextWriter); /// update an ORM member in a BATCH to be written in a background thread // - should have been preceded by a call to AsynchBatchStart(), or returns -1 // - is a wrapper around the TSQLRestBatch.Update() sent in the Timer thread // - this method is thread-safe function AsynchBatchUpdate(Value: TSQLRecord; const CustomFields: TSQLFieldBits=[]; DoNotAutoComputeFields: boolean=false): integer; /// delete an ORM member in a BATCH to be written in a background thread // - should have been preceded by a call to AsynchBatchStart(), or returns -1 // - is a wrapper around the TSQLRestBatch.Delete() sent in the Timer thread // - this method is thread-safe function AsynchBatchDelete(Table: TSQLRecordClass; ID: TID): integer; /// allows background garbage collection of specified RawUTF8 interning // - will run Interning.Clean(2) every 5 minutes by default // - set InterningMaxRefCount=0 to disable process of the Interning instance procedure AsynchInterning(Interning: TRawUTF8Interning; InterningMaxRefCount: integer=2; PeriodMinutes: integer=5); published /// the identifier of the thread, as logged property Name: RawUTF8 read fThreadName; end; /// a generic REpresentational State Transfer (REST) client/server class TSQLRest = class protected fModel: TSQLModel; fCache: TSQLRestCache; fTransactionActiveSession: cardinal; fTransactionTable: TSQLRecordClass; fServerTimestampOffset: TDateTime; fServerTimestampCacheTix: cardinal; fServerTimestampCacheValue: TTimeLogBits; fServices: TServiceContainer; fPrivateGarbageCollector: TSynObjectList; fRoutingClass: TSQLRestServerURIContextClass; fBackgroundTimer: TSQLRestBackgroundTimer; fOnDecryptBody, fOnEncryptBody: TNotifyRestBody; fCustomEncryptAES: TAESAbstract; fCustomEncryptSign: TSynSigner; fCustomEncryptCompress: TAlgoCompress; fCustomEncryptContentPrefix, fCustomEncryptContentPrefixUpper, fCustomEncryptUrlIgnore: RawUTF8; fAcquireExecution: array[TSQLRestServerURIContextCommand] of TSQLRestAcquireExecution; {$ifdef WITHLOG} fLogClass: TSynLogClass; // =SQLite3Log by default fLogFamily: TSynLogFamily; // =SQLite3Log.Family by default procedure SetLogClass(aClass: TSynLogClass); virtual; function GetLogClass: TSynLogClass; {$endif} procedure InternalCustomEncrypt(Sender: TSQLRest; var Body,Head,Url: RawUTF8); procedure InternalCustomDecrypt(Sender: TSQLRest; var Body,Head,Url: RawUTF8); function EnsureBackgroundTimerExists: TSQLRestBackgroundTimer; /// log the corresponding text (if logging is enabled) procedure InternalLog(const Text: RawUTF8; Level: TSynLogInfo); overload; {$ifdef HASINLINE}inline;{$endif} procedure InternalLog(const Format: RawUTF8; const Args: array of const; Level: TSynLogInfo=sllTrace); overload; /// internal method used by Delete(Table,SQLWhere) method function InternalDeleteNotifyAndGetIDs(Table: TSQLRecordClass; const SQLWhere: RawUTF8; var IDs: TIDDynArray): boolean; /// retrieve the server time stamp // - default implementation will use fServerTimestampOffset to compute // the value from PC time (i.e. NowUTC+fServerTimestampOffset as TTimeLog) // - inherited classes may override this method, or set the appropriate // value in fServerTimestampOffset protected field function GetServerTimestamp: TTimeLog; virtual; /// compute the server time stamp offset from the given procedure SetServerTimestamp(const Value: TTimeLog); /// handle Client or Server side fast in-memory cache // - creates the internal fCache instance, if necessary function GetCache: TSQLRestCache; {$ifdef HASINLINE}inline;{$endif} /// returns TRUE if this table is worth caching (e.g. not in memory) // - this default implementation always returns TRUE (always allow cache) function CacheWorthItForTable(aTableIndex: cardinal): boolean; virtual; /// compute SELECT ... FROM TABLE WHERE ... function SQLComputeForSelect(Table: TSQLRecordClass; const FieldNames, WhereClause: RawUTF8): RawUTF8; /// wrapper method for RoutingClass property procedure SetRoutingClass(aServicesRouting: TSQLRestServerURIContextClass); /// wrapper methods to access fAcquireExecution[] function GetAcquireExecutionMode(Cmd: TSQLRestServerURIContextCommand): TSQLRestServerAcquireMode; procedure SetAcquireExecutionMode(Cmd: TSQLRestServerURIContextCommand; Value: TSQLRestServerAcquireMode); function GetAcquireExecutionLockedTimeOut(Cmd: TSQLRestServerURIContextCommand): cardinal; procedure SetAcquireExecutionLockedTimeOut(Cmd: TSQLRestServerURIContextCommand; Value: cardinal); /// internal method called by TSQLRestServer.Batch() to process fast sending // to remote database engine (e.g. Oracle bound arrays or MS SQL Bulk insert) // - returns TRUE if this method is handled by the engine, or FALSE if // individual calls to Engine*() are expected // - this default implementation returns FALSE // - an overridden method returning TRUE shall ensure that calls to // EngineAdd / EngineUpdate / EngineDelete (depending of supplied Method) // will properly handle operations until InternalBatchStop() is called function InternalBatchStart(Method: TSQLURIMethod; BatchOptions: TSQLRestBatchOptions): boolean; virtual; /// internal method called by TSQLRestServer.Batch() to process fast sending // to remote database engine (e.g. Oracle bound arrays or MS SQL Bulk insert) // - this default implementation will raise an EORMException (since // InternalBatchStart returns always FALSE at this TSQLRest level) // - InternalBatchStart/Stop may safely use a lock for multithreading: // implementation in TSQLRestServer.Batch use a try..finally block procedure InternalBatchStop; virtual; /// send/execute the supplied JSON BATCH content, and return the expected array // - this method will be implemented for TSQLRestClient and TSQLRestServer only // - this default implementation will trigger an EORMException // - warning: supplied JSON Data can be parsed in-place, so modified function EngineBatchSend(Table: TSQLRecordClass; var Data: RawUTF8; var Results: TIDDynArray; ExpectedResultsCount: integer): integer; virtual; /// any overriden TSQLRest class should call it in the initialization section class procedure RegisterClassNameForDefinition; // inherited classes should unserialize the other aDefinition properties by // overriding this method, in a reverse logic to overriden DefinitionTo() constructor RegisteredClassCreateFrom(aModel: TSQLModel; aDefinition: TSynConnectionDefinition); virtual; /// used by Add() and AddWithBlobs() before EngineAdd() procedure GetJSONValuesForAdd(TableIndex: integer; Value: TSQLRecord; ForceID, DoNotAutoComputeFields, WithBlobs: boolean; CustomFields: PSQLFieldBits; var result: RawUTF8); /// used by all overloaded Add() methods function InternalAdd(Value: TSQLRecord; SendData: boolean; CustomFields: PSQLFieldBits; ForceID, DoNotAutoComputeFields: boolean): TID; virtual; protected // these abstract methods must be overriden by real database engine /// retrieve a list of members as JSON encoded data // - implements REST GET collection // - returns '' on error, or JSON data, even with no result rows // - override this method for direct data retrieval from the database engine // and direct JSON export, avoiding a TSQLTable which allocates memory for every // field values before the JSON export // - can be called for a single Table (ModelRoot/Table), or with low level SQL // query (ModelRoot + SQL sent as request body) // - if ReturnedRowCount points to an integer variable, it must be filled with // the number of row data returned (excluding field names) // - this method must be implemented in a thread-safe manner function EngineList(const SQL: RawUTF8; ForceAJAX: Boolean=false; ReturnedRowCount: PPtrInt=nil): RawUTF8; virtual; abstract; /// Execute directly a SQL statement, without any result // - implements POST SQL on ModelRoot URI // - return true on success // - override this method for proper calling the database engine // - don't call this method in normal cases // - this method must be implemented to be thread-safe function EngineExecute(const aSQL: RawUTF8): boolean; virtual; abstract; /// get a member from its ID // - implements REST GET member // - returns the data of this object as JSON // - override this method for proper data retrieval from the database engine // - this method must be implemented in a thread-safe manner function EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; virtual; abstract; /// create a new member // - implements REST POST collection // - SentData can contain the JSON object with field values to be added // - class is taken from Model.Tables[TableModelIndex] // - returns the TSQLRecord ID/RowID value, 0 on error // - if a "RowID":.. or "ID":.. member is set in SentData, it shall force // this value as insertion ID // - override this method for proper calling the database engine // - this method must be implemented in a thread-safe manner function EngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; virtual; abstract; /// update a member // - implements REST PUT collection // - SentData can contain the JSON object with field values to be added // - returns true on success // - override this method for proper calling the database engine // - this method must be implemented in a thread-safe manner function EngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; virtual; abstract; /// delete a member // - implements REST DELETE collection // - returns true on success // - override this method for proper calling the database engine // - this method must be implemented in a thread-safe manner function EngineDelete(TableModelIndex: integer; ID: TID): boolean; virtual; abstract; /// delete several members, from a WHERE clause // - IDs[] contains the already-computed matching IDs for SQLWhere // - returns true on success // - override this method for proper calling the database engine, i.e. // using either IDs[] or a faster SQL statement // - this method must be implemented in a thread-safe manner function EngineDeleteWhere(TableModelIndex: integer; const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean; virtual; abstract; /// get a blob field content from its member ID and field name // - implements REST GET member with a supplied blob field name // - returns TRUE on success // - returns the data of this blob as raw binary (not JSON) in BlobData // - override this method for proper data retrieval from the database engine // - this method must be implemented in a thread-safe manner function EngineRetrieveBlob(TableModelIndex: integer; aID: TID; BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean; virtual; abstract; /// update a blob field content from its member ID and field name // - implements REST PUT member with a supplied blob field name // - returns TRUE on success // - the data of this blob must be specified as raw binary (not JSON) in BlobData // - override this method for proper data retrieval from the database engine // - this method must be implemented in a thread-safe manner function EngineUpdateBlob(TableModelIndex: integer; aID: TID; BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean; virtual; abstract; /// update an individual record field value from a specified ID or Value // - return true on success // - will allow execution of requests like // $ UPDATE tablename SET setfieldname=setvalue WHERE wherefieldname=wherevalue // - SetValue and WhereValue parameters must match our inline format, i.e. // by double quoted with " for strings, or be plain text for numbers - e.g. // $ Client.EngineUpdateField(TSQLMyRecord,'FirstName','"Smith"','RowID','10') // but you should better use the UpdateField() overload methods instead // - WhereFieldName and WhereValue must be set: for security reasons, // implementations of this method will reject an UPDATE without any WHERE // clause, so you won't be able to use it to execute such statements: // $ UPDATE tablename SET setfieldname=setvalue // - this method must be implemented in a thread-safe manner function EngineUpdateField(TableModelIndex: integer; const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; virtual; abstract; /// increments one integer field value // - this default implementation is just a wrapper around OneFieldValue + // UpdateField methods function EngineUpdateFieldIncrement(TableModelIndex: integer; ID: TID; const FieldName: RawUTF8; Increment: Int64): boolean; virtual; function GetCurrentSessionUserID: TID; virtual; abstract; public /// initialize the class, and associate it to a specified database Model constructor Create(aModel: TSQLModel); virtual; /// release internal used instances // - e.g. release associated TSQLModel or TServiceContainer destructor Destroy; override; /// save the TSQLRest properties into a persistent storage object // - you can then use TSQLRest.CreateFrom() to re-instantiate it // - current Definition.Key value will be used for the password encryption // - this default implementation will set the class name in Definition.Kind: // inherited classes should override this method and serialize other // properties, then override RegisteredClassCreateFrom() protected method // to initiate the very same instance procedure DefinitionTo(Definition: TSynConnectionDefinition); virtual; /// save the properties into a JSON file // - you can then use TSQLRest.CreateFromJSON() to re-instantiate it // - you can specify a custom Key, if the default is not enough for you function DefinitionToJSON(Key: cardinal=0): RawUTF8; /// save the properties into a JSON file // - you can then use TSQLRest.CreateFromFile() to re-instantiate it // - you can specify a custom Key, if the default is not enough for you procedure DefinitionToFile(const aJSONFile: TFileName; aKey: cardinal=0); /// create a new TSQLRest instance from its Model and stored values // - aDefinition.Kind will define the actual class which will be // instantiated: currently TSQLRestServerFullMemory, TSQLRestServerDB, // TSQLRestClientURINamedPipe, TSQLRestClientURIMessage, // TSQLHttpClientWinSock, TSQLHttpClientWinINet, TSQLHttpClientWinHTTP, // and TSQLHttpClientCurl classes are recognized by this method // - then other aDefinition fields will be used to refine the instance: // please refer to each overriden DefinitionTo() method documentation // - use TSQLRestMongoDBCreate() and/or TSQLRestExternalDBCreate() instead // to create a TSQLRest instance will all tables defined as external when // aDefinition.Kind is 'MongoDB' or a TSQLDBConnectionProperties class // - will raise an exception if the supplied definition are not valid class function CreateFrom(aModel: TSQLModel; aDefinition: TSynConnectionDefinition): TSQLRest; /// try to create a new TSQLRest instance from its Model and stored values // - will return nil if the supplied definition are not valid // - if the newly created instance is a TSQLRestServer, will force the // supplied aServerHandleAuthentication parameter to enable authentication class function CreateTryFrom(aModel: TSQLModel; aDefinition: TSynConnectionDefinition; aServerHandleAuthentication: boolean): TSQLRest; /// create a new TSQLRest instance from its Model and JSON stored values // - aDefinition.Kind will define the actual class which will be instantiated // - you can specify a custom Key, if the default is not safe enough for you class function CreateFromJSON(aModel: TSQLModel; const aJSONDefinition: RawUTF8; aKey: cardinal=0): TSQLRest; /// create a new TSQLRest instance from its Model and a JSON file // - aDefinition.Kind will define the actual class which will be instantiated // - you can specify a custom Key, if the default is not safe enough for you class function CreateFromFile(aModel: TSQLModel; const aJSONFile: TFileName; aKey: cardinal=0): TSQLRest; /// retrieve the registered class from the aDefinition.Kind string class function ClassFrom(aDefinition: TSynConnectionDefinition): TSQLRestClass; {$ifdef WITHLOG} /// the logging family used for this instance // - is set by default to SQLite3Log.Family, but could be set to something // else by setting a custom class to the LogClass property property LogFamily: TSynLogFamily read fLogFamily; {$endif} /// a local "Garbage collector" list, for some classes instances which must // live during the whole TSQLRestServer process // - is used internally by the class, but can be used for business code property PrivateGarbageCollector: TSynObjectList read fPrivateGarbageCollector; public /// get the row count of a specified table // - returns -1 on error // - returns the row count of the table on success // - calls internaly the "SELECT Count(*) FROM TableName;" SQL statement function TableRowCount(Table: TSQLRecordClass): Int64; virtual; /// check if there is some data rows in a specified table // - calls internaly a "SELECT RowID FROM TableName LIMIT 1" SQL statement, // which is much faster than testing if "SELECT count(*)" equals 0 - see // @http://stackoverflow.com/questions/8988915 function TableHasRows(Table: TSQLRecordClass): boolean; virtual; /// search for the last inserted ID in a specified table // - returns -1 on error // - will execute by default "SELECT max(rowid) FROM TableName" function TableMaxID(Table: TSQLRecordClass): TID; virtual; /// check if a given ID do exist for a given table function MemberExists(Table: TSQLRecordClass; ID: TID): boolean; virtual; /// get the UTF-8 encoded value of an unique field with a Where Clause // - example of use - including inlined parameters via :(...): // ! aClient.OneFieldValue(TSQLRecord,'Name','ID=:(23):') // you should better call the corresponding overloaded method as such: // ! aClient.OneFieldValue(TSQLRecord,'Name','ID=?',[aID]) // which is the same as calling: // ! aClient.OneFieldValue(TSQLRecord,'Name',FormatUTF8('ID=?',[],[23])) // - call internaly ExecuteList() to get the value function OneFieldValue(Table: TSQLRecordClass; const FieldName, WhereClause: RawUTF8): RawUTF8; overload; /// get the Int64 value of an unique field with a Where Clause // - call internaly ExecuteList() to get the value function OneFieldValueInt64(Table: TSQLRecordClass; const FieldName, WhereClause: RawUTF8; Default: Int64=0): Int64; /// get the UTF-8 encoded value of an unique field with a Where Clause // - this overloaded function will call FormatUTF8 to create the Where Clause // from supplied parameters, binding all '?' chars with Args[] values // - example of use: // ! aClient.OneFieldValue(TSQLRecord,'Name','ID=?',[aID]) // - call internaly ExecuteList() to get the value // - note that this method prototype changed with revision 1.17 of the // framework: array of const used to be Args and '%' in the FormatSQLWhere // statement, whereas it now expects bound parameters as '?' function OneFieldValue(Table: TSQLRecordClass; const FieldName: RawUTF8; const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const): RawUTF8; overload; /// get the UTF-8 encoded value of an unique field with a Where Clause // - this overloaded function will call FormatUTF8 to create the Where Clause // from supplied parameters, replacing all '%' chars with Args[], and all '?' // chars with Bounds[] (inlining them with :(...): and auto-quoting strings) // - example of use: // ! OneFieldValue(TSQLRecord,'Name','%=?',['ID'],[aID]) // - call internaly ExecuteList() to get the value function OneFieldValue(Table: TSQLRecordClass; const FieldName: RawUTF8; const WhereClauseFmt: RawUTF8; const Args, Bounds: array of const): RawUTF8; overload; /// get one integer value of an unique field with a Where Clause // - this overloaded function will return the field value as integer function OneFieldValue(Table: TSQLRecordClass; const FieldName: RawUTF8; const WhereClauseFmt: RawUTF8; const Args, Bounds: array of const; out Data: Int64): boolean; overload; /// get the UTF-8 encoded value of an unique field from its ID // - example of use: OneFieldValue(TSQLRecord,'Name',23) // - call internaly ExecuteList() to get the value function OneFieldValue(Table: TSQLRecordClass; const FieldName: RawUTF8; WhereID: TID): RawUTF8; overload; /// get the UTF-8 encoded value of some fields with a Where Clause // - example of use: MultiFieldValue(TSQLRecord,['Name'],Name,'ID=:(23):') // (using inlined parameters via :(...): is always a good idea) // - FieldValue[] will have the same length as FieldName[] // - return true on success, false on SQL error or no result // - call internaly ExecuteList() to get the list function MultiFieldValue(Table: TSQLRecordClass; const FieldName: array of RawUTF8; var FieldValue: array of RawUTF8; const WhereClause: RawUTF8): boolean; overload; /// get the UTF-8 encoded value of some fields from its ID // - example of use: MultiFieldValue(TSQLRecord,['Name'],Name,23) // - FieldValue[] will have the same length as FieldName[] // - return true on success, false on SQL error or no result // - call internaly ExecuteList() to get the list function MultiFieldValue(Table: TSQLRecordClass; const FieldName: array of RawUTF8; var FieldValue: array of RawUTF8; WhereID: TID): boolean; overload; /// get the UTF-8 encoded values of an unique field with a Where Clause // - example of use: OneFieldValue(TSQLRecord,'FirstName','Name=:("Smith"):',Data) // (using inlined parameters via :(...): is always a good idea) // - leave WhereClause void to get all records // - call internaly ExecuteList() to get the list // - returns TRUE on success, FALSE if no data was retrieved function OneFieldValues(Table: TSQLRecordClass; const FieldName: RawUTF8; const WhereClause: RawUTF8; out Data: TRawUTF8DynArray): boolean; overload; /// get the integer value of an unique field with a Where Clause // - example of use: OneFieldValue(TSQLRecordPeople,'ID','Name=:("Smith"):',Data) // (using inlined parameters via :(...): is always a good idea) // - leave WhereClause void to get all records // - call internaly ExecuteList() to get the list function OneFieldValues(Table: TSQLRecordClass; const FieldName: RawUTF8; const WhereClause: RawUTF8; var Data: TInt64DynArray; SQL: PRawUTF8=nil): boolean; overload; /// dedicated method used to retrieve free-text matching DocIDs // - this method works for TSQLRecordFTS3, TSQLRecordFTS4 and TSQLRecordFTS5 // - this method expects the column/field names to be supplied in the MATCH // statement clause // - example of use: FTSMatch(TSQLMessage,'Body MATCH :("linu*"):',IntResult) // (using inlined parameters via :(...): is always a good idea) function FTSMatch(Table: TSQLRecordFTS3Class; const WhereClause: RawUTF8; var DocID: TIDDynArray): boolean; overload; /// dedicated method used to retrieve free-text matching DocIDs with // enhanced ranking information // - this method works for TSQLRecordFTS3, TSQLRecordFTS4 and TSQLRecordFTS5 // - this method will search in all FTS3 columns, and except some floating-point // constants for weigthing each column (there must be the same number of // PerFieldWeight parameters as there are columns in the TSQLRecordFTS3 table) // - example of use: FTSMatch(TSQLDocuments,'"linu*"',IntResult,[1,0.5]) // which will sort the results by the rank obtained with the 1st column/field // beeing given twice the weighting of those in the 2nd (and last) column // - FTSMatch(TSQLDocuments,'linu*',IntResult,[1,0.5]) will perform a // SQL query as such, which is the fastest way of ranking according to // http://www.sqlite.org/fts3.html#appendix_a // $ SELECT RowID FROM Documents WHERE Documents MATCH 'linu*' // $ ORDER BY rank(matchinfo(Documents),1.0,0.5) DESC function FTSMatch(Table: TSQLRecordFTS3Class; const MatchClause: RawUTF8; var DocID: TIDDynArray; const PerFieldWeight: array of double; limit: integer=0; offset: integer=0): boolean; overload; /// get the CSV-encoded UTF-8 encoded values of an unique field with a Where Clause // - example of use: OneFieldValue(TSQLRecord,'FirstName','Name=:("Smith")',Data) // (using inlined parameters via :(...): is always a good idea) // - leave WhereClause void to get all records // - call internaly ExecuteList() to get the list // - using inlined parameters via :(...): in WhereClause is always a good idea function OneFieldValues(Table: TSQLRecordClass; const FieldName: RawUTF8; const WhereClause: RawUTF8=''; const Separator: RawUTF8=','): RawUTF8; overload; /// get the string-encoded values of an unique field into some TStrings // - Items[] will be filled with string-encoded values of the given field) // - Objects[] will be filled with pointer(ID) // - call internaly ExecuteList() to get the list // - returns TRUE on success, FALSE if no data was retrieved // - if IDToIndex is set, its value will be replaced with the index in // Strings.Objects[] where ID=IDToIndex^ // - using inlined parameters via :(...): in WhereClause is always a good idea function OneFieldValues(Table: TSQLRecordClass; const FieldName, WhereClause: RawUTF8; Strings: TStrings; IDToIndex: PID=nil): Boolean; overload; /// Execute directly a SQL statement, expecting a list of resutls // - return a result table on success, nil on failure // - FieldNames can be the CSV list of field names to be retrieved // - if FieldNames is '', will get all simple fields, excluding BLOBs // - if FieldNames is '*', will get ALL fields, including ID and BLOBs // - call internaly ExecuteList() to get the list // - using inlined parameters via :(...): in WhereClause is always a good idea function MultiFieldValues(Table: TSQLRecordClass; const FieldNames: RawUTF8; const WhereClause: RawUTF8=''): TSQLTableJSON; overload; virtual; /// Execute directly a SQL statement, expecting a list of resutls // - return a result table on success, nil on failure // - FieldNames can be the CSV list of field names to be retrieved // - if FieldNames is '', will get all simple fields, excluding BLOBs // - if FieldNames is '*', will get ALL fields, including ID and BLOBs // - this overloaded function will call FormatUTF8 to create the Where Clause // from supplied parameters, binding all '?' chars with Args[] values // - example of use: // ! aList := aClient.MultiFieldValues(TSQLRecord,'Name,FirstName','Salary>=?',[aMinSalary]); // - call overloaded MultiFieldValues() / ExecuteList() to get the list // - note that this method prototype changed with revision 1.17 of the // framework: array of const used to be Args and '%' in the WhereClauseFormat // statement, whereas it now expects bound parameters as '?' function MultiFieldValues(Table: TSQLRecordClass; const FieldNames: RawUTF8; const WhereClauseFormat: RawUTF8; const BoundsSQLWhere: array of const): TSQLTableJSON; overload; /// Execute directly a SQL statement, expecting a list of results // - return a result table on success, nil on failure // - FieldNames can be the CSV list of field names to be retrieved // - if FieldNames is '', will get all simple fields, excluding BLOBs // - if FieldNames is '*', will get ALL fields, including ID and BLOBs // - in this version, the WHERE clause can be created with the same format // as FormatUTF8() function, replacing all '%' chars with Args[], and all '?' // chars with Bounds[] (inlining them with :(...): and auto-quoting strings) // - example of use: // ! Table := MultiFieldValues(TSQLRecord,'Name','%=?',['ID'],[aID]); // - call overloaded MultiFieldValues() / ExecuteList() to get the list function MultiFieldValues(Table: TSQLRecordClass; const FieldNames: RawUTF8; const WhereClauseFormat: RawUTF8; const Args, Bounds: array of const): TSQLTableJSON; overload; /// retrieve the main field (mostly 'Name') value of the specified record // - use GetMainFieldName() method to get the main field name // - use OneFieldValue() method to get the field value // - return '' if no such field or record exists // - if ReturnFirstIfNoUnique is TRUE and no unique property is found, // the first RawUTF8 property is returned anyway function MainFieldValue(Table: TSQLRecordClass; ID: TID; ReturnFirstIfNoUnique: boolean=false): RawUTF8; /// return the ID of the record which main field match the specified value // - search field is mainly the "Name" property, i.e. the one with // "stored AS_UNIQUE" (i.e. "stored false") definition on most TSQLRecord // - returns 0 if no matching record was found } function MainFieldID(Table: TSQLRecordClass; const Value: RawUTF8): TID; /// return the IDs of the record which main field match the specified values // - search field is mainly the "Name" property, i.e. the one with // "stored AS_UNIQUE" (i.e. "stored false") definition on most TSQLRecord // - if any of the Values[] is not existing, then no ID will appear in the // IDs[] array - e.g. it will return [] if no matching record was found // - returns TRUE if any matching ID was found (i.e. if length(IDs)>0) } function MainFieldIDs(Table: TSQLRecordClass; const Values: array of RawUTF8; out IDs: TIDDynArray): boolean; public // here are REST basic direct calls (works with Server or Client) /// get a member from a SQL statement // - implements REST GET collection // - return true on success // - Execute 'SELECT * FROM TableName WHERE SQLWhere LIMIT 1' SQL Statememt // (using inlined parameters via :(...): in SQLWhere is always a good idea) // - since no record is specified, locking is pointless here // - default implementation call ExecuteList(), and fill Value from a // temporary TSQLTable // - if aCustomFieldsCSV is '', will get all simple fields, excluding BLOBs // and TSQLRecordMany fields (use RetrieveBlob method or set // TSQLRestClientURI.ForceBlobTransfert) // - if aCustomFieldsCSV is '*', will get ALL fields, including ID and BLOBs // - if this default set of simple fields does not fit your need, you could // specify your own set function Retrieve(const SQLWhere: RawUTF8; Value: TSQLRecord; const aCustomFieldsCSV: RawUTF8=''): boolean; overload; virtual; /// get a member from a SQL statement // - implements REST GET collection // - return true on success // - same as Retrieve(const SQLWhere: RawUTF8; Value: TSQLRecord) method, but // this overloaded function will call FormatUTF8 to create the Where Clause // from supplied parameters, replacing all '%' chars with Args[], and all '?' // chars with Bounds[] (inlining them with :(...): and auto-quoting strings) // - if aCustomFieldsCSV is '', will get all simple fields, excluding BLOBs // - if aCustomFieldsCSV is '*', will get ALL fields, including ID and BLOBs function Retrieve(const WhereClauseFmt: RawUTF8; const Args,Bounds: array of const; Value: TSQLRecord; const aCustomFieldsCSV: RawUTF8=''): boolean; overload; /// get a member from its ID // - return true on success // - Execute 'SELECT * FROM TableName WHERE ID=:(aID): LIMIT 1' SQL Statememt // - if ForUpdate is true, the REST method is LOCK and not GET: it tries to lock // the corresponding record, then retrieve its content; caller has to call // UnLock() method after Value usage, to release the record // - this method will call EngineRetrieve() abstract method // - the TSQLRawBlob (BLOB) fields are not retrieved by this method, to // preserve bandwidth: use the RetrieveBlob() methods for handling // BLOB fields, or set either the TSQLRestClientURI.ForceBlobTransfert // or TSQLRestClientURI.ForceBlobTransfertTable[] properties // - the TSQLRecordMany fields are not retrieved either: they are separate // instances created by TSQLRecordMany.Create, with dedicated methods to // access to the separated pivot table function Retrieve(aID: TID; Value: TSQLRecord; ForUpdate: boolean=false): boolean; overload; virtual; /// get a member from its TRecordReference property content // - instead of the other Retrieve() methods, this implementation Create an // instance, with the appropriated class stored in Reference // - returns nil on any error (invalid Reference e.g.) // - if ForUpdate is true, the REST method is LOCK and not GET: it tries to lock // the corresponding record, then retrieve its content; caller has to call // UnLock() method after Value usage, to release the record // - the TSQLRawBlob (BLOB) fields are not retrieved by this method, to // preserve bandwidth: use the RetrieveBlob() methods for handling // BLOB fields, or set either the TSQLRestClientURI.ForceBlobTransfert // or TSQLRestClientURI.ForceBlobTransfertTable[] properties // - the TSQLRecordMany fields are not retrieved either: they are separate // instances created by TSQLRecordMany.Create, with dedicated methods to // access to the separated pivot table function Retrieve(Reference: TRecordReference; ForUpdate: boolean=false): TSQLRecord; overload; virtual; /// get a member from a published property TSQLRecord // - those properties are not class instances, but TObject(aRecordID) // - is just a wrapper around Retrieve(aPublishedRecord.ID,aValue) // - return true on success function Retrieve(aPublishedRecord, aValue: TSQLRecord): boolean; overload; /// get a list of members from a SQL statement as TObjectList // - implements REST GET collection // - for better server speed, the WHERE clause should use bound parameters // identified as '?' in the FormatSQLWhere statement, which is expected to // follow the order of values supplied in BoundsSQLWhere open array - use // DateToSQL()/DateTimeToSQL() for TDateTime, or directly any integer, // double, currency, RawUTF8 values to be bound to the request as parameters // - aCustomFieldsCSV can be the CSV list of field names to be retrieved // - if aCustomFieldsCSV is '', will get all simple fields, excluding BLOBs // - if aCustomFieldsCSV is '*', will get ALL fields, including ID and BLOBs // - return a TObjectList on success (possibly with Count=0) - caller is // responsible of freeing the instance // - this TObjectList will contain a list of all matching records // - return nil on error function RetrieveList(Table: TSQLRecordClass; const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const; const aCustomFieldsCSV: RawUTF8=''): TObjectList; overload; /// get a list of members from a SQL statement as RawJSON // - implements REST GET collection // - for better server speed, the WHERE clause should use bound parameters // identified as '?' in the FormatSQLWhere statement, which is expected to // follow the order of values supplied in BoundsSQLWhere open array - use // DateToSQL()/DateTimeToSQL() for TDateTime, or directly any integer, // double, currency, RawUTF8 values to be bound to the request as parameters // - aCustomFieldsCSV can be the CSV list of field names to be retrieved // - if aCustomFieldsCSV is '', will get all simple fields, excluding BLOBs // - if aCustomFieldsCSV is '*', will get ALL fields, including ID and BLOBs // - returns the raw JSON array content with all items on success, with // our expanded / not expanded JSON format - so can be used with SOA methods // and RawJSON results, for direct process from the client side // - returns '' on error // - the data is directly retrieved from raw JSON as returned by the database // without any conversion, so this method will be the fastest, but complex // types like dynamic array will be returned as Base64-encoded blob value - // if you need proper JSON access to those, see RetrieveDocVariantArray() function RetrieveListJSON(Table: TSQLRecordClass; const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const; const aCustomFieldsCSV: RawUTF8=''; aForceAJAX: boolean=false): RawJSON; overload; /// get a list of members from a SQL statement as RawJSON // - implements REST GET collection // - this overloaded version expect the SQLWhere clause to be already // prepared with inline parameters using a previous FormatUTF8() call // - aCustomFieldsCSV can be the CSV list of field names to be retrieved // - if aCustomFieldsCSV is '', will get all simple fields, excluding BLOBs // - if aCustomFieldsCSV is '*', will get ALL fields, including ID and BLOBs // - returns the raw JSON array content with all items on success, with // our expanded / not expanded JSON format - so can be used with SOA methods // and RawJSON results, for direct process from the client side // - returns '' on error // - the data is directly retrieved from raw JSON as returned by the database // without any conversion, so this method will be the fastest, but complex // types like dynamic array will be returned as Base64-encoded blob value - // if you need proper JSON access to those, see RetrieveDocVariantArray() function RetrieveListJSON(Table: TSQLRecordClass; const SQLWhere: RawUTF8; const aCustomFieldsCSV: RawUTF8=''; aForceAJAX: boolean=false): RawJSON; overload; {$ifndef NOVARIANTS} /// get a list of all members from a SQL statement as a TDocVariant // - implements REST GET collection // - if ObjectName='', it will return a TDocVariant of dvArray kind // - if ObjectName is set, it will return a TDocVariant of dvObject kind, // with one property containing the array of values: this returned variant // can be pasted e.g. directly as parameter to TSynMustache.Render() // - aCustomFieldsCSV can be the CSV list of field names to be retrieved // - if aCustomFieldsCSV is '', will get all simple fields, excluding BLOBs // - if aCustomFieldsCSV is '*', will get ALL fields, including ID and BLOBs // - the data will be converted to variants and TDocVariant following the // TSQLRecord layout, so complex types like dynamic array will be returned // as a true array of values (in contrast to the RetrieveListJSON method) // - warning: under FPC, we observed that assigning the result of this // method to a local variable may circumvent a memory leak FPC bug function RetrieveDocVariantArray(Table: TSQLRecordClass; const ObjectName, CustomFieldsCSV: RawUTF8; FirstRecordID: PID=nil; LastRecordID: PID=nil): variant; overload; {$ifdef HASINLINE}inline;{$endif} /// get a list of members from a SQL statement as a TDocVariant // - implements REST GET collection over a specified WHERE clause // - if ObjectName='', it will return a TDocVariant of dvArray kind // - if ObjectName is set, it will return a TDocVariant of dvObject kind, // with one property containing the array of values: this returned variant // can be pasted e.g. directly as parameter to TSynMustache.Render() // - for better server speed, the WHERE clause should use bound parameters // identified as '?' in the FormatSQLWhere statement, which is expected to // follow the order of values supplied in BoundsSQLWhere open array - use // DateToSQL()/DateTimeToSQL() for TDateTime, or directly any integer, // double, currency, RawUTF8 values to be bound to the request as parameters // - aCustomFieldsCSV can be the CSV list of field names to be retrieved // - if aCustomFieldsCSV is '', will get all simple fields, excluding BLOBs // - if aCustomFieldsCSV is '*', will get ALL fields, including ID and BLOBs // - the data will be converted to variants and TDocVariant following the // TSQLRecord layout, so complex types like dynamic array will be returned // as a true array of values (in contrast to the RetrieveListJSON method) // - warning: under FPC, we observed that assigning the result of this // method to a local variable may circumvent a memory leak FPC bug function RetrieveDocVariantArray(Table: TSQLRecordClass; const ObjectName: RawUTF8; const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const; const CustomFieldsCSV: RawUTF8; FirstRecordID: PID=nil; LastRecordID: PID=nil): variant; overload; /// get all values of a SQL statement on a single column as a TDocVariant array // - implements REST GET collection on a single field // - for better server speed, the WHERE clause should use bound parameters // identified as '?' in the FormatSQLWhere statement, which is expected to // follow the order of values supplied in BoundsSQLWhere open array - use // DateToSQL()/DateTimeToSQL() for TDateTime, or directly any integer, // double, currency, RawUTF8 values to be bound to the request as parameters // - the data will be converted to variants and TDocVariant following the // TSQLRecord layout, so complex types like dynamic array will be returned // as a true array of values (in contrast to the RetrieveListJSON method) function RetrieveOneFieldDocVariantArray(Table: TSQLRecordClass; const FieldName, FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const): variant; /// get one member from a SQL statement as a TDocVariant // - implements REST GET collection // - the data will be converted to a TDocVariant variant following the // TSQLRecord layout, so complex types like dynamic array will be returned // as a true array of values function RetrieveDocVariant(Table: TSQLRecordClass; const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const; const CustomFieldsCSV: RawUTF8): variant; {$endif NOVARIANTS} /// get a list of members from a SQL statement as T*ObjArray // - implements REST GET collection // - for better server speed, the WHERE clause should use bound parameters // identified as '?' in the FormatSQLWhere statement, which is expected to // follow the order of values supplied in BoundsSQLWhere open array - use // DateToSQL()/DateTimeToSQL() for TDateTime, or directly any integer, // double, currency, RawUTF8 values to be bound to the request as parameters // - aCustomFieldsCSV can be the CSV list of field names to be retrieved // - if aCustomFieldsCSV is '', will get all simple fields, excluding BLOBs // - if aCustomFieldsCSV is '*', will get ALL fields, including ID and BLOBs // - set the T*ObjArray variable with all items on success - so that it can // be used with SOA methods // - it is up to the caller to ensure that ObjClear(ObjArray) is called // when the T*ObjArray list is not needed any more // - returns true on success, false on error function RetrieveListObjArray(var ObjArray; Table: TSQLRecordClass; const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const; const aCustomFieldsCSV: RawUTF8=''): boolean; /// get and append a list of members as an expanded JSON array // - implements REST GET collection // - generates '[{rec1},{rec2},...]' using a loop similar to: // ! while FillOne do .. AppendJsonObject() .. // - for better server speed, the WHERE clause should use bound parameters // identified as '?' in the FormatSQLWhere statement, which is expected to // follow the order of values supplied in BoundsSQLWhere open array - use // DateToSQL()/DateTimeToSQL() for TDateTime, or directly any integer, // double, currency, RawUTF8 values to be bound to the request as parameters // - if OutputFieldName is set, the JSON array will be written as a JSON, // property i.e. surrounded as '"OutputFieldName":[....],' - note ending ',' // - CustomFieldsCSV can be the CSV list of field names to be retrieved // - if CustomFieldsCSV is '', will get all simple fields, excluding BLOBs // - if CustomFieldsCSV is '*', will get ALL fields, including ID and BLOBs // - is just a wrapper around TSQLRecord.AppendFillAsJsonArray() procedure AppendListAsJsonArray(Table: TSQLRecordClass; const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const; const OutputFieldName: RawUTF8; W: TJSONSerializer; const CustomFieldsCSV: RawUTF8=''); /// dedicated method used to retrieve matching IDs using a fast R-Tree index // - a TSQLRecordRTree is associated to a TSQLRecord with a specified BLOB // field, and will call TSQLRecordRTree BlobToCoord and ContainedIn virtual // class methods to execute an optimized SQL query // - as alternative, with SQLite3 >= 3.24.0, you may use Auxiliary Columns // - will return all matching DataTable IDs in DataID[] // - will generate e.g. the following statement // $ SELECT MapData.ID From MapData, MapBox WHERE MapData.ID=MapBox.ID // $ AND minX>=:(-81.0): AND maxX<=:(-79.6): AND minY>=:(35.0): AND :(maxY<=36.2): // $ AND MapBox_in(MapData.BlobField,:('\uFFF0base64encoded-81,-79.6,35,36.2'):); // when the following Delphi code is executed: // ! aClient.RTreeMatch(TSQLRecordMapData,'BlobField',TSQLRecordMapBox, // ! aMapData.BlobField,ResultID); function RTreeMatch(DataTable: TSQLRecordClass; const DataTableBlobFieldName: RawUTF8; RTreeTable: TSQLRecordRTreeClass; const DataTableBlobField: RawByteString; var DataID: TIDDynArray): boolean; /// Execute directly a SQL statement, expecting a list of results // - return a result table on success, nil on failure // - will call EngineList() abstract method to retrieve its JSON content function ExecuteList(const Tables: array of TSQLRecordClass; const SQL: RawUTF8): TSQLTableJSON; virtual; /// Execute directly a SQL statement, expecting a list of results // - you should not have to use this method, but the ORM versions instead // - return a result set as JSON on success, '' on failure // - will call EngineList() abstract method to retrieve its JSON content function ExecuteJson(const Tables: array of TSQLRecordClass; const SQL: RawUTF8; ForceAJAX: Boolean=false; ReturnedRowCount: PPtrInt=nil): RawJSON; virtual; /// Execute directly a SQL statement, without any expected result // - implements POST SQL on ModelRoot URI // - return true on success // - will call EngineExecute() abstract method to run the SQL statement function Execute(const aSQL: RawUTF8): boolean; virtual; /// Execute directly a SQL statement with supplied parameters, with no result // - expect the same format as FormatUTF8() function, replacing all '%' chars // with Args[] values // - return true on success function ExecuteFmt(const SQLFormat: RawUTF8; const Args: array of const): boolean; overload; /// Execute directly a SQL statement with supplied parameters, with no result // - expect the same format as FormatUTF8() function, replacing all '%' chars // with Args[] values, and all '?' chars with Bounds[] (inlining them // with :(...): and auto-quoting strings) // - return true on success function ExecuteFmt(const SQLFormat: RawUTF8; const Args, Bounds: array of const): boolean; overload; /// unlock the corresponding record // - record should have been locked previously e.g. with Retrieve() and // forupdate=true, i.e. retrieved not via GET with LOCK REST-like verb // - use our custom UNLOCK REST-like verb // - returns true on success function UnLock(Table: TSQLRecordClass; aID: TID): boolean; overload; virtual; abstract; /// unlock the corresponding record // - record should have been locked previously e.g. with Retrieve() and // forupdate=true, i.e. retrieved not via GET with LOCK REST-like verb // - use our custom UNLOCK REST-like method // - calls internally UnLock() above // - returns true on success function UnLock(Rec: TSQLRecord): boolean; overload; /// create a new member // - implements REST POST collection // - if SendData is true, client sends the current content of Value with the // request, otherwise record is created with default values // - if ForceID is true, client sends the Value.ID field to use this ID for // adding the record (instead of a database-generated ID) // - on success, returns the new RowID value; on error, returns 0 // - on success, Value.ID is updated with the new RowID // - the TSQLRawBlob(BLOB) fields values are not set by this method, to // preserve bandwidth - see UpdateBlobFields() and AddWithBlobs() methods // - the TSQLRecordMany fields are not set either: they are separate // instances created by TSQLRecordMany.Create, with dedicated methods to // access to the separated pivot table // - this method will call EngineAdd() to perform the request function Add(Value: TSQLRecord; SendData: boolean; ForceID: boolean=false; DoNotAutoComputeFields: boolean=false): TID; overload; {$ifdef FPC}inline;{$endif} /// create a new member, including selected fields // - implements REST POST collection // - if ForceID is true, client sends the Value.ID field to use this ID for // adding the record (instead of a database-generated ID) // - this method will call EngineAdd() to perform the request function Add(Value: TSQLRecord; const CustomCSVFields: RawUTF8; ForceID: boolean=false; DoNotAutoComputeFields: boolean=false): TID; overload; /// create a new member, including selected fields // - implements REST POST collection // - if ForceID is true, client sends the Value.ID field to use this ID for // adding the record (instead of a database-generated ID) // - this method will call EngineAdd() to perform the request function Add(Value: TSQLRecord; const CustomFields: TSQLFieldBits; ForceID: boolean=false; DoNotAutoComputeFields: boolean=false): TID; overload; {$ifdef FPC}inline;{$endif} /// create a new member, including its BLOB fields // - implements REST POST collection // - this method will create a JSON representation of the document // including the BLOB fields as Base64 encoded text, so will be less // efficient than a dual Add() + UpdateBlobFields() methods if the // binary content has a non trivial size // - this method will call EngineAdd() to perform the request function AddWithBlobs(Value: TSQLRecord; ForceID: boolean=false; DoNotAutoComputeFields: boolean=false): TID; virtual; /// create a new member, from a supplied list of field values // - implements REST POST collection // - the aSimpleFields parameters must follow explicitely the order of published // properties of the supplied aTable class, excepting the TSQLRawBlob and // TSQLRecordMany kind (i.e. only so called "simple fields") // - the aSimpleFields must have exactly the same count of parameters as // there are "simple fields" in the published properties // - if ForcedID is set to non null, client sends this ID to be used // when adding the record (instead of a database-generated ID) // - on success, returns the new RowID value; on error, returns 0 // - call internaly the Add virtual method above function AddSimple(aTable: TSQLRecordClass; const aSimpleFields: array of const; ForcedID: TID=0): TID; /// update a member from Value simple fields content // - implements REST PUT collection // - return true on success // - the TSQLRawBlob(BLOB) fields values are not updated by this method, to // preserve bandwidth: use the UpdateBlob() methods for handling BLOB fields // - the TSQLRecordMany fields are not set either: they are separate // instances created by TSQLRecordMany.Create, with dedicated methods to // access to the separated pivot table // - if CustomFields is left void, the simple fields will be used, or the // fields retrieved via a previous FillPrepare() call; otherwise, you can // specify your own set of fields to be transmitted (including BLOBs, even // if they will be Base64-encoded within the JSON content) - CustomFields // could be computed by TSQLRecordProperties.FieldBitsFromCSV() // or TSQLRecordProperties.FieldBitsFromRawUTF8() // - this method will always compute and send any TModTime fields // - this method will call EngineUpdate() to perform the request function Update(Value: TSQLRecord; const CustomFields: TSQLFieldBits=[]; DoNotAutoComputeFields: boolean=false): boolean; overload; virtual; /// update a member from Value simple fields content // - implements REST PUT collection // - return true on success // - is an overloaded method to Update(Value,FieldBitsFromCSV()) function Update(Value: TSQLRecord; const CustomCSVFields: RawUTF8; DoNotAutoComputeFields: boolean=false): boolean; overload; /// update a member from a supplied list of simple field values // - implements REST PUT collection // - the aSimpleFields parameters MUST follow explicitely both count and // order of published properties of the supplied aTable class, excepting the // TSQLRawBlob and TSQLRecordMany kind (i.e. only so called "simple fields") // - return true on success // - call internaly the Update() / EngineUpdate() virtual methods function Update(aTable: TSQLRecordClass; aID: TID; const aSimpleFields: array of const): boolean; overload; /// create or update a member, depending if the Value has already an ID // - implements REST POST if Value.ID=0 or ForceID is set, or a REST PUT // collection to update the record pointed by a Value.ID<>0 // - will return the created or updated ID function AddOrUpdate(Value: TSQLRecord; ForceID: boolean=false): TID; /// update one field/column value a given member // - implements REST PUT collection with one field value // - only one single field shall be specified in FieldValue, but could // be of any kind of value - for BLOBs, you should better use UpdateBlob() // - return true on success // - call internaly the EngineUpdateField() abstract method // - note that this method won't update the TModTime properties: you should // rather use a classic Retrieve()/FillPrepare() followed by Update(); but // it will notify the internal Cache function UpdateField(Table: TSQLRecordClass; ID: TID; const FieldName: RawUTF8; const FieldValue: array of const): boolean; overload; virtual; /// update one field in one or several members, depending on a WHERE clause // - implements REST PUT collection with one field value on a one where value // - only one single field shall be specified in FieldValue, but could // be of any kind of value - for BLOBs, you should better use UpdateBlob() // - only one single field shall be specified in WhereFieldValue, but could // be of any kind of value - for security reasons, void WHERE clause will // be rejected // - return true on success // - call internaly the EngineUpdateField() abstract method // - note that this method won't update the TModTime properties: you should // rather use a classic Retrieve()/FillPrepare() followed by Update(); but // it will notify the internal Cache function UpdateField(Table: TSQLRecordClass; const WhereFieldName: RawUTF8; const WhereFieldValue: array of const; const FieldName: RawUTF8; const FieldValue: array of const): boolean; overload; virtual; {$ifndef NOVARIANTS} /// update one field in a given member with a value specified as variant // - implements REST PUT collection with one field value // - any value can be set in FieldValue, but for BLOBs, you should better // use UpdateBlob() // - return true on success // - call internaly the EngineUpdateField() abstract method // - note that this method won't update the TModTime properties: you should // rather use a classic Retrieve()/FillPrepare() followed by Update(); but // it will notify the internal Cache function UpdateField(Table: TSQLRecordClass; ID: TID; const FieldName: RawUTF8; const FieldValue: variant): boolean; overload; virtual; /// update one field in one or several members, depending on a WHERE clause, // with both update and where values specified as variant // - implements REST PUT collection with one field value on a one where value // - any value can be set in FieldValue, but for BLOBs, you should better // use UpdateBlob() // - for security reasons, void WHERE clause will be rejected // - return true on success // - call internaly the EngineUpdateField() abstract method // - note that this method won't update the TModTime properties, nor the // internal table Cache: you should rather use a classic Retrieve()/FillPrepare() // followed by an Update() of the whole record function UpdateField(Table: TSQLRecordClass; const WhereFieldName: RawUTF8; const WhereFieldValue: variant; const FieldName: RawUTF8; const FieldValue: variant): boolean; overload; virtual; /// update one field in one or several members, depending on a set of IDs // - return true on success // - note that this method won't update the TModTime properties: you should // rather use a classic Retrieve()/FillPrepare() followed by Update(), but // it will be much slower, even over a BATCH; anyway, it will update the // internal Cache // - will be executed as a regular SQL statement: // $ UPDATE table SET fieldname=fieldvalue WHERE RowID IN (...) // - warning: this method will call directly EngineExecute(), and will // work just fine with SQLite3, but some other DB engines may not allow // a huge number of items within the IN(...) clause function UpdateField(Table: TSQLRecordClass; const IDs: array of Int64; const FieldName: RawUTF8; const FieldValue: variant): boolean; overload; virtual; {$endif NOVARIANTS} /// increments one integer field value // - if available, this method will use atomic value modification, e.g. // $ UPDATE table SET field=field+? function UpdateFieldIncrement(Table: TSQLRecordClass; ID: TID; const FieldName: RawUTF8; Increment: Int64=1): boolean; virtual; /// override this method to guess if this record can be updated or deleted // - this default implementation returns always true // - e.g. you can add digital signature to a record to disallow record editing // - the ErrorMsg can be set to a variable, which will contain an explicit // error message function RecordCanBeUpdated(Table: TSQLRecordClass; ID: TID; Action: TSQLEvent; ErrorMsg: PRawUTF8 = nil): boolean; virtual; /// delete a member // - implements REST DELETE collection // - return true on success // - call internaly the EngineDelete() abstract method function Delete(Table: TSQLRecordClass; ID: TID): boolean; overload; virtual; /// delete a member with a WHERE clause // - implements REST DELETE collection // - return true on success // - this default method call OneFieldValues() to retrieve all matching IDs, // then will delete each row using protected EngineDeleteWhere() virtual method function Delete(Table: TSQLRecordClass; const SQLWhere: RawUTF8): boolean; overload; virtual; /// delete a member with a WHERE clause // - implements REST DELETE collection // - return true on success // - for better server speed, the WHERE clause should use bound parameters // identified as '?' in the FormatSQLWhere statement, which is expected to // follow the order of values supplied in BoundsSQLWhere open array - use // DateToSQL/DateTimeToSQL for TDateTime, or directly any integer / double / // currency / RawUTF8 values to be bound to the request as parameters // - is a simple wrapper around: // ! Delete(Table,FormatUTF8(FormatSQLWhere,[],BoundsSQLWhere)) function Delete(Table: TSQLRecordClass; const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const): boolean; overload; /// access the internal caching parameters for a given TSQLRecord // - will always return a TSQLRestCache instance, creating one if needed // - purpose of this caching mechanism is to speed up retrieval of some // common values at either Client or Server level (like configuration settings) // - by default, this CRUD level per-ID cache is disabled // - use Cache.SetCache() and Cache.SetTimeOut() methods to set the appropriate // configuration for this particular TSQLRest instance // - only caching synchronization is about the direct RESTful/CRUD commands: // RETRIEVE, ADD, UPDATE and DELETE (that is, a complex direct SQL UPDATE or // via TSQLRecordMany pattern won't be taken into account - only exception is // TSQLRestStorage tables accessed as SQLite3 virtual table) // - this caching will be located at the TSQLRest level, that is no automated // synchronization is implemented between TSQLRestClient and TSQLRestServer - // you shall ensure that your business logic is safe, calling Cache.Flush() // overloaded methods on purpose: better no cache than unproper cache - // "premature optimization is the root of all evil" property Cache: TSQLRestCache read GetCache; /// access the internal caching parameters for a given TSQLRecord // - will return nil if no TSQLRestCache instance has been defined function CacheOrNil: TSQLRestCache; {$ifdef HASINLINE}inline;{$endif} /// get a blob field content from its record ID and supplied blob field name // - implements REST GET collection with a supplied member ID and a blob field name // - return true on success // - this method is defined as abstract, i.e. there is no default implementation: // it must be implemented 100% RestFul with a // GET ModelRoot/TableName/TableID/BlobFieldName request for example // - this method retrieve the blob data as a TSQLRawBlob string using // EngineRetrieveBlob() function RetrieveBlob(Table: TSQLRecordClass; aID: TID; const BlobFieldName: RawUTF8; out BlobData: TSQLRawBlob): boolean; overload; virtual; /// get a blob field content from its record ID and supplied blob field name // - implements REST GET collection with a supplied member ID and field name // - return true on success // - this method will create a TStream instance (which must be freed by the // caller after use) and fill it with the blob data function RetrieveBlob(Table: TSQLRecordClass; aID: TID; const BlobFieldName: RawUTF8; out BlobStream: THeapMemoryStream): boolean; overload; /// update a blob field from its record ID and supplied blob field name // - implements REST PUT collection with a supplied member ID and field name // - return true on success // - call internaly the EngineUpdateBlob() abstract method // - this method expect the Blob data to be supplied as TSQLRawBlob, using // EngineUpdateBlob() function UpdateBlob(Table: TSQLRecordClass; aID: TID; const BlobFieldName: RawUTF8; const BlobData: TSQLRawBlob): boolean; overload; virtual; /// update a blob field from its record ID and blob field name // - implements REST PUT collection with a supplied member ID and field name // - return true on success // - call internaly the EngineUpdateBlob() abstract method // - this method expect the Blob data to be supplied as a TStream: it will // send the whole stream content (from its beginning position upto its // current size) to the database engine function UpdateBlob(Table: TSQLRecordClass; aID: TID; const BlobFieldName: RawUTF8; BlobData: TStream): boolean; overload; /// update a blob field from its record ID and blob field name // - implements REST PUT collection with a supplied member ID and field name // - return true on success // - call internaly the EngineUpdateBlob() abstract method // - this method expect the Blob data to be supplied as direct memory pointer // and size function UpdateBlob(Table: TSQLRecordClass; aID: TID; const BlobFieldName: RawUTF8; BlobData: pointer; BlobSize: integer): boolean; overload; /// update all BLOB fields of the supplied Value // - call several REST PUT collection (one for each BLOB) for the member // - uses the UpdateBlob() method to send the BLOB properties content to the Server // - called internaly by Add and Update methods when ForceBlobTransfert / // ForceBlobTransfertTable[] is set // - you can use this method by hand, to avoid several calls to UpdateBlob() // - returns TRUE on success (or if there is no BLOB field) // - returns FALSE on error (e.g. if Value is invalid or with db/transmission) function UpdateBlobFields(Value: TSQLRecord): boolean; virtual; /// get all BLOB fields of the supplied value from the remote server // - call several REST GET collection (one for each BLOB) for the member // - call internaly e.g. by TSQLRestClient.Retrieve method when // ForceBlobTransfert / ForceBlobTransfertTable[] is set function RetrieveBlobFields(Value: TSQLRecord): boolean; virtual; /// begin a transaction // - implements REST BEGIN collection // - may be used to speed up CRUD statements like Add/Update/Delete // - in the current implementation, nested transactions are not allowed // - must be ended with Commit on success // - must be aborted with Rollback if any SQL statement failed // - default implementation just handle the protected fTransactionActiveSession flag // - return true if no transaction is active, false otherwise // - in aClient-Server environment with multiple Clients connected at the // same time, you should better use BATCH process, specifying a positive // AutomaticTransactionPerRow parameter to BatchStart() // - in a multi-threaded or Client-Server with multiple concurrent Client // connections, you may check the returned value, as such: // !if Client.TransactionBegin(TSQLRecordPeopleObject) then // !try // ! //.... modify the database content, raise exceptions on error // ! Client.Commit; // !except // ! Client.RollBack; // in case of error // !end; // or use the TransactionBeginRetry() method // - the supplied SessionID will allow multi-user transaction safety on the // Server-Side: all database modification from another session will wait // for the global transaction to be finished; on Client-side, the SessionID // is just ignored (TSQLRestClient will override this method with a default // SessionID=CONST_AUTHENTICATION_NOT_USED=1 parameter) // - if you have an external database engine which expect transactions to // take place in the same thread, ensure TSQLRestServer force execution of // this method when accessed from RESTful clients in the same thread, e.g.: // ! AcquireExecutionMode[execORMWrite] := amBackgroundThread; // ! AcquireWriteMode := amBackgroundThread; // same as previous function TransactionBegin(aTable: TSQLRecordClass; SessionID: cardinal): boolean; virtual; /// check current transaction status // - returns the session ID if a transaction is active // - returns 0 if no transaction is active function TransactionActiveSession: cardinal; /// end a transaction // - implements REST END collection // - write all pending SQL statements to the disk // - default implementation just reset the protected fTransactionActiveSession flag // - the supplied SessionID will allow multi-user transaction safety on the // Server-Side: all database modification from another session will wait // for the global transaction to be finished; on Client-side, the SessionID // is just ignored (TSQLRestClient will override this method with a default // SessionID=CONST_AUTHENTICATION_NOT_USED=1 parameter) // - if you have an external database engine which expect transactions to // take place in the same thread, ensure TSQLRestServer force execution of // this method when accessed from RESTful clients in the same thread, e.g.: // ! AcquireExecutionMode[execORMWrite] := amBackgroundThread; // ! AcquireWriteMode := amBackgroundThread; // same as previous // - by default, any exception will be catch and ignored, unless RaiseException // is set to TRUE so that the caller will be able to handle it procedure Commit(SessionID: cardinal; RaiseException: boolean=false); virtual; /// abort a transaction // - implements REST ABORT collection // - restore the previous state of the database, before the call to TransactionBegin // - default implementation just reset the protected fTransactionActiveSession flag // - the supplied SessionID will allow multi-user transaction safety on the // Server-Side: all database modification from another session will wait // for the global transaction to be finished; on Client-side, the SessionID // is just ignored (TSQLRestClient will override this method with a default // SessionID=CONST_AUTHENTICATION_NOT_USED=1 parameter) // - if you have an external database engine which expect transactions to // take place in the same thread, ensure TSQLRestServer force execution of // this method when accessed from RESTful clients in the same thread, e.g.: // ! AcquireExecutionMode[execORMWrite] := amBackgroundThread; // ! AcquireWriteMode := amBackgroundThread; // same as previous procedure RollBack(SessionID: cardinal); virtual; /// execute a BATCH sequence prepared in a TSQLRestBatch instance // - implements the "Unit Of Work" pattern, i.e. safe transactional process // even on multi-thread environments // - send all pending Add/Update/Delete statements to the DB or remote server // - will return the URI Status value, i.e. 200/HTTP_SUCCESS OK on success // - a dynamic array of integers will be created in Results, // containing all ROWDID created for each BatchAdd call, 200 (=HTTP_SUCCESS) // for all successfull BatchUpdate/BatchDelete, or 0 on error // - any error during server-side process MUST be checked against Results[] // (the main URI Status is 200 if about communication success, and won't // imply that all statements in the BATCH sequence were successfull), // or boRollbackOnError should be set in TSQLRestBatchOptions // - note that the caller shall still free the supplied Batch instance function BatchSend(Batch: TSQLRestBatch; var Results: TIDDynArray): integer; overload; virtual; /// execute a BATCH sequence prepared in a TSQLRestBatch instance // - just a wrapper around the overloaded BatchSend() method without the // Results: TIDDynArray parameter function BatchSend(Batch: TSQLRestBatch): integer; overload; {$ifdef ISDELPHI2010} // Delphi 2009/2010 generics support is buggy :( /// get an instance of one interface-based service // - may return nil if this service interface is not available function Service: T; /// get a list of members from a SQL statement // - implements REST GET collection // - aCustomFieldsCSV can be the CSV list of field names to be retrieved // - if aCustomFieldsCSV is '', will get all simple fields, excluding BLOBs // - if aCustomFieldsCSV is '*', will get ALL fields, including ID and BLOBs // - return a TObjectList on success (possibly with Count=0) - caller is // responsible of freeing the instance // - return nil on error // - you can write for instance: // !var List: TObjectList; // ! R: TSQLRecordTest; // ! ... // ! List := Client.RetrieveList('ID,Test'); // ! if List<>nil then // ! try // ! for R in List do // ! writeln(R.ID,'=',R.Test); // ! finally // ! List.Free; // ! end; function RetrieveList(const aCustomFieldsCSV: RawUTF8=''): TObjectList; overload; {$ifdef HASINLINE}inline;{$endif} /// get a list of members from a SQL statement // - implements REST GET collection with a WHERE clause // - for better server speed, the WHERE clause should use bound parameters // identified as '?' in the FormatSQLWhere statement, which is expected to // follow the order of values supplied in BoundsSQLWhere open array - use // DateToSQL()/DateTimeToSQL() for TDateTime, or directly any integer, // double, currency, RawUTF8 values to be bound to the request as parameters // - aCustomFieldsCSV can be the CSV list of field names to be retrieved // - if aCustomFieldsCSV is '', will get all simple fields, excluding BLOBs // - if aCustomFieldsCSV is '*', will get ALL fields, including ID and BLOBs // - return a TObjectList on success (possibly with Count=0) - caller is // responsible of freeing the instance // - return nil on error function RetrieveList(const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const; const aCustomFieldsCSV: RawUTF8=''): TObjectList; overload; {$endif ISDELPHIXE} /// you can call this method in TThread.Execute to ensure that // the thread will be taken into account during process // - this abstract method won't do anything, but TSQLRestServer's will procedure BeginCurrentThread(Sender: TThread); virtual; /// you can call this method just before a thread is finished to ensure // e.g. that the associated external DB connection will be released // - this abstract method will call fLogClass.Add.NotifyThreadEnded // but TSQLRestServer.EndCurrentThread will do the main process procedure EndCurrentThread(Sender: TThread); virtual; /// enter the Mutex associated with the write operations of this instance // - just a wrapper around fAcquireExecution[execORMWrite].Safe.Lock procedure WriteLock; /// leave the Mutex associated with the write operations of this instance // - just a wrapper around fAcquireExecution[execORMWrite].Safe.UnLock procedure WriteUnLock; /// allows to safely execute a processing method in a background thread // - returns a TSynBackgroundThreadMethod instance, ready to execute any // background task via its RunAndWait() method // - will properly call BeginCurrentThread/EndCurrentThread methods // - you should supply some runtime information to name the thread, for // proper debugging function NewBackgroundThreadMethod(const Format: RawUTF8; const Args: array of const): TSynBackgroundThreadMethod; /// allows to safely execute a process at a given pace // - returns a TSynBackgroundThreadProcess instance, ready to execute the // supplied aOnProcess event in a loop, as aOnProcessMS periodic task // - will properly call BeginCurrentThread/EndCurrentThread methods // - you should supply some runtime information to name the thread, for // proper debugging function NewBackgroundThreadProcess(aOnProcess: TOnSynBackgroundThreadProcess; aOnProcessMS: cardinal; const Format: RawUTF8; const Args: array of const; aStats: TSynMonitorClass=nil): TSynBackgroundThreadProcess; /// allows to safely execute a process in parallel // - returns a TSynParallelProcess instance, ready to execute any task // in parrallel in a thread-pool given by ThreadCount // - will properly call BeginCurrentThread/EndCurrentThread methods // - you should supply some runtime information to name the thread, for // proper debugging function NewParallelProcess(ThreadCount: integer; const Format: RawUTF8; const Args: array of const): TSynParallelProcess; /// define a task running on a periodic number of seconds in a background thread // - could be used to run background maintenance or monitoring tasks on // this TSQLRest instance, at a low pace (typically every few minutes) // - will instantiate and run a shared TSynBackgroundTimer instance for this // TSQLRest, so all tasks will share the very same thread // - you can run BackgroundTimer.EnQueue or ExecuteNow methods to implement // a FIFO queue, or force immediate execution of the process // - will call BeginCurrentThread/EndCurrentThread as expected e.g. by logs function TimerEnable(aOnProcess: TOnSynBackgroundTimerProcess; aOnProcessSecs: cardinal): TSynBackgroundTimer; /// undefine a task running on a periodic number of seconds // - should have been registered by a previous call to TimerEnable() method // - returns true on success, false if the supplied task was not registered function TimerDisable(aOnProcess: TOnSynBackgroundTimerProcess): boolean; /// define asynchronous execution of interface methods in a background thread // - this class allows to implements any interface via a fake class, which will // redirect all methods calls into calls of another interface, but as a FIFO // in a background thread, shared with TimerEnable/TimerDisable process // - it is an elegant resolution to the most difficult implementation // problem of SOA callbacks, which is to avoid race condition on reentrance, // e.g. if a callback is run from a thread, and then the callback code try // to execute something in the context of the initial thread, protected // by a critical section (mutex) // - is a wrapper around BackgroundTimer.AsynchRedirect() procedure AsynchRedirect(const aGUID: TGUID; const aDestinationInterface: IInvokable; out aCallbackInterface; const aOnResult: TOnAsynchRedirectResult=nil); overload; /// define asynchronous execution of interface methods in a background thread // - this class allows to implements any interface via a fake class, which will // redirect all methods calls into calls of another interface, but as a FIFO // in a background thread, shared with TimerEnable/TimerDisable process // - it is an elegant resolution to the most difficult implementation // problem of SOA callbacks, which is to avoid race condition on reentrance, // e.g. if a callback is run from a thread, and then the callback code try // to execute something in the context of the initial thread, protected // by a critical section (mutex) // - is a wrapper around BackgroundTimer.AsynchRedirect() procedure AsynchRedirect(const aGUID: TGUID; const aDestinationInstance: TInterfacedObject; out aCallbackInterface; const aOnResult: TOnAsynchRedirectResult=nil); overload; /// prepare an asynchronous ORM BATCH process, executed in a background thread // - will initialize a TSQLRestBatch and call TimerEnable to initialize the // background thread, following the given processing period (in seconds), // or the TSQLRestBatch.Count threshold to call BatchSend // - actual REST/CRUD commands will take place via AsynchBatchAdd, // AsynchBatchUpdate and AsynchBatchDelete methods // - only a single AsynchBatch() call per Table is allowed at a time, unless // AsynchBatchStop method is used to flush the current asynchronous BATCH // - using a BATCH in a dedicated thread will allow very fast bacgkround // asynchronous process of ORM methods, sufficient for most use cases // - is a wrapper around BackgroundTimer.AsynchBatchStart() function AsynchBatchStart(Table: TSQLRecordClass; SendSeconds: integer; PendingRowThreshold: integer=500; AutomaticTransactionPerRow: integer=1000; Options: TSQLRestBatchOptions=[boExtendedJSON]): boolean; /// finalize asynchronous ORM BATCH process, executed in a background thread // - should have been preceded by a call to AsynchBatch(), or returns false // - Table=nil will release all existing batch instances // - is a wrapper around BackgroundTimer.AsynchBatchStop() function AsynchBatchStop(Table: TSQLRecordClass): boolean; /// create a new ORM member in a BATCH to be written in a background thread // - should have been preceded by a call to AsynchBatchStart(), or returns -1 // - is a wrapper around BackgroundTimer.AsynchBatchAdd(), // so will return the index in the BATCH rows, not the created TID // - this method is thread-safe function AsynchBatchAdd(Value: TSQLRecord; SendData: boolean; ForceID: boolean=false; const CustomFields: TSQLFieldBits=[]; DoNotAutoComputeFields: boolean=false): integer; /// append some JSON content in a BATCH to be written in a background thread // - could be used to emulate AsynchBatchAdd() with an already pre-computed // JSON object // - is a wrapper around BackgroundTimer.AsynchBatchRawAdd(), // so will return the index in the BATCH rows, not the created TID // - this method is thread-safe function AsynchBatchRawAdd(Table: TSQLRecordClass; const SentData: RawUTF8): integer; /// append some JSON content in a BATCH to be writen in a background thread // - could be used to emulate AsynchBatchAdd() with an already pre-computed // JSON object, as stored in a TTextWriter instance // - is a wrapper around BackgroundTimer.AsynchBatchRawAppend() // - this method is thread-safe procedure AsynchBatchRawAppend(Table: TSQLRecordClass; SentData: TTextWriter); /// update an ORM member in a BATCH to be written in a background thread // - should have been preceded by a call to AsynchBatchStart(), or returns -1 // - is a wrapper around BackgroundTimer.AsynchBatchUpdate() // - this method is thread-safe function AsynchBatchUpdate(Value: TSQLRecord; const CustomFields: TSQLFieldBits=[]; DoNotAutoComputeFields: boolean=false): integer; /// delete an ORM member in a BATCH to be written in a background thread // - should have been preceded by a call to AsynchBatchStart(), or returns -1 // - is a wrapper around the TSQLRestBatch.Delete() sent in the Timer thread // - this method is thread-safe function AsynchBatchDelete(Table: TSQLRecordClass; ID: TID): integer; /// allows background garbage collection of specified RawUTF8 interning // - will run Interning.Clean(2) every 5 minutes by default // - set InterningMaxRefCount=0 to disable process of the Interning instance // - note that InterningMaxRefCount and PeriodMinutes parameters (if not 0), // are common for all TRawUTF8Interning instances (the latest value wins) // - you may e.g. run the following to clean up TDocVariant interned RawUTF8: // ! aRest.AsynchInterning(DocVariantType.InternNames); // ! aRest.AsynchInterning(DocVariantType.InternValues); procedure AsynchInterning(Interning: TRawUTF8Interning; InterningMaxRefCount: integer=2; PeriodMinutes: integer=5); /// define redirection of interface methods calls in one or several instances // - this class allows to implements any interface via a fake class, which // will redirect all methods calls to one or several other interfaces // - returned aCallbackInterface will redirect all its methods (identified // by aGUID) into an internal list handled by IMultiCallbackRedirect.Redirect // - typical use is thefore: // ! fSharedCallback: IMyService; // ! fSharedCallbacks: IMultiCallbackRedirect; // ! ... // ! if fSharedCallbacks=nil then begin // ! fSharedCallbacks := aRest.MultiRedirect(IMyService,fSharedCallback); // ! aServices.SubscribeForEvents(fSharedCallback); // ! end; // ! fSharedCallbacks.Redirect(TMyCallback.Create,[]); // ! // now each time fSharedCallback receive one event, all callbacks // ! // previously registered via Redirect() will receive it // ! ... // ! fSharedCallbacks := nil; // will stop redirection // ! // and unregister callbacks, if needed function MultiRedirect(const aGUID: TGUID; out aCallbackInterface; aCallBackUnRegisterNeeded: boolean=true): IMultiCallbackRedirect; overload; /// will gather CPU and RAM information in a background thread // - you can specify the update frequency, in seconds // - access to the information via the returned instance, which maps // the TSystemUse.Current class function // - do nothing if global TSystemUse.Current was already assigned function SystemUseTrack(periodSec: integer=10): TSystemUse; /// initialize some custom AES encryption and/or digital signature, with // optional compression // - will intercept the calls by setting OnDecryptBody/OnEncryptBody events // - will own the supplied aes instance or won't encrypt the content if nil // - will digitally sign the content body and uri with the supplied // TSynSigner, or won't compute any digital signature if sign=nil // - if both aes and sign are nil, then call interception is disabled // - you can optionally specify a compression algorithm (like AlgoSynLZ or // AlgoDeflate/AlgoDeflateFast) to be applied before encryption // - any URI starting with uriignore characters won't be encrypted: it could // be used to define a method-based service for handshake and aes/sign // mutual agreement // - TSQLRestServer will require incoming requests to be of the corresponding // [aesclass][signalgo]/[originaltype] HTTP content-type e.g. // 'aesofb256sha256/application/json' - any plain request will be rejected // - note that it will only encrypt and sign the HTTP requests bodies, so URI // or plain GET won't be checked - as such, it is not a replacement of // TSQLRestServerAuthentication nor TWebSocketProtocolBinary encryption, // but a cheap alternative to HTTPS, when you need to protect HTTP flow // from MiM attacks (e.g. in a IoT context) with simple and proven algorithms procedure SetCustomEncryption(aes: TAESAbstract; sign: PSynSigner; comp: TAlgoCompress; const uriignore: RawUTF8=''); /// how this class execute its internal commands // - by default, TSQLRestServer.URI() will lock for Write ORM according to // AcquireWriteMode (i.e. AcquireExecutionMode[execORMWrite]=amLocked) and // other operations won't be protected (for better scaling) // - you can tune this behavior by setting this property to the expected // execution mode, e.g. execute all method-based services in a dedicated // thread via // ! aServer.AcquireExecutionMode[execSOAByMethod] := amBackgroundThread; // - if you use external DB and a custom ConnectionTimeOutMinutes value, // both read and write access should be locked, so you should set: // ! aServer.AcquireExecutionMode[execORMGet] := am***; // ! aServer.AcquireExecutionMode[execORMWrite] := am***; // here, safe blocking am*** modes are any mode but amUnlocked, i.e. either // amLocked, amBackgroundThread, amBackgroundORMSharedThread or amMainThread property AcquireExecutionMode[Cmd: TSQLRestServerURIContextCommand]: TSQLRestServerAcquireMode read GetAcquireExecutionMode write SetAcquireExecutionMode; /// the time (in mili seconds) to try locking internal commands of this class // - this value is used only for AcquireExecutionMode[*]=amLocked // - by default, TSQLRestServer.URI() will lock for Write ORM according to // AcquireWriteTimeOut (i.e. AcquireExecutionLockedTimeOut[execORMWrite]) // and other operations won't be locked nor have any time out set property AcquireExecutionLockedTimeOut[Cmd: TSQLRestServerURIContextCommand]: cardinal read GetAcquireExecutionLockedTimeOut write SetAcquireExecutionLockedTimeOut; /// how this class will handle write access to the database // - is a common wrapper to AcquireExecutionMode[execORMWrite] property // - default amLocked mode will wait up to AcquireWriteTimeOut mili seconds // to have a single access to the server write ORM methods // - amBackgroundThread will execute the write methods in a queue, in a // dedicated unique thread (which can be convenient, especially for // external database transaction process) // - amBackgroundORMSharedThread will execute all ORM methods in a queue, in // a dedicated unique thread, shared for both execORMWrite and execORMGet, // but still dedicated for execSOAByMethod and execSOAByInterface // - a slower alternative to amBackgroundThread may be amMainThread // - you can set amUnlocked for a concurrent write access, but be aware // that it may lead into multi-thread race condition issues, depending on // the database engine used property AcquireWriteMode: TSQLRestServerAcquireMode index execORMWrite read GetAcquireExecutionMode write SetAcquireExecutionMode; /// the time (in mili seconds) which the class will wait for acquiring a // write acccess to the database, when AcquireWriteMode is amLocked // - is a common wrapper to AcquireExecutionLockedTimeOut[execORMWrite] // - in order to handle safe transactions and multi-thread safe writing, the // server will identify transactions using the client Session ID: this // property will set the time out wait period // - default value is 5000, i.e. TSQLRestServer.URI will wait up to 5 seconds // in order to acquire the right to write on the database before returning // a "408 Request Time-out" status error property AcquireWriteTimeOut: cardinal index execORMWrite read GetAcquireExecutionLockedTimeOut write SetAcquireExecutionLockedTimeOut; /// used e.g. by IAdministratedDaemon to implement "pseudo-SQL" commands // - this default implementation will handle #time #model #rest commands procedure AdministrationExecute(const DatabaseName,SQL: RawUTF8; var result: TServiceCustomAnswer); virtual; /// access to the interface-based services list // - may be nil if no service interface has been registered yet: so be // aware that the following line may trigger an access violation if // no ICalculator is defined on server side: // ! if fServer.Services['Calculator'].Get(Calc)) then // ! ... // - safer typical use, following the DI/IoC pattern, and which will not // trigger any access violation if Services=nil, could be: // ! if fServer.Services.Resolve(ICalculator,Calc) then // ! ... property Services: TServiceContainer read fServices; /// access or initialize the internal IoC resolver, used for interface-based // remote services, and more generaly any Services.Resolve() call // - create and initialize the internal TServiceContainer if no service // interface has been registered yet // - may be used to inject some dependencies, which are not interface-based // remote services, but internal IoC, without the ServiceRegister() // or ServiceDefine() methods - e.g. // ! aRest.ServiceContainer.InjectResolver([TInfraRepoUserFactory.Create(aRest)],true); // - overriden methods will return TServiceContainerClient or // TServiceContainerServer instances, on TSQLRestClient or TSQLRestServer function ServiceContainer: TServiceContainer; virtual; abstract; /// the routing classs of the service remote request // - by default, will use TSQLRestRoutingREST, i.e. an URI-based // layout which is secure (since will use our RESTful authentication scheme), // and also very fast // - but TSQLRestRoutingJSON_RPC can e.g. be set (on BOTH client and // server sides), if the client will rather use JSON/RPC alternative pattern // - NEVER set the abstract TSQLRestServerURIContext class on this property property ServicesRouting: TSQLRestServerURIContextClass read fRoutingClass write SetRoutingClass; /// low-level background timer thread associated with this TSQLRest // - contains nil if TimerEnable/AsynchInvoke was never executed // - you may instantiate your own TSQLRestBackgroundTimer instances, if // more than one working thread is needed property BackgroundTimer: TSQLRestBackgroundTimer read fBackgroundTimer; /// the Database Model associated with this REST Client or Server property Model: TSQLModel read fModel; /// event called before TSQLRestServer.URI or after TSQLRestClientURI.URI // - defined e.g. by SetCustomEncryption property OnDecryptBody: TNotifyRestBody read fOnDecryptBody write fOnDecryptBody; /// event called after TSQLRestServer.URI or before TSQLRestClientURI.URI // - defined e.g. by SetCustomEncryption property OnEncryptBody: TNotifyRestBody read fOnEncryptBody write fOnEncryptBody; published /// the current UTC Date and Time, as retrieved from the server // - this property will return the timestamp as TTimeLog / Int64 // after correction from the Server returned time-stamp (if any) // - is used e.g. by TSQLRecord.ComputeFieldsBeforeWrite to update TModTime // and TCreateTime published fields // - default implementation will return the executable UTC time, i.e. NowUTC // so that any GUI code should convert this UTC value into local time // - on TSQLRestServer, if you use an external database, the TSQLDBConnection // ServerTimestamp value will be set to this property // - you can use this value in a WHERE clause for a query, as such: // ! aRec.CreateAndFillPrepare(Client,'Datum<=?',[TimeLogToSQL(Client.ServerTimestamp)]); // - or you could use ServerTimestamp everywhere in your code, when you need // a reference time base property ServerTimestamp: TTimeLog read GetServerTimestamp write SetServerTimestamp; {$ifdef WITHLOG} /// the logging class used for this instance // - is set by default to SQLite3Log, but could be set to a custom class property LogClass: TSynLogClass read GetLogClass write SetLogClass; {$endif} public /// the custom queries parameters for User Interface Query action QueryCustom: array of TSQLQueryCustom; /// evaluate a basic operation for implementing User Interface Query action // - expect both Value and Reference to be UTF-8 encoded (as in TSQLTable // or TSQLTableToGrid) // - aID parameter is ignored in this function implementation (expect only // this parameter to be not equal to 0) // - is TSQLQueryEvent prototype compatible // - for qoContains and qoBeginWith, the Reference is expected to be // already uppercase // - for qoSoundsLike* operators, Reference is not a PUTF8Char, but a // typecase of a prepared TSynSoundEx object instance (i.e. pointer(@SoundEx)) class function QueryIsTrue(aTable: TSQLRecordClass; aID: TID; FieldType: TSQLFieldType; Value: PUTF8Char; Operator: integer; Reference: PUTF8Char): boolean; /// add a custom query // - one event handler with an enumeration type containing all available // query names // - and associated operators procedure QueryAddCustom(aTypeInfo: pointer; aEvent: TSQLQueryEvent; const aOperators: TSQLQueryOperators); end; {$M+} /// a simple TThread for doing some process within the context of a REST instance // - also define a Start method for compatibility with older versions of Delphi // - inherited classes should override InternalExecute abstract method TSQLRestThread = class(TThread) protected fRest: TSQLRest; fOwnRest: boolean; fLog: TSynLog; fSafe: TSynLocker; fEvent: TEvent; fExecuting: boolean; /// allows customization in overriden Create (before Execute) fThreadName: RawUTF8; /// will call BeginCurrentThread/EndCurrentThread and catch exceptions procedure Execute; override; /// you should override this method with the proper process procedure InternalExecute; virtual; abstract; public /// initialize the thread // - if aOwnRest is TRUE, the supplied REST instance will be // owned by this thread constructor Create(aRest: TSQLRest; aOwnRest, aCreateSuspended: boolean); {$ifndef HASTTHREADSTART} /// method to be called to start the thread // - Resume is deprecated in the newest RTL, since some OS - e.g. Linux - // do not implement this pause/resume feature; we define here this method // for older versions of Delphi procedure Start; {$endif} {$ifdef HASTTHREADTERMINATESET} /// properly terminate the thread // - called by TThread.Terminate procedure TerminatedSet; override; {$else} /// properly terminate the thread // - called by reintroduced Terminate procedure TerminatedSet; virtual; /// reintroduced to call TeminatedSet procedure Terminate; reintroduce; {$endif} /// wait for Execute to be ended (i.e. fExecuting=false) procedure WaitForNotExecuting(maxMS: integer=500); /// finalize the thread // - and the associated REST instance if OwnRest is TRUE destructor Destroy; override; /// safe version of Sleep() which won't break the thread process // - returns TRUE if the thread was Terminated // - returns FALSE if successfully waited up to MS milliseconds function SleepOrTerminated(MS: integer): boolean; /// read-only access to the associated REST instance property Rest: TSQLRest read FRest; /// TRUE if the associated REST instance will be owned by this thread property OwnRest: boolean read fOwnRest; /// a critical section is associated to this thread // - could be used to protect shared resources within the internal process property Safe: TSynLocker read fSafe; /// read-only access to the TSynLog instance of the associated REST instance property Log: TSynLog read fLog; /// a event associated to this thread property Event: TEvent read fEvent; /// publishes the thread running state property Terminated; /// publishes the thread executing state (set when Execute leaves) property Executing: boolean read fExecuting; end; {$M-} /// event signature used to notify a client callback // - implemented e.g. by TSQLHttpServer.NotifyCallback TSQLRestServerNotifyCallback = function(aSender: TSQLRestServer; const aInterfaceDotMethodName,aParams: RawUTF8; aConnectionID: Int64; aFakeCallID: integer; aResult, aErrorMsg: PRawUTF8): boolean of object; /// event signature used by TSQLRestServer.OnServiceCreateInstance // - as called by TServiceFactoryServer.CreateInstance // - the actual Instance class can be quickly retrieved from // Sender.ImplementationClass TOnServiceCreateInstance = procedure( Sender: TServiceFactoryServer; Instance: TInterfacedObject) of object; {$ifdef MSWINDOWS} TSQLRestServerNamedPipeResponse = class; /// Server thread accepting connections from named pipes TSQLRestServerNamedPipe = class(TSQLRestThread) private protected fServer: TSQLRestServer; fChild: array of TSQLRestServerNamedPipeResponse; fChildRunning: integer; fPipeName: TFileName; procedure InternalExecute; override; public /// register a new response thread procedure AddChild(new: TSQLRestServerNamedPipeResponse); /// unregister a new response thread procedure RemoveChild(new: TSQLRestServerNamedPipeResponse); /// create the server thread constructor Create(aServer: TSQLRestServer; const PipeName: TFileName); reintroduce; /// release all associated memory, and wait for all // TSQLRestServerNamedPipeResponse children to be terminated destructor Destroy; override; /// the associated pipe name property PipeName: TFileName read fPipeName; end; /// Server child thread dealing with a connection through a named pipe TSQLRestServerNamedPipeResponse = class(TSQLRestThread) private protected fServer: TSQLRestServer; fPipe: cardinal; fMasterThread: TSQLRestServerNamedPipe; procedure InternalExecute; override; public /// create the child connection thread constructor Create(aServer: TSQLRestServer; aMasterThread: TSQLRestServerNamedPipe; aPipe: cardinal); reintroduce; /// release all associated memory, and decrement fMasterThread.fChildRunning destructor Destroy; override; end; {$ifdef FPC} TWMCopyData = record Msg: UINT; From: WPARAM; CopyDataStruct: LPARAM; Result: LRESULT; end; {$endif} {$endif} /// function prototype for remotely calling a TSQLRestServer // - use PUTF8Char instead of string: no need to share a memory manager, and can // be used with any language (even C or .NET, thanks to the cdecl calling convention) // - you can specify some POST/PUT data in SendData (leave as nil otherwise) // - returns in result.Lo the HTTP STATUS integer error or success code // - returns in result.Hi the server database internal status // - on success, allocate and store the resulting JSON body into Resp^, headers in Head^ // - use a GlobalFree() function to release memory for Resp and Head responses TURIMapRequest = function(url, method, SendData: PUTF8Char; Resp, Head: PPUTF8Char): Int64Rec; cdecl; TSQLRestServerAuthentication = class; /// structure used to specify custom request paging parameters for TSQLRestServer // - default values are the one used for YUI component paging (i.e. // PAGINGPARAMETERS_YAHOO constant, as set by TSQLRestServer.Create) // - warning: using paging can be VERY expensive on Server side, especially // when used with external databases (since all data is retrieved before // paging, when SQLite3 works in virtual mode) TSQLRestServerURIPagingParameters = record /// parameter name used to specify the request sort order // - default value is 'SORT=' Sort: RawUTF8; /// parameter name used to specify the request sort direction // - default value is 'DIR=' Dir: RawUTF8; /// parameter name used to specify the request starting offset // - default value is 'STARTINDEX=' StartIndex: RawUTF8; /// parameter name used to specify the request the page size (LIMIT clause) // - default value is 'RESULTS=' Results: RawUTF8; /// parameter name used to specify the request field names // - default value is 'SELECT=' Select: RawUTF8; /// parameter name used to specify the request WHERE clause // - default value is 'WHERE=' Where: RawUTF8; /// returned JSON field value of optional total row counts // - default value is nil, i.e. no total row counts field // - computing total row counts can be very expensive, depending on the // database back-end used (especially for external databases) // - can be set e.g. to ',"totalRows":%' value (note that the initial "," is // expected by the produced JSON content, and % will be set with the value) SendTotalRowsCountFmt: RawUTF8; end; /// used to define how to trigger Events on record update // - see TSQLRestServer.OnUpdateEvent property and InternalUpdateEvent() method // - returns true on success, false if an error occured (but action must continue) // - to be used only server-side, not to synchronize some clients: the framework // is designed around a stateless RESTful architecture (like HTTP/1.1), in which // clients ask the server for refresh (see TSQLRestClientURI.UpdateFromServer) TNotifySQLEvent = function(Sender: TSQLRestServer; Event: TSQLEvent; aTable: TSQLRecordClass; const aID: TID; const aSentData: RawUTF8): boolean of object; /// used to define how to trigger Events on record field update // - see TSQLRestServer.OnBlobUpdateEvent property and InternalUpdateEvent() method // - returns true on success, false if an error occured (but action must continue) // - to be used only server-side, not to synchronize some clients: the framework // is designed around a stateless RESTful architecture (like HTTP/1.1), in which // clients ask the server for refresh (see TSQLRestClientURI.UpdateFromServer) TNotifyFieldSQLEvent = function(Sender: TSQLRestServer; Event: TSQLEvent; aTable: TSQLRecordClass; const aID: TID; const aAffectedFields: TSQLFieldBits): boolean of object; /// session-related callbacks triggered by TSQLRestServer // - for OnSessionCreate, returning TRUE will abort the session creation - // and you can set Ctxt.Call^.OutStatus to a corresponding error code TNotifySQLSession = function(Sender: TSQLRestServer; Session: TAuthSession; Ctxt: TSQLRestServerURIContext): boolean of object; /// callback allowing to customize the retrieval of an authenticated user // - as defined in TSQLRestServer.OnAuthenticationUserRetrieve // - and executed by TSQLRestServerAuthentication.GetUser // - on call, either aUserID will be <> 0, or aUserName is to be used // - if the function returns nil, default Server.SQLAuthUserClass.Create() // methods won't be called, and the user will be reported as not found TOnAuthenticationUserRetrieve = function(Sender: TSQLRestServerAuthentication; Ctxt: TSQLRestServerURIContext; aUserID: TID; const aUserName: RawUTF8): TSQLAuthUser of object; /// callback raised in case of authentication failure // - as used by TSQLRestServerURIContext.AuthenticationFailed event TNotifyAuthenticationFailed = procedure(Sender: TSQLRestServer; Reason: TNotifyAuthenticationFailedReason; Session: TAuthSession; Ctxt: TSQLRestServerURIContext) of object; /// callback raised before TSQLRestServer.URI execution // - should return TRUE to execute the command, FALSE to cancel it TNotifyBeforeURI = function(Ctxt: TSQLRestServerURIContext): boolean of object; /// callback raised after TSQLRestServer.URI execution TNotifyAfterURI = procedure(Ctxt: TSQLRestServerURIContext) of object; /// callback raised if TSQLRestServer.URI execution failed // - should return TRUE to execute Ctxt.Error(E,...), FALSE if returned // content has already been set as expected by the client TNotifyErrorURI = function(Ctxt: TSQLRestServerURIContext; E: Exception): boolean of object; {$ifndef NOVARIANTS} /// callback allowing to customize the information returned by root/timestamp/info TOnInternalInfo = procedure(Sender: TSQLRestServer; var info: TDocVariantData) of object; {$endif} TSQLRestStorageInMemory = class; TSQLVirtualTableModule = class; /// class-reference type (metaclass) of our abstract table storage // - may be e.g. TSQLRestStorageInMemory, TSQLRestStorageInMemoryExternal, // TSQLRestStorageExternal or TSQLRestStorageMongoDB TSQLRestStorageClass = class of TSQLRestStorage; /// class-reference type (metaclass) of our TObjectList memory-stored table storage // - may be TSQLRestStorageInMemory or TSQLRestStorageInMemoryExternal TSQLRestStorageInMemoryClass = class of TSQLRestStorageInMemory; /// table containing the available user access rights for authentication // - this class should be added to the TSQLModel, together with TSQLAuthUser, // to allow authentication support // - you can inherit from it to add your custom properties to each user info: // TSQLModel will search for any class inheriting from TSQLAuthGroup to // manage per-group authorization data // - by default, it won't be accessible remotely by anyone TSQLAuthGroup = class(TSQLRecord) private fIdent: RawUTF8; fSessionTimeOut: integer; fAccessRights: RawUTF8; function GetSQLAccessRights: TSQLAccessRights; procedure SetSQLAccessRights(const Value: TSQLAccessRights); public /// called when the associated table is created in the database // - on a new database, if TSQLAuthUser and TSQLAuthGroup tables are defined // in the associated TSQLModel, it this will add 'Admin', 'Supervisor', // and 'User' rows in the AuthUser table (with 'synopse' as default password), // and associated 'Admin', 'Supervisor', 'User' and 'Guest' groups, with the // following access rights to the AuthGroup table: // $ POSTSQL SELECTSQL Service AuthR AuthW TablesR TablesW // $ Admin Yes Yes Yes Yes Yes Yes Yes // $ Supervisor No Yes Yes Yes No Yes Yes // $ User No No Yes No No Yes Yes // $ Guest No No No No No Yes No // - 'Admin' will be the only able to execute remote not SELECT SQL statements // for POST commands (reSQL flag in TSQLAccessRights.AllowRemoteExecute) and // modify the Auth tables (i.e. AuthUser and AuthGroup) // - 'Admin' and 'Supervisor' will allow any SELECT SQL statements to be // executed, even if the table can't be retrieved and checked (corresponding // to the reSQLSelectWithoutTable flag) // - 'User' won't have the reSQLSelectWithoutTable flag, nor the right // to retrieve the Auth tables data for other users // - 'Guest' won't have access to the interface-based remote JSON-RPC service // (no reService flag), nor perform any modification to a table: in short, // this is an ORM read-only limited user // - you MUST override the default 'synopse' password to a custom value, // or at least customize the global AuthAdminDefaultPassword, // AuthSupervisorDefaultPassword, AuthUserDefaultPassword variables // - of course, you can change and tune the settings of the AuthGroup and // AuthUser tables, but only 'Admin' group users will be able to remotely // modify the content of those two tables class procedure InitializeTable(Server: TSQLRestServer; const FieldName: RawUTF8; Options: TSQLInitializeTableOptions); override; /// corresponding TSQLAccessRights for this authentication group // - content is converted into/from text format via AccessRight DB property // (so it will be not fixed e.g. by the binary TSQLFieldTables layout, i.e. // the MAX_SQLTABLES constant value) property SQLAccessRights: TSQLAccessRights read GetSQLAccessRights write SetSQLAccessRights; published /// the access right identifier, ready to be displayed // - the same identifier can be used only once (this column is marked as // unique via a "stored AS_UNIQUE" (i.e. "stored false") attribute) // - so you can retrieve a TSQLAuthGroup ID from its identifier, as such: // ! UserGroupID := fClient.MainFieldID(TSQLAuthGroup,'User'); property Ident: RawUTF8 index 50 read fIdent write fIdent stored AS_UNIQUE; /// the number of minutes a session is kept alive property SessionTimeout: integer read fSessionTimeOut write fSessionTimeOut; /// a textual representation of a TSQLAccessRights buffer property AccessRights: RawUTF8 index 1600 read fAccessRights write fAccessRights; end; /// table containing the Users registered for authentication // - this class should be added to the TSQLModel, together with TSQLAuthGroup, // to allow authentication support // - you can inherit from it to add your custom properties to each user info: // TSQLModel will search for any class inheriting from TSQLAuthUser to manage // per-user authorization data // - by default, it won't be accessible remotely by anyone; to enhance security, // you could use the TSynValidatePassWord filter to this table TSQLAuthUser = class(TSQLRecord) protected fLogonName: RawUTF8; fPasswordHashHexa: RawUTF8; fDisplayName: RawUTF8; fGroup: TSQLAuthGroup; fData: TSQLRawBlob; procedure SetPasswordPlain(const Value: RawUTF8); /// check if the user can authenticate in its current state // - called by TSQLRestServerAuthentication.GetUser() method // - this default implementation will return TRUE, i.e. allow the user // to log on // - override this method to disable user authentication, e.g. if the // user is disabled via a custom ORM boolean and date/time field function CanUserLog(Ctxt: TSQLRestServerURIContext): boolean; virtual; public /// static function allowing to compute a hashed password // - as expected by this class // - defined as virtual so that you may use your own hashing class // - you may specify your own values in aHashSalt/aHashRound, to enable // PBKDF2_HMAC_SHA256() use instead of plain SHA256(): it will increase // security on storage side (reducing brute force attack via rainbow tables) class function ComputeHashedPassword(const aPasswordPlain: RawUTF8; const aHashSalt: RawUTF8=''; aHashRound: integer=20000): RawUTF8; virtual; /// able to set the PasswordHashHexa field from a plain password content // - in fact, PasswordHashHexa := SHA256('salt'+PasswordPlain) in UTF-8 // - use SetPassword() method if you want to customize the hash salt value // and use the much safer PBKDF2_HMAC_SHA256 algorithm property PasswordPlain: RawUTF8 write SetPasswordPlain; /// set the PasswordHashHexa field from a plain password content and salt // - use this method to specify aHashSalt/aHashRound values, enabling // PBKDF2_HMAC_SHA256() use instead of plain SHA256(): it will increase // security on storage side (reducing brute force attack via rainbow tables) // - you may use an application specific fixed salt, and/or append the // user LogonName to make the challenge unique for each TSQLAuthUser // - the default aHashRound=20000 is slow but secure - since the hashing // process is expected to be done on client side, you may specify your // own higher/slower value, depending on the security level you expect procedure SetPassword(const aPasswordPlain, aHashSalt: RawUTF8; aHashRound: integer=20000); published /// the User identification Name, as entered at log-in // - the same identifier can be used only once (this column is marked as // unique via a "stored AS_UNIQUE" - i.e. "stored false" - attribute), and // therefore indexed in the database (e.g. hashed in TSQLRestStorageInMemory) property LogonName: RawUTF8 index 20 read fLogonName write fLogonName stored AS_UNIQUE; /// the User Name, as may be displayed or printed property DisplayName: RawUTF8 index 50 read fDisplayName write fDisplayName; /// the hexa encoded associated SHA-256 hash of the password // - see TSQLAuthUser.ComputeHashedPassword() or SetPassword() methods // - store the SHA-256 32 bytes as 64 hexa chars property PasswordHashHexa: RawUTF8 index 64 read fPasswordHashHexa write fPasswordHashHexa; /// the associated access rights of this user // - access rights are managed by group // - in TAuthSession.User instance, GroupRights property will contain a // REAL TSQLAuthGroup instance for fast retrieval in TSQLRestServer.URI // - note that 'Group' field name is not allowed by SQLite property GroupRights: TSQLAuthGroup read fGroup write fGroup; /// some custom data, associated to the User // - Server application may store here custom data // - its content is not used by the framework but 'may' be used by your // application property Data: TSQLRawBlob read fData write fData; end; /// class used to maintain in-memory sessions // - this is not a TSQLRecord table so won't be remotely accessible, for // performance and security reasons // - the User field is a true instance, copy of the corresponding database // content (for better speed) // - you can inherit from this class, to add custom session process TAuthSession = class(TSynPersistent) protected fUser: TSQLAuthUser; fID: RawUTF8; fIDCardinal: cardinal; fTimeOutTix: cardinal; fTimeOutShr10: cardinal; fPrivateKey: RawUTF8; fPrivateSalt: RawUTF8; fSentHeaders: RawUTF8; fRemoteIP: RawUTF8; fPrivateSaltHash: Cardinal; fLastTimestamp: Cardinal; fExpectedHttpAuthentication: RawUTF8; fAccessRights: TSQLAccessRights; fMethods: TSynMonitorInputOutputObjArray; fInterfaces: TSynMonitorInputOutputObjArray; function GetUserName: RawUTF8; function GetUserID: TID; function GetGroupID: TID; procedure SaveTo(W: TFileBufferWriter); virtual; procedure ComputeProtectedValues; virtual; constructor CreateFrom(var P: PAnsiChar; PEnd: PAnsiChar; Server: TSQLRestServer); virtual; public /// initialize a session instance with the supplied TSQLAuthUser instance // - this aUser instance will be handled by the class until Destroy // - raise an exception on any error // - on success, will also retrieve the aUser.Data BLOB field content constructor Create(aCtxt: TSQLRestServerURIContext; aUser: TSQLAuthUser); reintroduce; virtual; /// will release the User and User.GroupRights instances destructor Destroy; override; public /// the session ID number, as numerical value // - never equals to 1 (CONST_AUTHENTICATION_NOT_USED, i.e. authentication // mode is not enabled), nor 0 (CONST_AUTHENTICATION_SESSION_NOT_STARTED, // i.e. session still in handshaking phase) property IDCardinal: cardinal read fIDCardinal; /// the associated User // - this is a true TSQLAuthUser instance, and User.GroupRights will contain // also a true TSQLAuthGroup instance property User: TSQLAuthUser read fUser; /// set by the Access() method to the current GetTickCount64 shr 10 // timestamp + TimeoutSecs property TimeOutTix: cardinal read fTimeOutTix; /// copy of the associated user access rights // - extracted from User.TSQLAuthGroup.SQLAccessRights property AccessRights: TSQLAccessRights read fAccessRights; /// the hexadecimal private key as returned to the connected client // as 'SessionID+PrivateKey' property PrivateKey: RawUTF8 read fPrivateKey; /// the transmitted HTTP headers, if any // - can contain e.g. 'RemoteIp: 127.0.0.1' or 'User-Agent: Mozilla/4.0' property SentHeaders: RawUTF8 read fSentHeaders; /// per-session statistics about method-based services // - Methods[] follows TSQLRestServer.fPublishedMethod[] array // - is initialized and maintained only if mlSessions is defined in // TSQLRestServer.StatLevels property property Methods: TSynMonitorInputOutputObjArray read fMethods; /// per-session statistics about interface-based services // - Interfaces[] follows TSQLRestServer.Services.fListInterfaceMethod[] array // - is initialized and maintained only if mlSessions is defined in // TSQLRestServer.StatLevels property property Interfaces: TSynMonitorInputOutputObjArray read fInterfaces; published /// the session ID number, as text property ID: RawUTF8 read fID; /// the associated User Name, as in User.LogonName property UserName: RawUTF8 read GetUserName; /// the associated User ID, as in User.ID property UserID: TID read GetUserID; /// the associated Group ID, as in User.GroupRights.ID property GroupID: TID read GetGroupID; /// the timestamp (in numbers of 1024 ms) until a session is kept alive // - extracted from User.TSQLAuthGroup.SessionTimeout // - is used for fast comparison with GetTickCount64 shr 10 property TimeoutShr10: cardinal read fTimeOutShr10; /// the remote IP, if any // - is extracted from SentHeaders properties property RemoteIP: RawUTF8 read fRemoteIP; end; /// class-reference type (metaclass) used to define overridden session instances // - since all sessions data remain in memory, ensure they are not taking too // much resource (memory or process time) // - if you plan to use session persistence, ensure you override the // TAuthSession.SaveTo/CreateFrom methods in the inherited class TAuthSessionClass = class of TAuthSession; /// class-reference type (metaclass) used to define an authentication scheme TSQLRestServerAuthenticationClass = class of TSQLRestServerAuthentication; /// maintain a list of TSQLRestServerAuthentication instances TSQLRestServerAuthenticationDynArray = array of TSQLRestServerAuthentication; /// define how TSQLRestServerAuthentication.ClientSetUser() should interpret // the supplied password // - passClear means that the password is not encrypted, e.g. as entered // by the user in the login screen // - passHashed means that the passwod is already hashed as in // TSQLAuthUser.PasswordHashHexa i.e. SHA256('salt'+Value) // - passKerberosSPN indicates that the password is the Kerberos SPN domain TSQLRestServerAuthenticationClientSetUserPassword = ( passClear, passHashed, passKerberosSPN); /// optional behavior of TSQLRestServerAuthentication class // - by default, saoUserByLogonOrID is set, allowing // TSQLRestServerAuthentication.GetUser() to retrieve the TSQLAuthUser by // logon name or by ID, if the supplied logon name is an integer // - if saoHandleUnknownLogonAsStar is defined, any user successfully // authenticated could be logged with the same ID (and authorization) // than TSQLAuthUser.Logon='*' - of course, this is meaningfull only with // an external credential check (e.g. via SSPI or Active Directory) TSQLRestServerAuthenticationOption = ( saoUserByLogonOrID, saoHandleUnknownLogonAsStar); /// defines the optional behavior of TSQLRestServerAuthentication class TSQLRestServerAuthenticationOptions = set of TSQLRestServerAuthenticationOption; /// abstract class used to implement server-side authentication in TSQLRestServer // - inherit from this class to implement expected authentication scheme TSQLRestServerAuthentication = class protected fServer: TSQLRestServer; fOptions: TSQLRestServerAuthenticationOptions; fAlgoName: RawUTF8; // GET ModelRoot/auth?UserName=...&Session=... -> release session function AuthSessionRelease(Ctxt: TSQLRestServerURIContext): boolean; /// retrieve an User instance from its logon name // - should return nil if not found // - this default implementation will retrieve it from ORM, and // call TSQLAuthUser.CanUserLog() to ensure authentication is allowed // - if aUserName is an integer, it will try to retrieve it from ORM using // the supplied value as its TSQLAuthUser.ID: it may be convenient when the // client is not an end-user application but a mORMot server (in a cloud // architecture), since it will benefit from local ORM cache // - you can override this method and return an on-the-fly created value // as a TSQLRestServer.SQLAuthUserClass instance (i.e. not persisted // in database nor retrieved by ORM), but the resulting TSQLAuthUser // must have its ID and LogonName properties set with unique values (which // will be used to identify it for a later call and session owner // identification), and its GroupRights property must not yet contain a real // TSQLAuthGroup instance, just a TSQLAuthGroup(aGroupID) value (as directly // retrieved from the ORM) - TAuthSession.Create will retrieve the instance // - another possibility, orthogonal to all TSQLRestServerAuthentication // classes, may be to define a TSQLRestServer.OnAuthenticationUserRetrieve // custom event function GetUser(Ctxt: TSQLRestServerURIContext; const aUserName: RawUTF8): TSQLAuthUser; virtual; /// create a session on the server for a given user // - this default implementation will call fServer.SessionCreate() and // return a '{"result":"HEXASALT","logonname":"UserName"}' JSON content // and will always call User.Free // - on failure, will call TSQLRestServerURIContext.AuthenticationFailed() // with afSessionAlreadyStartedForThisUser or afSessionCreationAborted reason procedure SessionCreate(Ctxt: TSQLRestServerURIContext; var User: TSQLAuthUser); virtual; /// Ctxt.Returns(['result',result,....[,'data',data]],200,header); procedure SessionCreateReturns(Ctxt: TSQLRestServerURIContext; Session: TAuthSession; const result, data, header: RawUTF8); /// abstract method which will be called by ClientSetUser() to process the // authentication step on the client side // - at call, a TSQLAuthUser instance will be supplied, with LogonName set // with aUserName and PasswordHashHexa with a SHA-256 hash of aPassword // - override with the expected method, returning the session key on success class function ClientComputeSessionKey(Sender: TSQLRestClientURI; User: TSQLAuthUser): RawUTF8; virtual; abstract; /// is called by ClientComputeSessionKey() overriden method to execute the // root/Auth service with the supplied parameters, then retrieve and // decode the "result": session key and any other values (e.g. "version") class function ClientGetSessionKey(Sender: TSQLRestClientURI; User: TSQLAuthUser; const aNameValueParameters: array of const): RawUTF8; virtual; public /// initialize the authentication method to a specified server // - you can define several authentication schemes for the same server constructor Create(aServer: TSQLRestServer); virtual; /// called by the Server to implement the Auth RESTful method // - overridden method shall return TRUE if the request has been handled // - returns FALSE to let the next registered TSQLRestServerAuthentication // class to try implementing the content // - Ctxt.Parameters has been tested to contain an UserName=... value // - method execution is protected by TSQLRestServer.fSessions.Lock function Auth(Ctxt: TSQLRestServerURIContext): boolean; virtual; abstract; /// called by the Server to check if the execution context match a session // - returns a session instance corresponding to the remote request, and // fill Ctxt.Session* members according to in-memory session information // - returns nil if this remote request does not match this authentication // - method execution should be protected by TSQLRestServer.fSessions.Lock function RetrieveSession(Ctxt: TSQLRestServerURIContext): TAuthSession; virtual; abstract; /// allow to tune the authentication process // - default value is [saoUserByLogonOrID] property Options: TSQLRestServerAuthenticationOptions read fOptions write fOptions; /// class method to be used on client side to create a remote session // - call this method instead of TSQLRestClientURI.SetUser() if you need // a custom authentication class // - if saoUserByLogonOrID is defined in the server Options, aUserName may // be a TSQLAuthUser.ID and not a TSQLAuthUser.LogonName // - if passClear is used, you may specify aHashSalt and aHashRound, // to enable PBKDF2_HMAC_SHA256() use instead of plain SHA256(), and increase // security on storage side (reducing brute force attack via rainbow tables) // - will call the ModelRoot/Auth service, i.e. call TSQLRestServer.Auth() // published method to create a session for this user // - returns true on success class function ClientSetUser(Sender: TSQLRestClientURI; const aUserName, aPassword: RawUTF8; aPasswordKind: TSQLRestServerAuthenticationClientSetUserPassword=passClear; const aHashSalt: RawUTF8=''; aHashRound: integer=20000): boolean; virtual; /// class method to be called on client side to sign an URI // - used by TSQLRestClientURI.URI() // - shall match the method as expected by RetrieveSession() virtual method class procedure ClientSessionSign(Sender: TSQLRestClientURI; var Call: TSQLRestURIParams); virtual; abstract; end; /// weak authentication scheme using URL-level parameter TSQLRestServerAuthenticationURI = class(TSQLRestServerAuthentication) public /// will check URI-level signature // - retrieve the session ID from 'session_signature=...' parameter // - method execution should be protected by TSQLRestServer.fSessions.Lock function RetrieveSession(Ctxt: TSQLRestServerURIContext): TAuthSession; override; /// class method to be called on client side to add the SessionID to the URI // - append '&session_signature=SessionID' to the url class procedure ClientSessionSign(Sender: TSQLRestClientURI; var Call: TSQLRestURIParams); override; end; /// algorithms known by TSQLRestServerAuthenticationSignedURI to digitaly // compute the session_signature parameter value for a given URI // - by default, suaCRC32 will compute fast but not cryptographically secure // ! crc32(crc32(privatesalt,timestamp,8),url,urllen) // - suaCRC32C and suaXXHASH will be faster and slightly safer // - but you can select other stronger alternatives, which result will be // reduced to 32-bit hexadecimal - suaMD5 will be the fastest cryptographic // hash available on all platforms, for enhanced security, by calling e.g. // ! (aServer.AuthenticationRegister(TSQLRestServerAuthenticationDefault) as // ! TSQLRestServerAuthenticationDefault).Algorithm := suaMD5; // - suaSHA1, suaSHA256 and suaSHA512 will be the slowest, to provide // additional level of trust, depending on your requirements: note that // since the hash is reduced to 32-bit resolution, those may not provide // higher security than suaMD5 // - note that SynCrossPlatformRest clients only implements suaCRC32 yet TSQLRestServerAuthenticationSignedURIAlgo = (suaCRC32, suaCRC32C, suaXXHASH, suaMD5, suaSHA1, suaSHA256, suaSHA512); /// function prototype for TSQLRestServerAuthenticationSignedURI computation // of the session_signature parameter value TSQLRestServerAuthenticationSignedURIComputeSignature = function( privatesalt: cardinal; timestamp, url: PAnsiChar; urllen: integer): cardinal of object; /// secure authentication scheme using URL-level digital signature // - default suaCRC32 format of session_signature is // !Hexa8(SessionID)+ // !Hexa8(Timestamp)+ // !Hexa8(crc32('SessionID+HexaSessionPrivateKey'+Sha256('salt'+PassWord)+ // ! Hexa8(Timestamp)+url)) TSQLRestServerAuthenticationSignedURI = class(TSQLRestServerAuthenticationURI) protected fNoTimestampCoherencyCheck: Boolean; fTimestampCoherencySeconds: cardinal; fTimestampCoherencyTicks: cardinal; fComputeSignature: TSQLRestServerAuthenticationSignedURIComputeSignature; procedure SetNoTimestampCoherencyCheck(value: boolean); procedure SetTimestampCoherencySeconds(value: cardinal); procedure SetAlgorithm(value: TSQLRestServerAuthenticationSignedURIAlgo); // class functions implementing TSQLRestServerAuthenticationSignedURIAlgo class function ComputeSignatureCrc32(privatesalt: cardinal; timestamp, url: PAnsiChar; urllen: integer): cardinal; class function ComputeSignatureCrc32c(privatesalt: cardinal; timestamp, url: PAnsiChar; urllen: integer): cardinal; class function ComputeSignaturexxHash(privatesalt: cardinal; timestamp, url: PAnsiChar; urllen: integer): cardinal; class function ComputeSignatureMD5(privatesalt: cardinal; timestamp, url: PAnsiChar; urllen: integer): cardinal; class function ComputeSignatureSHA1(privatesalt: cardinal; timestamp, url: PAnsiChar; urllen: integer): cardinal; class function ComputeSignatureSHA256(privatesalt: cardinal; timestamp, url: PAnsiChar; urllen: integer): cardinal; class function ComputeSignatureSHA512(privatesalt: cardinal; timestamp, url: PAnsiChar; urllen: integer): cardinal; class function GetComputeSignature(algo: TSQLRestServerAuthenticationSignedURIAlgo): TSQLRestServerAuthenticationSignedURIComputeSignature; public /// initialize the authentication method to a specified server constructor Create(aServer: TSQLRestServer); override; /// will check URI-level signature // - check session_signature=... parameter to be a valid digital signature // - method execution should be protected by TSQLRestServer.fSessions.Lock function RetrieveSession(Ctxt: TSQLRestServerURIContext): TAuthSession; override; /// class method to be called on client side to sign an URI // - generate the digital signature as expected by overridden RetrieveSession() // - timestamp resolution is about 256 ms in the current implementation class procedure ClientSessionSign(Sender: TSQLRestClientURI; var Call: TSQLRestURIParams); override; /// allow any order when creating sessions // - by default, signed sessions are expected to be sequential, and new // signed session signature can't be older in time than the last one, // with a tolerance of TimestampCoherencySeconds // - but if your client is asynchronous (e.g. for AJAX requests), session // may be rejected due to the delay involved on the client side: you can set // this property to TRUE to enabled a weaker but more tolerant behavior // ! (aServer.AuthenticationRegister(TSQLRestServerAuthenticationDefault) as // ! TSQLRestServerAuthenticationSignedURI).NoTimestampCoherencyCheck := true; property NoTimestampCoherencyCheck: Boolean read fNoTimestampCoherencyCheck write SetNoTimestampCoherencyCheck; /// time tolerance in seconds for the signature timestamps coherency check // - by default, signed sessions are expected to be sequential, and new // signed session signature can't be older in time than the last one, // with a tolerance time defined by this property // - default value is 5 seconds, which cover most kind of clients (AJAX or // WebSockets), even over a slow Internet connection property TimestampCoherencySeconds: cardinal read fTimestampCoherencySeconds write SetTimestampCoherencySeconds; /// customize the session_signature signing algorithm with a specific function // - the very same function should be set on TSQLRestClientURI // - to select a known hash algorithm, you may change the Algorithm property property ComputeSignature: TSQLRestServerAuthenticationSignedURIComputeSignature read fComputeSignature write fComputeSignature; /// customize the session_signature signing algorithm // - you need to set this value on the server side only; those known algorithms // will be recognized by TSQLRestClientURI on the client side during the // session handshake, to select the matching ComputeSignature function property Algorithm: TSQLRestServerAuthenticationSignedURIAlgo write SetAlgorithm; end; /// mORMot secure RESTful authentication scheme // - this method will use a password stored via safe SHA-256 hashing in the // TSQLAuthUser ORM table TSQLRestServerAuthenticationDefault = class(TSQLRestServerAuthenticationSignedURI) protected /// check a supplied password content // - will match ClientComputeSessionKey() algorithm as overridden here, i.e. // a SHA-256 based signature with a 10 minutes activation window function CheckPassword(Ctxt: TSQLRestServerURIContext; User: TSQLAuthUser; const aClientNonce, aPassWord: RawUTF8): boolean; virtual; /// class method used on client side to create a remote session // - will call the ModelRoot/Auth service, i.e. call TSQLRestServer.Auth() // published method to create a session for this user: so // TSQLRestServerAuthenticationDefault should be registered on server side // - User.LogonName and User.PasswordHashHexa will be checked class function ClientComputeSessionKey(Sender: TSQLRestClientURI; User: TSQLAuthUser): RawUTF8; override; public /// will try to handle the Auth RESTful method with mORMot authentication // - to be called in a two pass "challenging" algorithm: // $ GET ModelRoot/auth?UserName=... // $ -> returns an hexadecimal nonce contents (valid for 5 minutes) // $ GET ModelRoot/auth?UserName=...&PassWord=...&ClientNonce=... // $ -> if password is OK, will open the corresponding session // $ and return 'SessionID+HexaSessionPrivateKey' // The Password parameter as sent for the 2nd request will be computed as // ! Sha256(ModelRoot+Nonce+ClientNonce+UserName+Sha256('salt'+PassWord)) // - the returned HexaSessionPrivateKey content will identify the current // user logged and its corresponding session (the same user may have several // sessions opened at once, each with its own private key) // - then the private session key must be added to every query sent to // the server as a session_signature=???? parameter, which will be computed // as such: // $ ModelRoot/url?A=1&B=2&session_signature=012345670123456701234567 // were the session_signature= parameter will be computed as such: // ! Hexa8(SessionID)+Hexa8(Timestamp)+ // ! Hexa8(crc32('SessionID+HexaSessionPrivateKey'+Sha256('salt'+PassWord)+ // ! Hexa8(Timestamp)+url)) // ! with url='ModelRoot/url?A=1&B=2' // this query authentication uses crc32 for hashing instead of SHA-256 in // in order to lower the Server-side CPU consumption; the salted password // (i.e. TSQLAuthUser.PasswordHashHexa) and client-side Timestamp are // inserted inside the session_signature calculation to prevent naive // man-in-the-middle attack (MITM) // - the session ID will be used to retrieve the rights associated with the // user which opened the session via a successful call to the Auth service // - when you don't need the session any more (e.g. if the TSQLRestClientURI // instance is destroyed), you can call the service as such: // $ GET ModelRoot/auth?UserName=...&Session=... // - for a way of computing SHA-256 in JavaScript, see for instance // @http://www.webtoolkit.info/javascript-sha256.html function Auth(Ctxt: TSQLRestServerURIContext): boolean; override; end; /// mORMot weak RESTful authentication scheme // - this method will authenticate with a given username, but no signature // - on client side, this scheme is not called by TSQLRestClientURI.SetUser() // method - so you have to write: // ! TSQLRestServerAuthenticationNone.ClientSetUser(Client,'User',''); TSQLRestServerAuthenticationNone = class(TSQLRestServerAuthenticationURI) protected /// class method used on client side to create a remote session // - will call the ModelRoot/Auth service, i.e. call TSQLRestServer.Auth() // published method to create a session for this user: so // TSQLRestServerAuthenticationNone should be registered on server side // - will check User.LogonName, but User.PasswordHashHexa will be ignored class function ClientComputeSessionKey(Sender: TSQLRestClientURI; User: TSQLAuthUser): RawUTF8; override; public /// will try to handle the Auth RESTful method with mORMot authentication // - to be called in a weak one pass request: // $ GET ModelRoot/auth?UserName=... // $ -> if the specified user name exists, will open the corresponding // $ session and return 'SessionID+HexaSessionPrivateKey' function Auth(Ctxt: TSQLRestServerURIContext): boolean; override; end; /// abstract class for implementing HTTP authentication // - do not use this abstract class, but e.g. TSQLRestServerAuthenticationHttpBasic // - this class will transmit the session_signature as HTTP cookie, not at // URI level, so is expected to be used only from browsers or old clients TSQLRestServerAuthenticationHttpAbstract = class(TSQLRestServerAuthentication) protected /// should be overriden according to the HTTP authentication scheme class function ComputeAuthenticateHeader( const aUserName,aPasswordClear: RawUTF8): RawUTF8; virtual; abstract; public /// will check the caller signature // - retrieve the session ID from "Cookie: mORMot_session_signature=..." HTTP header // - method execution should be protected by TSQLRestServer.fSessions.Lock function RetrieveSession(Ctxt: TSQLRestServerURIContext): TAuthSession; override; /// class method to be called on client side to sign an URI in Auth Basic // resolution is about 256 ms in the current implementation // - set "Cookie: mORMot_session_signature=..." HTTP header class procedure ClientSessionSign(Sender: TSQLRestClientURI; var Call: TSQLRestURIParams); override; /// class method to be used on client side to create a remote session // - call TSQLRestServerAuthenticationHttpBasic.ClientSetUser() instead of // TSQLRestClientURI.SetUser(), and never the method of this abstract class // - needs the plain aPassword, so aPasswordKind should be passClear // - returns true on success class function ClientSetUser(Sender: TSQLRestClientURI; const aUserName, aPassword: RawUTF8; aPasswordKind: TSQLRestServerAuthenticationClientSetUserPassword=passClear; const aHashSalt: RawUTF8=''; aHashRound: integer=20000): boolean; override; /// class method to be used on client side to force the HTTP header for // the corresponding HTTP authentication, without creating any remote session // - call virtual protected method ComputeAuthenticateHeader() // - here the password should be given as clear content // - potential use case is to use a mORMot client through a HTTPS proxy, // e.g. with TSQLRestServerAuthenticationHttpBasic authentication // - then you can use TSQLRestServerAuthentication*.ClientSetUser() to // define any another "mORMot only" authentication // - this method is also called by the ClientSetUser() method of this class // for a full client + server authentication via HTTP // TSQLRestServerAuthenticationHttp*.ClientSetUser() class procedure ClientSetUserHttpOnly(Sender: TSQLRestClientURI; const aUserName, aPasswordClear: RawUTF8); virtual; end; /// authentication using HTTP Basic scheme // - this protocol send both name and password as clear (just base-64 encoded) // so should only be used over SSL / HTTPS, or for compatibility reasons // - will rely on TSQLRestServerAuthenticationNone for authorization // - on client side, this scheme is not called by TSQLRestClientURI.SetUser() // method - so you have to write: // ! TSQLRestServerAuthenticationHttpBasic.ClientSetUser(Client,'User','password'); // - for a remote proxy-only authentication (without creating any mORMot // session), you can write: // ! TSQLRestServerAuthenticationHttpBasic.ClientSetUserHttpOnly(Client,'proxyUser','proxyPass'); TSQLRestServerAuthenticationHttpBasic = class(TSQLRestServerAuthenticationHttpAbstract) protected /// this overriden method returns "Authorization: Basic ...." HTTP header class function ComputeAuthenticateHeader( const aUserName,aPasswordClear: RawUTF8): RawUTF8; override; /// decode "Authorization: Basic ...." header // - you could implement you own password transmission pattern, by // overriding both ComputeAuthenticateHeader and GetUserPassFromInHead methods class function GetUserPassFromInHead(Ctxt: TSQLRestServerURIContext; out userPass,user,pass: RawUTF8): boolean; virtual; /// check a supplied password content // - this default implementation will use the SHA-256 hash value stored // within User.PasswordHashHexa // - you can override this method to provide your own password check // mechanism, for the given TSQLAuthUser instance function CheckPassword(Ctxt: TSQLRestServerURIContext; User: TSQLAuthUser; const aPassWord: RawUTF8): boolean; virtual; public /// will check URI-level signature // - retrieve the session ID from 'session_signature=...' parameter // - will also check incoming "Authorization: Basic ...." HTTP header // - method execution should be protected by TSQLRestServer.fSessions.Lock function RetrieveSession(Ctxt: TSQLRestServerURIContext): TAuthSession; override; /// handle the Auth RESTful method with HTTP Basic // - will first return HTTP_UNAUTHORIZED (401), then expect user and password // to be supplied as incoming "Authorization: Basic ...." headers function Auth(Ctxt: TSQLRestServerURIContext): boolean; override; end; {$ifdef DOMAINAUTH} /// authentication of the current logged user using Windows Security Support // Provider Interface (SSPI) or GSSAPI library on Linux // - is able to authenticate the currently logged user on the client side, // using either NTLM (Windows only) or Kerberos - it will allow to safely // authenticate on a mORMot server without prompting the user to enter its // password // - if ClientSetUser() receives aUserName as '', aPassword should be either // '' if you expect NTLM authentication to take place, or contain the SPN // registration (e.g. 'mymormotservice/myserver.mydomain.tld') for Kerberos // authentication // - if ClientSetUser() receives aUserName as 'DomainName\UserName', then // authentication will take place on the specified domain, with aPassword // as plain password value TSQLRestServerAuthenticationSSPI = class(TSQLRestServerAuthenticationSignedURI) protected /// Windows built-in authentication // - holds information between calls to ServerSSPIAuth fSSPIAuthContexts: TSecContextDynArray; /// class method used on client side to create a remote session // - will call the ModelRoot/Auth service, i.e. call TSQLRestServer.Auth() // published method to create a session for this user: so // TSQLRestServerAuthenticationSSPI should be registered on server side // - Windows SSPI authentication will be performed - in this case, // table TSQLAuthUser shall contain an entry for the logged Windows user, // with the LoginName in form 'DomainName\UserName' // - if User.LogonName is '', then User.PasswordHashHexa is '' for // NTLM authentication, or the SPN registration for Kerberos authentication // - if User.LogonName is set as 'DomainName\UserName', then authentication // will take place on the specified domain, with User.PasswordHashHexa as // plain password class function ClientComputeSessionKey(Sender: TSQLRestClientURI; User: TSQLAuthUser): RawUTF8; override; public /// initialize the authentication method to a specified server constructor Create(aServer: TSQLRestServer); override; /// finalize internal memory structures destructor Destroy; override; /// will try to handle the Auth RESTful method with Windows SSPI API // - to be called in a two pass algorithm, used to cypher the password // - the client-side logged user will be identified as valid, according // to a Windows SSPI API secure challenge function Auth(Ctxt: TSQLRestServerURIContext): boolean; override; end; {$endif DOMAINAUTH} /// supported REST authentication schemes // - used by the overloaded TSQLHttpServer.Create(TSQLHttpServerDefinition) // constructor in mORMotHttpServer.pas, and also in dddInfraSettings.pas // - asSSPI won't be defined under Linux, since it is a Windows-centric feature TSQLHttpServerRestAuthentication = ( adDefault, adHttpBasic, adWeak, adSSPI); /// parameters supplied to publish a TSQLRestServer via HTTP // - used by the overloaded TSQLHttpServer.Create(TSQLHttpServerDefinition) // constructor in mORMotHttpServer.pas, and also in dddInfraSettings.pas TSQLHttpServerDefinition = class(TSynPersistentWithPassword) protected FBindPort: RawByteString; FAuthentication: TSQLHttpServerRestAuthentication; FEnableCORS: RawUTF8; FThreadCount: byte; FHttps: boolean; FHttpSysQueueName: SynUnicode; FRemoteIPHeader: RawUTF8; published /// defines the port to be used for REST publishing // - may include an optional IP address to bind, e.g. '127.0.0.1:8888' property BindPort: RawByteString read FBindPort write FBindPort; /// which authentication is expected to be published property Authentication: TSQLHttpServerRestAuthentication read FAuthentication write FAuthentication; /// allow Cross-origin resource sharing (CORS) access // - set this property to '*' if you want to be able to access the // REST methods from an HTML5 application hosted in another location, // or define a CSV white list of TMatch-compatible origins // - will set e.g. the following HTTP header: // ! Access-Control-Allow-Origin: * property EnableCORS: RawUTF8 read FEnableCORS write FEnableCORS; /// how many threads the thread pool associated with this HTTP server // should create // - if set to 0, will use default value 32 // - this parameter may be ignored depending on the actual HTTP // server used, which may not have any thread pool property ThreadCount: byte read fThreadCount write fThreadCount; /// defines if https:// protocol should be used // - implemented only by http.sys server under Windows, not by socket servers property Https: boolean read FHttps write FHttps; /// the displayed name in the http.sys queue // - used only by http.sys server under Windows, not by socket-based servers property HttpSysQueueName: SynUnicode read FHttpSysQueueName write FHttpSysQueueName; /// the value of a custom HTTP header containing the real client IP // - by default, the RemoteIP information will be retrieved from the socket // layer - but if the server runs behind some proxy service, you should // define here the HTTP header name which indicates the true remote client // IP value, mostly as 'X-Real-IP' or 'X-Forwarded-For' property RemoteIPHeader: RawUTF8 read fRemoteIPHeader write fRemoteIPHeader; /// if defined, this HTTP server will use WebSockets, and our secure // encrypted binary protocol // - when stored in the settings JSON file, the password will be safely // encrypted as defined by TSynPersistentWithPassword // - use the inherited PlainPassword property to set or read its value property WebSocketPassword: RawUTF8 read fPassWord write fPassWord; end; /// TSynAuthentication* class using TSQLAuthUser/TSQLAuthGroup for credentials // - could be used e.g. for SynDBRemote access in conjunction with mORMot TSynAuthenticationRest = class(TSynAuthenticationAbstract) protected fServer: TSQLRestServer; fAllowedGroups: TIntegerDynArray; function GetPassword(const UserName: RawUTF8; out Password: RawUTF8): boolean; override; function GetUsersCount: integer; override; public /// initialize the authentication scheme // - you can optionally set the groups allowing to use SynDBRemote - if none // is specify, username/password is enough constructor Create(aServer: TSQLRestServer; const aAllowedGroups: array of integer); reintroduce; /// add some new groups to validate an user authentication procedure RegisterAllowedGroups(const aAllowedGroups: array of integer); /// to be used to compute a Hash on the client side, for a given Token // - the password will be hashed as expected by the GetPassword() method class function ComputeHash(Token: Int64; const UserName,PassWord: RawUTF8): cardinal; override; end; /// common ancestor for tracking TSQLRecord modifications // - e.g. TSQLRecordHistory and TSQLRecordVersion will inherit from this class // to track TSQLRecord changes TSQLRecordModification = class(TSQLRecord) protected fModifiedRecord: TID; fTimestamp: TModTime; public /// returns the modified record table, as stored in ModifiedRecord function ModifiedTable(Model: TSQLModel): TSQLRecordClass; {$ifdef HASINLINE}inline;{$endif} /// returns the record table index in the TSQLModel, as stored in ModifiedRecord function ModifiedTableIndex: integer; {$ifdef HASINLINE}inline;{$endif} /// returns the modified record ID, as stored in ModifiedRecord function ModifiedID: TID; {$ifdef HASINLINE}inline;{$endif} published /// identifies the modified record // - ID and table index in TSQLModel is stored as one RecordRef integer // - you can use ModifiedTable/ModifiedID to retrieve the TSQLRecord item // - in case of the record deletion, all matching TSQLRecordHistory won't // be touched by TSQLRestServer.AfterDeleteForceCoherency(): so this // property is a plain TID/Int64, not a TRecordReference field property ModifiedRecord: TID read fModifiedRecord write fModifiedRecord; /// when the modification was recorded // - even if in most cases, this timestamp may be synchronized over TSQLRest // instances (thanks to TSQLRestClientURI.ServerTimestampSynchronize), it // is not safe to use this field as absolute: you should rather rely on // pure monotonic ID/RowID increasing values (see e.g. TSQLRecordVersion) property Timestamp: TModTime read fTimestamp write fTimestamp; end; /// common ancestor for tracking changes on TSQLRecord tables // - used by TSQLRestServer.TrackChanges() method for simple fields history // - TSQLRestServer.InternalUpdateEvent will use this table to store individual // row changes as SentDataJSON, then will compress them in History BLOB // - note that any layout change of the tracked TSQLRecord table (e.g. adding // a new property) will break the internal data format, so will void the table TSQLRecordHistory = class(TSQLRecordModification) protected fEvent: TSQLHistoryEvent; fSentData: RawUTF8; fHistory: TSQLRawBlob; // BLOB storage layout is: RTTIheader + offsets + recordsdata fHistoryModel: TSQLModel; fHistoryTable: TSQLRecordClass; fHistoryTableIndex: integer; fHistoryUncompressed: RawByteString; fHistoryUncompressedCount: integer; fHistoryUncompressedOffset: TIntegerDynArray; fHistoryAdd: TFileBufferWriter; fHistoryAddCount: integer; fHistoryAddOffset: TIntegerDynArray; /// override this to customize fields intialization class procedure InitializeFields(const Fields: array of const; var JSON: RawUTF8); virtual; public /// load the change history of a given record // - then you can use HistoryGetLast, HistoryCount or HistoryGet() to access // all previous stored versions constructor CreateHistory(aClient: TSQLRest; aTable: TSQLRecordClass; aID: TID); /// finalize any internal memory destructor Destroy; override; /// called when the associated table is created in the database // - create index on History(ModifiedRecord,Event) for process speed-up class procedure InitializeTable(Server: TSQLRestServer; const FieldName: RawUTF8; Options: TSQLInitializeTableOptions); override; public /// prepare to access the History BLOB content // - ModifiedRecord should have been set to a proper value // - returns FALSE if the History BLOB is incorrect (e.g. TSQLRecord // layout changed): caller shall flush all previous history function HistoryOpen(Model: TSQLModel): boolean; /// returns how many revisions are stored in the History BLOB // - HistoryOpen() or CreateHistory() should have been called before // - this method will ignore any previous HistoryAdd() call function HistoryCount: integer; /// retrieve an historical version // - HistoryOpen() or CreateHistory() should have been called before // - this method will ignore any previous HistoryAdd() call // - if Rec=nil, will only retrieve Event and Timestamp // - if Rec is set, will fill all simple properties of this TSQLRecord function HistoryGet(Index: integer; out Event: TSQLHistoryEvent; out Timestamp: TModTime; Rec: TSQLRecord): boolean; overload; /// retrieve an historical version // - HistoryOpen() or CreateHistory() should have been called before // - this method will ignore any previous HistoryAdd() call // - will fill all simple properties of the supplied TSQLRecord instance function HistoryGet(Index: integer; Rec: TSQLRecord): boolean; overload; /// retrieve an historical version // - HistoryOpen() or CreateHistory() should have been called before // - this method will ignore any previous HistoryAdd() call // - will return either nil, or a TSQLRecord with all simple properties set function HistoryGet(Index: integer): TSQLRecord; overload; /// retrieve the latest stored historical version // - HistoryOpen() or CreateHistory() should have been called before // - this method will ignore any previous HistoryAdd() call // - you should not have to use it, since a TSQLRest.Retrieve() is faster function HistoryGetLast(Rec: TSQLRecord): boolean; overload; /// retrieve the latest stored historical version // - HistoryOpen() or CreateHistory() should have been called before, // otherwise it will return nil // - this method will ignore any previous HistoryAdd() call // - you should not have to use it, since a TSQLRest.Retrieve() is faster function HistoryGetLast: TSQLRecord; overload; /// add a record content to the History BLOB // - HistoryOpen() should have been called before using this method - // CreateHistory() won't allow history modification // - use then HistorySave() to compress and replace the History field procedure HistoryAdd(Rec: TSQLRecord; Hist: TSQLRecordHistory); /// update the History BLOB field content // - HistoryOpen() should have been called before using this method - // CreateHistory() won't allow history modification // - if HistoryAdd() has not been used, returns false // - ID field should have been set for proper persistence on Server // - otherwise compress the data into History BLOB, deleting the oldest // versions if resulting size is biggger than expected, and returns true // - if Server is set, write save the History BLOB to database // - if Server and LastRec are set, its content will be compared with the // current record in DB (via a Retrieve() call) and stored: it will allow // to circumvent any issue about inconsistent use of tracking, e.g. if the // database has been modified directly, by-passing the ORM function HistorySave(Server: TSQLRestServer; LastRec: TSQLRecord=nil): boolean; published /// the kind of modification stored // - is heArchiveBlob when this record stores the compress BLOB in History // - otherwise, SentDataJSON may contain the latest values as JSON property Event: TSQLHistoryEvent read fEvent write fEvent; /// for heAdd/heUpdate, the data is stored as JSON // - note that we defined a default maximum size of 4KB for this column, // to avoid using a CLOB here - perhaps it may not be enough for huge // records - feedback is welcome... property SentDataJSON: RawUTF8 index 4000 read fSentData write fSentData; /// after some events are written as individual SentData content, they // will be gathered and compressed within one BLOB field // - use HistoryOpen/HistoryCount/HistoryGet to access the stored data after // a call to CreateHistory() constructor // - as any BLOB field, this one won't be retrieved by default: use // explicitly TSQLRest.RetrieveBlobFields(aRecordHistory) to get it if you // want to access it directly, and not via CreateHistory() property History: TSQLRawBlob read fHistory write fHistory; end; /// class-reference type (metaclass) to specify the storage table to be used // for tracking TSQLRecord changes // - you can create your custom type from TSQLRecordHistory, even for a // particular table, to split the tracked changes storage in several tables: // ! type // ! TSQLRecordMyHistory = class(TSQLRecordHistory); // - as expected by TSQLRestServer.TrackChanges() method TSQLRecordHistoryClass = class of TSQLRecordHistory; /// ORM table used to store the deleted items of a versioned table // - the ID/RowID primary key of this table will be the version number // (i.e. value computed by TSQLRestServer.InternalRecordVersionCompute), // mapped with the corresponding 'TableIndex shl 58' (so that e.g. // TSQLRestServer.RecordVersionSynchronizeToBatch() could easily ask for the // deleted rows of a given table with a single WHERE clause on the ID/RowID) TSQLRecordTableDeleted = class(TSQLRecord) protected fDeleted: Int64; published /// this Deleted published field will track the deleted row // - defined as Int64 and not TID, to avoid the generation of the index on // this column, which is not needed here (all requests are about ID/RowID) property Deleted: Int64 read fDeleted write fDeleted; end; /// class-reference type (metaclass) to specify the storage table to be used // for tracking TSQLRecord deletion TSQLRecordTableDeletedClass = class of TSQLRecordTableDeleted; /// defines what is stored in a TSQLRestTempStorageItem entry TSQLRestTempStorageItemKind = set of (itemInsert,itemFakeID); /// used to store an entry in the TSQLRestTempStorage class TSQLRestTempStorageItem = record /// the ID of this entry // - after an AddCopy(ForceID=false), is a "fake" ID, which is > maxInt ID: TID; /// the stored item, either after adding or updating // - equals nil if the item has been deleted Value: TSQLRecord; /// identify the fields stored in the Value instance // - e.g. an Update() - or even an Add() - may only have set only simple or // specific fields ValueFields: TSQLFieldBits; /// what is stored in this entry Kind: TSQLRestTempStorageItemKind; end; /// used to store the entries in the TSQLRestTempStorage class TSQLRestTempStorageItemDynArray = array of TSQLRestTempStorageITem; /// abstract class used for temporary in-memory storage of TSQLRecord // - purpose of this class is to gather write operations (Add/Update/Delete) // - inherited implementations may send all updates at once to a server (i.e. // "asynchronous write"), or maintain a versioned image of the content // - all public methods (AddCopy/AddOwned/Update/Delete/FlushAsBatch) are // thread-safe, protected by a mutex lock TSQLRestTempStorage = class(TSynPersistentLock) protected fStoredClass: TSQLRecordClass; fStoredClassRecordProps: TSQLRecordProperties; fItem: TSQLRestTempStorageItemDynArray; fItems: TDynArray; fLastFakeID: TID; fCount: integer; function InternalSetFields(const FieldNames: RawUTF8; out Fields: TSQLFieldBits): boolean; procedure InternalAddItem(const item: TSQLRestTempStorageItem); public /// initialize the temporary storage for a given class constructor Create(aClass: TSQLRecordClass); reintroduce; virtual; /// finalize this temporary storage instance destructor Destroy; override; /// add a copy of a TSQLRecord to the internal storage list // - if ForceID is true, Value.ID will be supplied with the ID to add // - if ForceID is false, a "fake" ID is returned, which may be used later // on for Update() calls - WARNING: but this ID should not be stored as // a cross reference in another record, since it is private to this storage; // the definitive ID will be returned eventually after proper persistence // (e.g. sent as TSQLRestBatch to a mORMot server) // - FieldNames can be the CSV list of field names to be set // - if FieldNames is '', will set all simple fields, excluding BLOBs // - if FieldNames is '*', will set ALL fields, including BLOBs // - this method will clone the supplied Value, and make its own copy // for its internal storage - consider use AddOwned() if the caller does // not need to store the instance afterwards function AddCopy(Value: TSQLRecord; ForceID: boolean; const FieldNames: RawUTF8=''): TID; overload; /// add and own a TSQLRecord in the internal storage list // - if ForceID is true, Value.ID will be supplied with the ID to add // - if ForceID is false, a "fake" ID is returned, which may be used later // on for Update() calls - WARNING: but this ID should not be stored as // a cross reference in another record, since it is private to this storage; // the definitive ID will be returned eventually after proper persistence // (e.g. sent as TSQLRestBatch to a mORMot server) // - FieldNames can be the CSV list of field names to be set // - if FieldNames is '', will set all simple fields, excluding BLOBs // - if FieldNames is '*', will set ALL fields, including BLOBs // - this method will store the supplied Value, and let its internal // storage owns it and manage its lifetime - consider use AddCopy() if the // caller does need to store this instance afterwards // - returns 0 in case of error (e.g. ForceID and no or duplicated Value.ID) function AddOwned(Value: TSQLRecord; ForceID: boolean; const FieldNames: RawUTF8=''): TID; overload; /// add, update or delete a TSQLRecord in the internal storage list // - could be used from a TNotifySQLEvent/InternalUpdateEvent(seAdd) callback // - here the value to be added is supplied as a JSON object and a ID field // - returns false in case of error (e.g. duplicated ID or void JSON) function FromEvent(Event: TSQLEvent; ID: TID; const JSON: RawUTF8): boolean; /// add and own a TSQLRecord in the internal storage list // - if ForceID is true, Value.ID will be supplied with the ID to add // - if ForceID is false, a "fake" ID is returned, which may be used later // on for Update() calls - WARNING: but this ID should not be stored as // a cross reference in another record, since it is private to this storage; // the definitive ID will be returned eventually after proper persistence // (e.g. sent as TSQLRestBatch to a mORMot server) // - this overloaded version expects the fields to be specified as bits // - this method will store the supplied Value, and let its internal // storage owns it and manage its lifetime - consider use AddCopy() if the // caller does need to store this instance afterwards // - returns 0 in case of error (e.g. ForceID and no or duplicated Value.ID) function AddOwned(Value: TSQLRecord; ForceID: boolean; const Fields: TSQLFieldBits): TID; overload; /// mark a TSQLRecord as deleted in the internal storage list procedure Delete(const ID: TID); /// update a TSQLRecord and store the new values in the internal storage list // - Value.ID is used to identify the record to be updated (which may be // a just added "fake" ID) // - FieldNames can be the CSV list of field names to be updated // - if FieldNames is '', will update all simple fields, excluding BLOBs // - if FieldNames is '*', will update ALL fields, including BLOBs // - the supplied Value won't be owned by this instance: the caller should // release it when Value is no longer needed // - returns false in case of error (e.g. unknwown ID or invalid fields) function Update(Value: TSQLRecord; const FieldNames: RawUTF8=''): boolean; overload; /// update a TSQLRecord and store the new values in the internal storage list // - Value.ID is used to identify the record to be updated (which may be // a just added "fake" ID) // - this overloaded version expects the fields to be specified as bits // - the supplied Value won't be owned by this instance: the caller should // release it when Value is no longer needed // - returns false in case of error (e.g. unknwown ID or no field set) function Update(Value: TSQLRecord; const Fields: TSQLFieldBits): boolean; overload; /// convert the internal list as a TSQLRestBatch instance, ready to be // sent to the server function FlushAsBatch(Rest: TSQLRest; AutomaticTransactionPerRow: cardinal=1000): TSQLRestBatch; /// direct access to the low-level storage list // - the Count property is the number of items, length(Item) is the capacity // - the list is stored in increasing ID order property Item: TSQLRestTempStorageItemDynArray read fItem; /// how many entries are stored in the low-level storage list property Count: integer read fCount; end; /// how TSQLRestServer should maintain its statistical information // - used by TSQLRestServer.StatLevels property TSQLRestServerMonitorLevels = set of ( mlTables, mlMethods, mlInterfaces, mlSessions, mlSQLite3); /// used for high-level statistics in TSQLRestServer.URI() TSQLRestServerMonitor = class(TSynMonitorServer) protected fServer: TSQLRestServer; fStartDate: RawUTF8; fCurrentThreadCount: TSynMonitorOneCount; fSuccess: TSynMonitorCount64; fOutcomingFiles: TSynMonitorCount64; fServiceMethod: TSynMonitorCount64; fServiceInterface: TSynMonitorCount64; fCreated: TSynMonitorCount64; fRead: TSynMonitorCount64; fUpdated: TSynMonitorCount64; fDeleted: TSynMonitorCount64; // [Write: boolean] per-table statistics fPerTable: array[boolean] of TSynMonitorWithSizeObjArray; // no overriden Changed: TSQLRestServer.URI will do it in finally block public /// initialize the instance constructor Create(aServer: TSQLRestServer); reintroduce; /// finalize the instance destructor Destroy; override; /// should be called when a task successfully ended // - thread-safe method procedure ProcessSuccess(IsOutcomingFile: boolean); virtual; /// update and returns the CurrentThreadCount property // - this method is thread-safe function NotifyThreadCount(delta: integer): integer; /// update the Created/Read/Updated/Deleted properties // - this method is thread-safe procedure NotifyORM(aMethod: TSQLURIMethod); /// update the per-table statistics // - this method is thread-safe procedure NotifyORMTable(TableIndex, DataSize: integer; Write: boolean; const MicroSecondsElapsed: QWord); published /// when this monitoring instance (therefore the server) was created property StartDate: RawUTF8 read fStartDate; /// number of valid responses // - i.e. which returned status code 200/HTTP_SUCCESS or 201/HTTP_CREATED // - any invalid request will increase the TSynMonitor.Errors property property Success: TSynMonitorCount64 read fSuccess; /// count of the remote method-based service calls property ServiceMethod: TSynMonitorCount64 read fServiceMethod; /// count of the remote interface-based service calls property ServiceInterface: TSynMonitorCount64 read fServiceInterface; /// count of files transmitted directly (not part of Output size property) // - i.e. when the service uses STATICFILE_CONTENT_TYPE/HTTP_RESP_STATICFILE // as content type to let the HTTP server directly serve the file content property OutcomingFiles: TSynMonitorCount64 read fOutcomingFiles; /// number of current declared thread counts // - as registered by BeginCurrentThread/EndCurrentThread property CurrentThreadCount: TSynMonitorOneCount read fCurrentThreadCount; /// how many Create / Add ORM operations did take place property Created: TSynMonitorCount64 read fCreated; /// how many Read / Get ORM operations did take place property Read: TSynMonitorCount64 read fRead; /// how many Update ORM operations did take place property Updated: TSynMonitorCount64 read fUpdated; /// how many Delete ORM operations did take place property Deleted: TSynMonitorCount64 read fDeleted; end; /// ORM table used to store TSynMonitorUsage information in TSynMonitorUsageRest // - the ID primary field is the TSynMonitorUsageID (accessible from UsageID // public property) shifted by 16 bits (by default) to include a // TSynUniqueIdentifierProcess value TSQLMonitorUsage = class(TSQLRecordNoCaseExtended) protected fGran: TSynMonitorUsageGranularity; fProcess: Int64; fInfo: variant; fComment: RawUTF8; public /// compute the corresponding 23 bit TSynMonitorUsageID.Value time slice // - according to the stored Process field, after bit shift // - allows a custom aProcessIDShift if it is not set as default 16 bits function UsageID(aProcessIDShift: integer=16): integer; published /// the granularity of the statistics of this entry property Gran: TSynMonitorUsageGranularity read fGran write fGran; /// identify which application is monitored // - match the lower bits of each record ID // - by default, is expected to be a TSynUniqueIdentifierProcess 16-bit value property Process: Int64 read fProcess write fProcess; /// the actual statistics information, stored as a TDocVariant JSON object property Info: variant read fInfo write fInfo; /// a custom text, which may be used e.g. by support or developpers property Comment: RawUTF8 read fComment write fComment; end; /// class-reference type (metaclass) of a TSQLMonitorUsage table TSQLMonitorUsageClass = class of TSQLMonitorUsage; /// will store TSynMonitorUsage information in TSQLMonitorUsage ORM tables // - TSQLRecord.ID will be the TSynMonitorUsageID shifted by ProcessIDShift bits TSynMonitorUsageRest = class(TSynMonitorUsage) protected fStorage: TSQLRest; fProcessID: Int64; fProcessIDShift: integer; fStoredClass: TSQLMonitorUsageClass; fStoredCache: array[mugHour..mugYear] of TSQLMonitorUsage; fSaveBatch: TSQLRestBatch; function SaveDB(ID: integer; const Track: variant; Gran: TSynMonitorUsageGranularity): boolean; override; function LoadDB(ID: integer; Gran: TSynMonitorUsageGranularity; out Track: variant): boolean; override; public /// initialize storage via ORM // - if a 16-bit TSynUniqueIdentifierProcess is supplied, it will be used to // identify the generating process by shifting TSynMonitorUsageID values // by aProcessIDShift bits (default 16 but you may increase it up to 40 bits) // - will use TSQLMonitorUsage table, unless another one is specified constructor Create(aStorage: TSQLRest; aProcessID: Int64; aStoredClass: TSQLMonitorUsageClass=nil; aProcessIDShift: integer=16); reintroduce; virtual; /// finalize the process, saving pending changes destructor Destroy; override; /// you can set an optional Batch instance to speed up DB writing // - when calling the Modified() method property SaveBatch: TSQLRestBatch read fSaveBatch write fSaveBatch; published /// the actual ORM class used for persistence property StoredClass: TSQLMonitorUsageClass read fStoredClass; /// how the information could be stored for several processes // - e.g. when several SOA nodes gather monitoring information in a // shared (MongoDB) database // - is by default a TSynUniqueIdentifierProcess value, but may be // any integer up to ProcessIDShift bits as set in Create() property ProcessID: Int64 read fProcessID; /// how process ID are stored within the mORMot TSQLRecord.ID // - equals 16 bits by default, to match TSynUniqueIdentifierProcess resolution property ProcessIDShift: integer read fProcessIDShift; end; /// the flags used for TSQLRestServer.AddStats TSQLRestServerAddStat = (withTables, withMethods, withInterfaces, withSessions); /// some flags used for TSQLRestServer.AddStats TSQLRestServerAddStats = set of TSQLRestServerAddStat; /// a specialized UTF-8 string type, used for TSQLRestServerURI storage // - URI format is 'address:port/root', but port or root are optional // - you could use TSQLRestServerURI record to store and process it TSQLRestServerURIString = type RawUTF8; /// a list of UTF-8 strings, used for TSQLRestServerURI storage // - URI format is 'address:port/root', but port or root are optional // - you could use TSQLRestServerURI record to store and process each item TSQLRestServerURIStringDynArray = array of TSQLRestServerURIString; /// used to access a TSQLRestServer from its TSQLRestServerURIString URI // - URI format is 'address:port/root', and may be transmitted as // TSQLRestServerURIString text instances {$ifdef USERECORDWITHMETHODS}TSQLRestServerURI = record {$else}TSQLRestServerURI = object{$endif} private function GetURI: TSQLRestServerURIString; procedure SetURI(const Value: TSQLRestServerURIString); public /// the TSQLRestServer IP Address or DNS name Address: RawUTF8; /// the TSQLRestServer IP port Port: RawUTF8; /// the TSQLRestServer model Root Root: RawUTF8; /// returns TRUE if all field values do match, case insensitively function Equals(const other: TSQLRestServerURI): boolean; /// property which allows to read or set the Address/Port/Root fields as // one UTF-8 text field (i.e. a TSQLRestServerURIString instance) // - URI format is 'address:port/root', but port or root are optional property URI: TSQLRestServerURIString read GetURI write SetURI; end; /// store a list of TSQLRestServer URIs TSQLRestServerURIDynArray = array of TSQLRestServerURI; /// used to publish all Services supported by a TSQLRestServer instance // - as expected by TSQLRestServer.ServicesPublishedInterfaces // - can be serialized as a JSON object via RecordLoadJSON/RecordSaveJSON {$ifdef USERECORDWITHMETHODS}TServicesPublishedInterfaces = record {$else}TServicesPublishedInterfaces = object{$endif} public /// how this TSQLRestServer could be accessed PublicURI: TSQLRestServerURI; /// the list of supported services names // - in fact this is the Interface name without the initial 'I', e.g. // 'Calculator' for ICalculator Names: TRawUTF8DynArray; end; /// store a list of published Services supported by a TSQLRestServer instance TServicesPublishedInterfacesDynArray = array of TServicesPublishedInterfaces; /// used e.g. by TSQLRestServer to store a list of TServicesPublishedInterfaces TServicesPublishedInterfacesList = class(TSynPersistentLock) private fDynArray: TDynArray; fDynArrayTimeoutTix: TDynArray; fTimeoutTix: TInt64DynArray; fTimeoutTixCount: integer; fLastPublishedJson: cardinal; fTimeOut: integer; public /// the internal list of published services // - the list is stored in-order, i.e. it will follow the RegisterFromJSON() // execution order: the latest registrations will appear last List: TServicesPublishedInterfacesDynArray; /// how many items are actually stored in List[] Count: Integer; /// initialize the storage // - an optional time out period, in milliseconds, may be defined - but the // clients should ensure that RegisterFromClientJSON() is called in order // to refresh the list (e.g. from _contract_ HTTP body) constructor Create(aTimeoutMS: integer); reintroduce; virtual; /// add the JSON serialized TServicesPublishedInterfaces to the list // - called by TSQLRestServerURIContext.InternalExecuteSOAByInterface when // the client provides its own services as _contract_ HTTP body // - warning: supplied PublishedJson will be parsed in place, so modified procedure RegisterFromClientJSON(var PublishedJson: RawUTF8); /// set the list from JSON serialized TServicesPublishedInterfacesDynArray // - may be used to duplicate the whole TSQLRestServer.AssociatedServices // content, as returned from /root/Stat?findservice=* // - warning: supplied PublishedJson will be parsed in place, so modified procedure RegisterFromServerJSON(var PublishedJson: RawUTF8); /// set the list from a remote TSQLRestServer // - will call /root/Stat?findservice=* URI, then RegisterFromServerJSON() function RegisterFromServer(Client: TSQLRestClientURI): boolean; /// search for a public URI in the registration list function FindURI(const aPublicURI: TSQLRestServerURI): integer; /// search for the latest registrations of a service, by name // - will lookup for the Interface name without the initial 'I', e.g. // 'Calculator' for ICalculator - warning: research is case-sensitive // - if the service name has been registered several times, all // registration will be returned, the latest in first position function FindService(const aServiceName: RawUTF8): TSQLRestServerURIDynArray; /// return all services URI by name, from the registration list, as URIs // - will lookup for the Interface name without the initial 'I', e.g. // 'Calculator' for ICalculator - warning: research is case-sensitive // - the returned string will contain all matching server URI, the latest // registration being the first to appear, e.g. // $ ["addresslast:port/root","addressprevious:port/root","addressfirst:port/root"] function FindServiceAll(const aServiceName: RawUTF8): TSQLRestServerURIStringDynArray; overload; /// return all services URI by name, from the registration list, as JSON // - will lookup for the Interface name without the initial 'I', e.g. // 'Calculator' for ICalculator - warning: research is case-sensitive // - the returned JSON array will contain all matching server URI, encoded as // a TSQLRestServerURI JSON array, the latest registration being // the first to appear, e.g. // $ [{"Address":"addresslast","Port":"port","Root":"root"},...] // - if aServiceName='*', it will return ALL registration items, encoded as // a TServicesPublishedInterfaces JSON array, e.g. // $ [{"PublicURI":{"Address":"1.2.3.4","Port":"123","Root":"root"},"Names":['Calculator']},...] procedure FindServiceAll(const aServiceName: RawUTF8; aWriter: TTextWriter); overload; /// the number of milliseconds after which an entry expires // - is 0 by default, meaning no expiration // - you can set it to a value so that any service URI registered with // RegisterFromJSON() AFTER this property modification may expire property TimeOut: integer read fTimeOut write fTimeOut; end; /// class-reference type (metaclass) of a table containing the Users // registered for authentication // - see also TSQLRestServer.OnAuthenticationUserRetrieve custom event TSQLAuthUserClass = class of TSQLAuthUser; /// class-reference type (metaclass) of the table containing the available // user access rights for authentication, defined as a group TSQLAuthGroupClass = class of TSQLAuthGroup; /// class-reference type (metaclass) of a REST server TSQLRestServerClass = class of TSQLRestServer; /// some options for TSQLRestServer process // - read-only rsoNoAJAXJSON indicates that JSON data is transmitted in "not // expanded" format: you should NEVER change this option by including // this property in TSQLRestServer.Options, but always call explicitly // TSQLRestServer.NoAJAXJSON := true so that the SetNoAJAXJSON virtual // method should be called as expected (e.g. to flush TSQLRestServerDB cache) // - rsoGetAsJsonNotAsString will let ORM GET return to AJAX (non Delphi) // clients JSON objects instead of the JSON text stored in database fields // - rsoGetID_str will add a "ID_str": string field to circumvent JavaScript // limitation of 53-bit for integers - only for AJAX (non Delphi) clients // - unauthenticated requests from browsers (i.e. not Delphi clients) may // be redirected to the TSQLRestServer.Auth() method via rsoRedirectForbiddenToAuth // (e.g. for TSQLRestServerAuthenticationHttpBasic popup) // - some REST/AJAX clients may expect to return status code 204 as // instead of 200 in case of a successful operation, but with no returned // body (e.g. a DELETE with SAPUI5 / OpenUI5 framework): include // rsoHttp200WithNoBodyReturns204 so that any HTTP_SUCCESS (200) with no // returned body will return a HTTP_NOCONTENT (204), as expected by // some clients // - by default, Add() or Update() will return HTTP_CREATED (201) or // HTTP_SUCCESS (200) with no body, unless rsoAddUpdateReturnsContent is set // to return as JSON the last inserted/updated record // - TModTime / TCreateTime fields are expected to be filled on client side, // unless you set rsoComputeFieldsBeforeWriteOnServerSide so that AJAX requests // will set the fields on the server side by calling the TSQLRecord // ComputeFieldsBeforeWrite virtual method, before writing to the database // - rsoSecureConnectionRequired will ensure Call is flagged as llfSecured // (i.e. in-process, HTTPS, or encrypted WebSockets) - with the only exception // of the Timestamp method-based service (for monitoring purposes) - note that // this option doesn't make sense behind a proxy, just with a true HTTPS server // - by default, cookies will contain only 'Path=/Model.Root', but // '; Path=/' may be also added setting rsoCookieIncludeRootPath // - you can disable the 'HttpOnly' flag via rsoCookieHttpOnlyFlagDisable // - TSQLRestServerURIContext.AuthenticationBearerToken will return the // ?authenticationbearer=... URI parameter value alternatively to the HTTP // header unless rsoAuthenticationURIDisable is set (for security reasons) // - you can switch off root/timestamp/info URI via rsoTimestampInfoURIDisable // - URI() header output will be sanitized for any EOL injection, unless // rsoHttpHeaderCheckDisable is defined (to gain a few cycles?) // - by default, TSQLAuthUser.Data blob is retrieved from the database, // unless rsoGetUserRetrieveNoBlobData is defined // - rsoNoInternalState could be state to avoid transmitting the // 'Server-InternalState' header, e.g. if the clients wouldn't need it TSQLRestServerOption = ( rsoNoAJAXJSON, rsoGetAsJsonNotAsString, rsoGetID_str, rsoRedirectForbiddenToAuth, rsoHttp200WithNoBodyReturns204, rsoAddUpdateReturnsContent, rsoComputeFieldsBeforeWriteOnServerSide, rsoSecureConnectionRequired, rsoCookieIncludeRootPath, rsoCookieHttpOnlyFlagDisable, rsoAuthenticationURIDisable, rsoTimestampInfoURIDisable, rsoHttpHeaderCheckDisable, rsoGetUserRetrieveNoBlobData, rsoNoInternalState); /// allow to customize the TSQLRestServer process via its Options property TSQLRestServerOptions = set of TSQLRestServerOption; /// a generic REpresentational State Transfer (REST) server // - descendent must implement the protected EngineList() Retrieve() Add() // Update() Delete() methods // - automatic call of this methods by a generic URI() RESTful function // - any published method of descendants must match TSQLRestServerCallBack // prototype, and is expected to be thread-safe TSQLRestServer = class(TSQLRest) protected fVirtualTableDirect: boolean; fHandleAuthentication: boolean; fBypassORMAuthentication: TSQLURIMethods; /// the TSQLAuthUser and TSQLAuthGroup classes, as defined in model fSQLAuthUserClass: TSQLAuthUserClass; fSQLAuthGroupClass: TSQLAuthGroupClass; fAfterCreation: boolean; fOptions: TSQLRestServerOptions; fJWTForUnauthenticatedRequest: TJWTAbstract; /// how in-memory sessions are handled fSessionClass: TAuthSessionClass; /// will contain the in-memory representation of some static tables // - this array has the same length as the associated Model.Tables[] // - fStaticData[] will contain pure in-memory tables, not declared as // SQLite3 virtual tables, therefore not available from joined SQL statements fStaticData: TSQLRestDynArray; /// map TSQLRestStorageInMemory or TSQLRestStorageExternal engines // - this array has the same length as the associated Model.Tables[] // - fStaticVirtualTable[] will contain in-memory or external tables declared // as SQLite3 virtual tables, therefore available from joined SQL statements // - the very same TSQLRestStorage is handled in fStaticData fStaticVirtualTable: TSQLRestDynArray; /// in-memory storage of TAuthSession instances fSessions: TSynObjectListLocked; fSessionsDeprecatedTix: cardinal; /// used to compute genuine TAuthSession.ID cardinal value fSessionCounter: cardinal; fSessionAuthentication: TSQLRestServerAuthenticationDynArray; {$ifdef MSWINDOWS} /// thread initialized by ExportServerNamedPipe() to response to client through a pipe fExportServerNamedPipeThread: TSQLRestServerNamedPipe; /// internal server window handle, initialized by ExportServerMessage() method fServerWindow: HWND; /// internal server window class name, initialized by ExportServerMessage() method // - use "string" type, i.e. UnicodeString for Delphi 2009+, in order // to call directly the correct FindWindow?()=FindWindow Win32 API fServerWindowName: string; {$endif} fPublishedMethod: TSQLRestServerMethods; fPublishedMethods: TDynArrayHashed; fPublishedMethodTimestampIndex: integer; fPublishedMethodAuthIndex: integer; fPublishedMethodBatchIndex: integer; fPublicURI: TSQLRestServerURI; fAssociatedServices: TServicesPublishedInterfacesList; fStats: TSQLRestServerMonitor; fStatLevels: TSQLRestServerMonitorLevels; fStatUsage: TSynMonitorUsage; fShutdownRequested: boolean; fCreateMissingTablesOptions: TSQLInitializeTableOptions; fRootRedirectGet: RawUTF8; fSafeRootUpper: RawUTF8; fSafeProtocol: IProtocol; fSafeProtocolNext: IProtocol; fSafeProtocols: TSynDictionary; fRecordVersionMax: TRecordVersion; fRecordVersionDeleteIgnore: boolean; fOnIdleLastTix: cardinal; fSQLRecordVersionDeleteTable: TSQLRecordTableDeletedClass; fRecordVersionSlaveCallbacks: array of IServiceRecordVersionCallback; fIPBan, fIPWhiteJWT: TIPBan; // TSQLRecordHistory.ModifiedRecord handles up to 64 (=1 shl 6) tables fTrackChangesHistoryTableIndex: TIntegerDynArray; fTrackChangesHistoryTableIndexCount: cardinal; fTrackChangesHistory: array of record CurrentRow: integer; MaxSentDataJsonRow: integer; MaxRevisionJSON: integer; MaxUncompressedBlobSize: integer; end; constructor RegisteredClassCreateFrom(aModel: TSQLModel; aServerHandleAuthentication: boolean; aDefinition: TSynConnectionDefinition); reintroduce; virtual; function GetAuthenticationSchemesCount: integer; function GetCurrentSessionUserID: TID; override; // called by Stat() and Info() method-based services procedure InternalStat(Ctxt: TSQLRestServerURIContext; W: TTextWriter); virtual; procedure AddStat(Flags: TSQLRestServerAddStats; W: TTextWriter); {$ifndef NOVARIANTS} procedure InternalInfo(var info: TDocVariantData); virtual; {$endif} procedure SetStatUsage(usage: TSynMonitorUsage); function GetServiceMethodStat(const aMethod: RawUTF8): TSynMonitorInputOutput; /// fast get the associated static server, if any function GetStaticDataServer(aClass: TSQLRecordClass): TSQLRest; /// retrieve a TSQLRestStorage instance associated to a Virtual Table // - is e.g. TSQLRestStorageInMemory instance associated to a // TSQLVirtualTableBinary or TSQLVirtualTableJSON class // - may be a TSQLRestStorageExternal (as defined in mORMotDB unit) // for a virtual table giving access to an external database function GetVirtualTable(aClass: TSQLRecordClass): TSQLRest; /// fast get the associated static server or Virtual table, if any // - this can be used to call directly the TSQLRestStorage instance // on the server side // - same as a dual call to StaticDataServer[aClass] + StaticVirtualTable[aClass] // - TSQLRestServer.URI will make a difference between the a static server // or a TSQLVirtualTable, but this method won't - you can set a reference // to a TSQLRestServerKind variable to retrieve the database server type function GetStaticTable(aClass: TSQLRecordClass): TSQLRest; {$ifdef HASINLINE}inline;{$endif} /// overloaded method using table index in associated Model function GetStaticTableIndex(aTableIndex: integer): TSQLRest; overload; {$ifdef HASINLINE}inline;{$endif} function GetStaticTableIndex(aTableIndex: integer; out Kind: TSQLRestServerKind): TSQLRest; overload; {$ifdef HASINLINE}inline;{$endif} function GetRemoteTable(TableIndex: Integer): TSQLRest; function IsInternalSQLite3Table(aTableIndex: integer): boolean; /// intercepts all calls to TSQLRestServer.URI for fSafeProtocol // - e.g. ModelRoot/_safe_ URI called by the clients to implement a secure // custom encryption, by sending POST requests with an encrypted body as // BINARY_CONTENT_TYPE ('application/octet-stream') input and output procedure InternalSafeProtocol(var Call: TSQLRestURIParams; var SafeID: integer); /// retrieve a list of members as JSON encoded data - used by OneFieldValue() // and MultiFieldValue() public functions function InternalAdaptSQL(TableIndex: integer; var SQL: RawUTF8): TSQLRest; function InternalListRawUTF8(TableIndex: integer; const SQL: RawUTF8): RawUTF8; /// will retrieve the monotonic value of a TRecordVersion field from the DB procedure InternalRecordVersionMaxFromExisting(RetrieveNext: PID); virtual; procedure InternalRecordVersionDelete(TableIndex: integer; ID: TID; Batch: TSQLRestBatch); virtual; procedure InternalRecordVersionHandle(Occasion: TSQLOccasion; TableIndex: integer; var Decoder: TJSONObjectDecoder; RecordVersionField: TSQLPropInfoRTTIRecordVersion); virtual; /// will compute the next monotonic value for a TRecordVersion field // - you may override this method to customize the returned Int64 value // (e.g. to support several synchronization nodes) function InternalRecordVersionComputeNext: TRecordVersion; virtual; /// this method is overridden for setting the NoAJAXJSON field // of all associated TSQLRestStorage servers procedure SetNoAJAXJSON(const Value: boolean); virtual; function GetNoAJAXJSON: boolean; /// add a new session to the internal session list // - do not use this method directly: this callback is to be used by // TSQLRestServerAuthentication* classes // - will check that the logon name is valid // - on failure, will call TSQLRestServerURIContext.AuthenticationFailed() // with afSessionAlreadyStartedForThisUser or afSessionCreationAborted reason procedure SessionCreate(var User: TSQLAuthUser; Ctxt: TSQLRestServerURIContext; out Session: TAuthSession); virtual; /// search for Ctxt.Session ID and fill Ctxt.Session* members if found // - returns nil if not found, or fill aContext.User/Group values if matchs // - this method will also check for outdated sessions, and delete them // - this method is not thread-safe: caller should use Sessions.Lock/Unlock function SessionAccess(Ctxt: TSQLRestServerURIContext): TAuthSession; /// delete a session from its index in Sessions[] // - will perform any needed clean-up, and log the event // - this method is not thread-safe: caller should use Sessions.Lock/Unlock procedure SessionDelete(aSessionIndex: integer; Ctxt: TSQLRestServerURIContext); /// returns TRUE if this table is worth caching (e.g. already in memory) // - this overridden implementation returns FALSE for TSQLRestStorageInMemory function CacheWorthItForTable(aTableIndex: cardinal): boolean; override; /// overridden methods which will perform CRUD operations // - will call any static TSQLRestStorage, or call MainEngine*() virtual methods function EngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; override; function EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; override; function EngineList(const SQL: RawUTF8; ForceAJAX: Boolean=false; ReturnedRowCount: PPtrInt=nil): RawUTF8; override; function EngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; override; function EngineDelete(TableModelIndex: integer; ID: TID): boolean; override; function EngineDeleteWhere(TableModelIndex: integer; const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean; override; 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 EngineUpdateField(TableModelIndex: integer; const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; override; function EngineUpdateFieldIncrement(TableModelIndex: integer; ID: TID; const FieldName: RawUTF8; Increment: Int64): boolean; override; function EngineBatchSend(Table: TSQLRecordClass; var Data: RawUTF8; var Results: TIDDynArray; ExpectedResultsCount: integer): integer; override; /// virtual methods which will perform CRUD operations on the main DB function MainEngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; virtual; abstract; function MainEngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; virtual; abstract; function MainEngineList(const SQL: RawUTF8; ForceAJAX: Boolean; ReturnedRowCount: PPtrInt): RawUTF8; virtual; abstract; function MainEngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; virtual; abstract; function MainEngineDelete(TableModelIndex: integer; ID: TID): boolean; virtual; abstract; function MainEngineDeleteWhere(TableModelIndex: integer; const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean; virtual; abstract; function MainEngineRetrieveBlob(TableModelIndex: integer; aID: TID; BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean; virtual; abstract; function MainEngineUpdateBlob(TableModelIndex: integer; aID: TID; BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean; virtual; abstract; function MainEngineUpdateField(TableModelIndex: integer; const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; virtual; abstract; function MainEngineUpdateFieldIncrement(TableModelIndex: integer; ID: TID; const FieldName: RawUTF8; Increment: Int64): boolean; virtual; abstract; public /// this integer property is incremented by the database engine when any SQL // statement changes the database contents (i.e. on any not SELECT statement) // - its value can be published to the client on every remote request // - it may be used by client to avoid retrieve data only if necessary // - if its value is 0, this feature is not activated on the server, and the // client must ignore it and always retrieve the content InternalState: Cardinal; /// a method can be specified here to trigger events after any table update // - is called BEFORE deletion, and AFTER insertion or update // - note that the aSentData parameter does not contain all record fields, // but only transmitted information: e.g. if only one field is updated, only // this single field (and the ID) is available // - to be used only server-side, not to synchronize some clients: the framework // is designed around a stateless RESTful architecture (like HTTP/1.1), in which // clients ask the server for refresh (see TSQLRestClientURI.UpdateFromServer) OnUpdateEvent: TNotifySQLEvent; /// a method can be specified here to trigger events after any blob update // - is called AFTER update of one or several blobs, never on delete nor insert // - to be used only server-side, not to synchronize some clients: the framework // is designed around a stateless RESTful architecture (like HTTP/1.1), in which // clients ask the server for refresh (see TSQLRestClientURI.UpdateFromServer) OnBlobUpdateEvent: TNotifyFieldSQLEvent; /// a method can be specified to be notified when a session is created // - for OnSessionCreate, returning TRUE will abort the session creation - // and you can set Ctxt.Call^.OutStatus to a corresponding error code // - it could be used e.g. to limit the number of client sessions OnSessionCreate: TNotifySQLSession; /// a custom method to retrieve the TSQLAuthUser instance for authentication // - will be called by TSQLRestServerAuthentication.GetUser() instead of // plain SQLAuthUserClass.Create() OnAuthenticationUserRetrieve: TOnAuthenticationUserRetrieve; /// this event handler will be executed when a session failed to initialize // (DenyOfService attack?) or the request is not valid (ManIntheMiddle attack?) // - e.g. if the URI signature is invalid, or OnSessionCreate event handler // aborted the session creation by returning TRUE (in this later case, // the Session parameter is not nil) // - you can access the current execution context from the Ctxt parameter, // e.g. to retrieve the caller's IP and ban aggressive users in Ctxt.RemoteIP // or the text error message corresponding to Reason in Ctxt.CustomErrorMsg OnAuthenticationFailed: TNotifyAuthenticationFailed; /// a method can be specified to be notified when a session is closed // - for OnSessionClosed, the returning boolean value is ignored // - Ctxt is nil if the session is closed due to a timeout // - Ctxt is not nil if the session is closed explicitly by the client OnSessionClosed: TNotifySQLSession; /// this event will be executed to push notifications from the server to // a remote client, using a (fake) interface parameter // - is nil by default, but may point e.g. to TSQLHttpServer.NotifyCallback OnNotifyCallback: TSQLRestServerNotifyCallback; /// this event will be executed by TServiceFactoryServer.CreateInstance // - you may set a callback to customize a server-side service instance, // i.e. inject class-level dependencies: // !procedure TMyClass.OnCreateInstance( // ! Sender: TServiceFactoryServer; Instance: TInterfacedObject); // !begin // ! if Sender.ImplementationClass=TLegacyStockQuery then // ! TLegacyStockQuery(Instance).fDbConnection := fDbConnection; // !end; // - consider using a TInjectableObjectClass implementation for pure IoC/DI OnServiceCreateInstance: TOnServiceCreateInstance; /// event trigerred when URI() starts to process a request // - the supplied Ctxt parameter will give access to the command about to // be executed, e.g. Ctxt.Command=execSOAByInterface will identify a SOA // service execution, with the corresponding Service and ServiceMethodIndex // parameters as set by TSQLRestServerURIContext.URIDecodeSOAByInterface // - should return TRUE if the method can be executed // - should return FALSE if the method should not be executed, and the // callback should set the corresponding error to the supplied context e.g. // ! Ctxt.Error('Unauthorized method',HTTP_NOTALLOWED); // - since this event will be executed by every TSQLRestServer.URI call, // it should better not make any slow process (like writing to a remote DB) // - see also TSQLRest.OnDecryptBody, which is common to the client side, so // may be a better place for implementing shared process (e.g. encryption) OnBeforeURI: TNotifyBeforeURI; /// event trigerred when URI() finished to process a request // - the supplied Ctxt parameter will give access to the command which has // been executed, e.g. via Ctxt.Call.OutStatus or Ctxt.MicroSecondsElapsed // - since this event will be executed by every TSQLRestServer.URI call, // it should better not make any slow process (like writing to a remote DB) // - see also TSQLRest.OnDecryptBody/OnEncryptBody, which is common to the // client side, so may be better to implement shared process (e.g. encryption) OnAfterURI: TNotifyAfterURI; /// event trigerred when URI() failed to process a request // - if Ctxt.ExecuteCommand raised an execption, this callback will be // run with all neeed information // - should return TRUE to execute Ctxt.Error(E,...), FALSE if returned // content has already been set as expected by the client OnErrorURI: TNotifyErrorURI; {$ifndef NOVARIANTS} /// event to customize the information returned by root/timestamp/info // - called by TSQLRestServer.InternalInfo method // - you can add some application-level information for monitoring OnInternalInfo: TOnInternalInfo; {$endif} /// event trigerred when URI() is called, and at least 128 ms is elapsed // - could be used to execute some additional process after a period of time // - note that if TSQLRestServer.URI is not called by any client, this // callback won't be executed either OnIdle: TNotifyEvent; /// this property can be used to specify the URI parmeters to be used // for query paging // - is set by default to PAGINGPARAMETERS_YAHOO constant by // TSQLRestServer.Create() constructor URIPagingParameters: TSQLRestServerURIPagingParameters; /// implement Server-Side TSQLRest deletion // - uses internally EngineDelete() function for calling the database engine // - call corresponding fStaticData[] if necessary // - this record is also erased in all available TRecordReference properties // in the database Model, for relational database coherency function Delete(Table: TSQLRecordClass; ID: TID): boolean; override; /// implement Server-Side TSQLRest deletion with a WHERE clause // - will process all ORM-level validation, coherency checking and // notifications together with a low-level SQL deletion work (if possible) function Delete(Table: TSQLRecordClass; const SQLWhere: RawUTF8): boolean; override; /// overridden method for direct static class call (if any) function TableRowCount(Table: TSQLRecordClass): Int64; override; /// overridden method for direct static class call (if any) function TableHasRows(Table: TSQLRecordClass): boolean; override; /// overridden method for direct static class call (if any) function MemberExists(Table: TSQLRecordClass; ID: TID): boolean; override; /// virtual method called when a record is updated // - default implementation will call the OnUpdateEvent/OnBlobUpdateEvent // methods, if defined // - will also handle TSQLRecordHistory tables, as defined by TrackChanges() // - returns true on success, false if an error occured (but action must continue) // - you can override this method to implement a server-wide notification, // but be aware it may be the first step to break the stateless architecture // of the framework function InternalUpdateEvent(aEvent: TSQLEvent; aTableIndex: integer; aID: TID; const aSentData: RawUTF8; aIsBlobFields: PSQLFieldBits): boolean; virtual; /// initialize change tracking for the given tables // - by default, it will use the TSQLRecordHistory table to store the // changes - you can specify a dedicated class as aTableHistory parameter // - if aTableHistory is not already part of the TSQLModel, it will be added // - note that this setting should be consistent in time: if you disable // tracking for a while, or did not enable tracking before adding a record, // then the content history won't be consistent (or disabled) for this record // - at every change, aTableHistory.SentDataJSON records will be added, up // to aMaxHistoryRowBeforeBlob items - then aTableHistory.History will store // a compressed version of all previous changes // - aMaxHistoryRowBeforeBlob is the maximum number of JSON rows per Table // before compression into BLOB is triggerred // - aMaxHistoryRowPerRecord is the maximum number of JSON rows per record, // above which the versions will be compressed as BLOB // - aMaxUncompressedBlobSize is the maximum BLOB size per record // - you can specify aMaxHistoryRowBeforeBlob=0 to disable change tracking // - you should call this method after the CreateMissingTables call // - note that change tracking may slow down the writing process, and // may increase storage space a lot (even if BLOB maximum size can be set), // so should be defined only when necessary procedure TrackChanges(const aTable: array of TSQLRecordClass; aTableHistory: TSQLRecordHistoryClass=nil; aMaxHistoryRowBeforeBlob: integer=1000; aMaxHistoryRowPerRecord: integer=10; aMaxUncompressedBlobSize: integer=64*1024); virtual; /// force compression of all aTableHistory.SentDataJson into History BLOB // - by default, this will take place in InternalUpdateEvent() when // aMaxHistoryRowBeforeBlob - as set by TrackChanges() method - is reached // - you can manually call this method to force History BLOB update, e.g. // when the server is in Idle state, and ready for process procedure TrackChangesFlush(aTableHistory: TSQLRecordHistoryClass); virtual; /// check if OnUpdateEvent or change tracked has been defined for this table // - is used internally e.g. by TSQLRestServerDB.MainEngineUpdateField to // ensure that the updated ID fields will be computed as expected function InternalUpdateEventNeeded(aTableIndex: integer): boolean; /// will compute the next monotonic value for a TRecordVersion field function RecordVersionCompute: TRecordVersion; /// read only access to the current monotonic value for a TRecordVersion field function RecordVersionCurrent: TRecordVersion; /// synchronous master/slave replication from a slave TSQLRest // - apply all the updates from another (distant) master TSQLRest for a given // TSQLRecord table, using its TRecordVersion field, to the calling slave // - both remote Master and local slave TSQLRestServer should have the supplied // Table class in their data model (maybe in diverse order) // - by default, all pending updates are retrieved, but you can define a value // to ChunkRowLimit, so that the updates will be retrieved by smaller chunks // - returns -1 on error, or the latest applied revision number (which may // be 0 if there is no data in the table) // - this method will use regular REST ORM commands, so will work with any // communication channels: for real-time push synchronization, consider using // RecordVersionSynchronizeMasterStart and RecordVersionSynchronizeSlaveStart // over a bidirectionnal communication channel like WebSockets // - you can use RecordVersionSynchronizeSlaveToBatch if your purpose is // to access the updates before applying to the current slave storage function RecordVersionSynchronizeSlave(Table: TSQLRecordClass; Master: TSQLRest; ChunkRowLimit: integer=0; OnWrite: TOnBatchWrite=nil): TRecordVersion; /// synchronous master/slave replication from a slave TSQLRest into a Batch // - will retrieve all the updates from a (distant) master TSQLRest for a // given TSQLRecord table, using its TRecordVersion field, and a supplied // TRecordVersion monotonic value, into a TSQLRestBatch instance // - both remote Source and local TSQLRestSever should have the supplied // Table class in each of their data model // - by default, all pending updates are retrieved, but you can define a value // to MaxRowLimit, so that the updates will be retrieved by smaller chunks // - returns nil if nothing new was found, or a TSQLRestBatch instance // containing all modifications since RecordVersion revision // - when executing the returned TSQLRestBatch on the database, you should // set TSQLRestServer.RecordVersionDeleteIgnore := true so that the // TRecordVersion fields will be forced from the supplied value // - usually, you should not need to use this method, but rather the more // straightforward RecordVersionSynchronizeSlave() function RecordVersionSynchronizeSlaveToBatch(Table: TSQLRecordClass; Master: TSQLRest; var RecordVersion: TRecordVersion; MaxRowLimit: integer=0; OnWrite: TOnBatchWrite=nil): TSQLRestBatch; virtual; /// initiate asynchronous master/slave replication on a master TSQLRest // - allow synchronization of a TSQLRecord table, using its TRecordVersion // field, for real-time master/slave replication on the master side // - this method will register the IServiceRecordVersion service on the // server side, so that RecordVersionSynchronizeStartSlave() will be able // to receive push notifications of any updates // - this method expects the communication channel to be bidirectional, e.g. // a mORMotHTTPServer's TSQLHttpServer in useBidirSocket mode function RecordVersionSynchronizeMasterStart(ByPassAuthentication: boolean=false): boolean; /// initiate asynchronous master/slave replication on a slave TSQLRest // - start synchronization of a TSQLRecord table, using its TRecordVersion // field, for real-time master/slave replication on the slave side // - this method will first retrieve any pending modification by regular // REST calls to RecordVersionSynchronizeSlave, then create and register a // callback instance using RecordVersionSynchronizeSubscribeMaster() // - this method expects the communication channel to be bidirectional, e.g. // a TSQLHttpClientWebsockets // - the modifications will be pushed by the master, then applied to the // slave storage, until RecordVersionSynchronizeSlaveStop method is called // - an optional OnNotify event may be defined, which will be triggered // for all incoming change, supllying the updated TSQLRecord instance function RecordVersionSynchronizeSlaveStart(Table: TSQLRecordClass; MasterRemoteAccess: TSQLRestClientURI; OnNotify: TOnBatchWrite=nil): boolean; /// finalize asynchronous master/slave replication on a slave TSQLRest // - stop synchronization of a TSQLRecord table, using its TRecordVersion // field, for real-time master/slave replication on the slave side // - expect a previous call to RecordVersionSynchronizeSlaveStart function RecordVersionSynchronizeSlaveStop(Table: TSQLRecordClass): boolean; /// low-level callback registration for asynchronous master/slave replication // - you should not have to use this method, but rather // RecordVersionSynchronizeMasterStart and RecordVersionSynchronizeSlaveStart // RecordVersionSynchronizeSlaveStop methods // - register a callback interface on the master side, which will be called // each time a write operation is performed on a given TSQLRecord with a // TRecordVersion field // - the callback parameter could be a TServiceRecordVersionCallback instance, // which will perform all update operations as expected // - the callback process will be blocking for the ORM write point of view: // so it should be as fast as possible, or asynchronous - note that regular // callbacks using WebSockets, as implemented by SynBidirSock.pas and // mORMotHTTPServer's TSQLHttpServer in useBidirSocket mode, are asynchronous // - if the supplied RecordVersion is not the latest on the server side, // this method will return FALSE and the caller should synchronize again via // RecordVersionSynchronize() to avoid any missing update // - if the supplied RecordVersion is the latest on the server side, // this method will return TRUE and put the Callback notification in place function RecordVersionSynchronizeSubscribeMaster(Table: TSQLRecordClass; RecordVersion: TRecordVersion; const SlaveCallback: IServiceRecordVersionCallback): boolean; overload; /// this method is called internally after any successfull deletion to // ensure relational database coherency // - reset all matching TRecordReference properties in the database Model, // for database coherency, into 0 // - delete all records containing a matched TRecordReferenceToBeDeleted // property value in the database Model (e.g. TSQLRecordHistory) // - reset all matching TSQLRecord properties in the database Model, // for database coherency, into 0 // - important notice: we don't use FOREIGN KEY constraints in this framework, // and handle all integrity check within this method (it's therefore less // error-prone, and more cross-database engine compatible) function AfterDeleteForceCoherency(aTableIndex: integer; aID: TID): boolean; virtual; /// update all BLOB fields of the supplied Value // - this overridden method will execute the direct static class, if any function UpdateBlobFields(Value: TSQLRecord): boolean; override; /// get all BLOB fields of the supplied value from the remote server // - this overridden method will execute the direct static class, if any function RetrieveBlobFields(Value: TSQLRecord): boolean; override; /// implement Server-Side TSQLRest unlocking // - to be called e.g. after a Retrieve() with forupdate=TRUE // - implements our custom UNLOCK REST-like verb // - locking is handled by TSQLServer.Model // - returns true on success function UnLock(Table: TSQLRecordClass; aID: TID): boolean; override; /// end a transaction // - implements REST END collection // - write all pending TSQLVirtualTableJSON data to the disk procedure Commit(SessionID: cardinal; RaiseException: boolean); override; /// grant access to this database content from a dll using the global // URIRequest() function // - returns true if the URIRequest() function is set to this TSQLRestServer // - returns false if a TSQLRestServer was already exported // - client must release all memory acquired by URIRequest() with GlobalFree() function ExportServer: boolean; overload; {$ifdef MSWINDOWS} /// declare the server on the local machine as a Named Pipe: allows // TSQLRestClientURINamedPipe local or remote client connection // - ServerApplicationName ('DBSERVER' e.g.) will be used to create a named // pipe server identifier, it is of UnicodeString type since Delphi 2009 // (use of Unicode FileOpen() version) // - this server identifier is appended to '\\.\pipe\mORMot_' to obtain // the full pipe name to initiate ('\\.\pipe\mORMot_DBSERVER' e.g.) // - this server identifier may also contain a fully qualified path // ('\\.\pipe\ApplicationName' e.g.) // - allows only one ExportServer*() by running process // - returns true on success, false otherwise (ServerApplicationName already used?) function ExportServerNamedPipe(const ServerApplicationName: TFileName): boolean; /// end any currently initialized named pipe server function CloseServerNamedPipe: boolean; /// declare the server on the local machine to be accessible for local // client connection, by using Windows messages // - the data is sent and received by using the standard and fast WM_COPYDATA message // - Windows messages are very fast (faster than named pipe and much faster // than HTTP), but only work localy on the same computer // - create a new Window Class with the supplied class name (UnicodeString // since Delphi 2009 for direct use of Wide Win32 API), and instanciate // a window which will handle pending WM_COPYDATA messages // - the main server instance has to process the windows messages regularely // (e.g. with Application.ProcessMessages) // - ServerWindowName ('DBSERVER' e.g.) will be used to create a // Window name identifier // - allows only one ExportServer*() by running process // - returns true on success, false otherwise (ServerWindowName already used?) function ExportServerMessage(const ServerWindowName: string): boolean; /// implement a message-based server response // - this method is called automaticaly if ExportServerMessage() method // was initilialized // - you can also call this method from the WM_COPYDATA message handler // of your main form, and use the TSQLRestClientURIMessage class to access // the server instance from your clients // - it will answer to the Client with another WM_COPYDATA message // - message oriented architecture doesn't need any thread, but will use // the main thread of your application procedure AnswerToMessage(var Msg: TWMCopyData); message WM_COPYDATA; /// end any currently initialized message-oriented server function CloseServerMessage: boolean; /// returns TRUE if remote connection is possible via named pipes or Windows // messages function ExportedAsMessageOrNamedPipe: Boolean; {$endif} /// Server initialization with a specified Database Model // - if HandleUserAuthentication is false, will set URI access rights to // 'Supervisor' (i.e. all R/W access) by default // - if HandleUserAuthentication is true, will add TSQLAuthUser and // TSQLAuthGroup to the TSQLModel (if not already there) constructor Create(aModel: TSQLModel; aHandleUserAuthentication: boolean=false); reintroduce; virtual; /// Server initialization with a temporary Database Model // - a Model will be created with supplied tables, and owned by the server // - if you instantiate a TSQLRestServerFullMemory or TSQLRestServerDB // with this constructor, an in-memory engine will be created, with // enough abilities to run regression tests, for instance constructor CreateWithOwnModel(const Tables: array of TSQLRecordClass; aHandleUserAuthentication: boolean=false; const aRoot: RawUTF8='root'); /// create a new minimal TSQLRestServer instance, to be used with // external SQL or NoSQL storage // - will try to instantiate an in-memory TSQLRestServerDB, and if // mORMotSQLite3.pas is not linked, fallback to a TSQLRestServerFullMemory // - used e.g. by TSQLRestMongoDBCreate() and TSQLRestExternalDBCreate() class function CreateInMemoryForAllVirtualTables(aModel: TSQLModel; aHandleUserAuthentication: boolean): TSQLRestServer; /// release memory and any existing pipe initialized by ExportServer() destructor Destroy; override; /// you can call this method to prepare the server for shutting down // - it will reject any incoming request from now on, and will wait until // all pending requests are finished, for proper server termination // - you could optionally save the current server state (e.g. user sessions) // into a file, ready to be retrieved later on using SessionsLoadFromFile - // note that this will work only for ORM sessions, NOT complex SOA state // - this method is called by Destroy itself procedure Shutdown(const aStateFileName: TFileName=''); virtual; /// wait for the specified number of milliseconds // - if Shutdown is called in-between, returns true // - if the thread waited the supplied time, returns false function SleepOrShutdown(MS: integer): boolean; /// Missing tables are created if they don't exist yet for every TSQLRecord // class of the Database Model // - you must call explicitely this before having called StaticDataCreate() // - all table description (even Unique feature) is retrieved from the Model // - this method should also create additional fields, if the TSQLRecord definition // has been modified; only field adding is mandatory, field renaming or // field deleting are not allowed in the FrameWork (in such cases, you must // create a new TSQLRecord type) // - this virtual method do nothing by default - overridden versions should // implement it as expected by the underlying storage engine (e.g. SQLite3 // or TSQLRestServerFullInMemory) // - you can tune some options transmitted to the TSQLRecord.InitializeTable // virtual methods, e.g. to avoid the automatic create of indexes procedure CreateMissingTables(user_version: cardinal=0; options: TSQLInitializeTableOptions=[]); virtual; /// run the TSQLRecord.InitializeTable methods for all void tables of the model // - can be used instead of CreateMissingTables e.g. for MongoDB storage // - you can specify the creation options, e.g. INITIALIZETABLE_NOINDEX procedure InitializeTables(Options: TSQLInitializeTableOptions); /// create an external static in-memory database for a specific class // - call it just after Create, before TSQLRestServerDB.CreateMissingTables; // warning: if you don't call this method before CreateMissingTable method // is called, the table will be created as a regular table by the main // database engine, and won't be static // - can load the table content from a file if a file name is specified // (could be either JSON or compressed Binary format on disk) // - you can define a particular external engine by setting a custom class - // by default, it will create a TSQLRestStorageInMemory instance // - this data handles basic REST commands, since no complete SQL interpreter // can be implemented by TSQLRestStorage; to provide full SQL process, // you should better use a Virtual Table class, inheriting e.g. from // TSQLRecordVirtualTableAutoID associated with TSQLVirtualTableJSON/Binary // via a Model.VirtualTableRegister() call before TSQLRestServer.Create // - return nil on any error, or an EModelException if the class is not in // the database model function StaticDataCreate(aClass: TSQLRecordClass; const aFileName: TFileName = ''; aBinaryFile: boolean=false; aServerClass: TSQLRestStorageInMemoryClass=nil): TSQLRestStorage; /// register an external static storage for a given table // - will be added to StaticDataServer[] internal list // - called e.g. by StaticDataCreate(), RemoteDataCreate() or // StaticMongoDBRegister() function StaticDataAdd(aStaticData: TSQLRestStorage): boolean; /// create an external static redirection for a specific class // - call it just after Create, before TSQLRestServerDB.CreateMissingTables; // warning: if you don't call this method before CreateMissingTable method // is called, the table will be created as a regular table by the main // database engine, and won't be static // - the specified TSQLRecord will have all its CRUD / ORM methods be // redirected to aRemoteRest, which may be a TSQLRestClient or another // TSQLRestServer instance (e.g. a fast SQLITE_MEMORY_DATABASE_NAME) // - if aRemoteRest is a TSQLRestClient, it should have been authenticated // to the remote TSQLRestServer, so that CRUD / ORM operations will pass // - this will enable easy creation of proxies, or local servers, with they // own cache and data model - e.g. a branch office server which may serve // its local clients over Ethernet, but communicating to a main mORMot // server via Internet, storing the corporate data in the main office server // - you may also share some tables (e.g. TSQLAuthUser and TSQLAuthGroup) // between TSQLRestServer instances in a single service function RemoteDataCreate(aClass: TSQLRecordClass; aRemoteRest: TSQLRest): TSQLRestStorageRemote; virtual; /// call this method when the internal DB content is known to be invalid // - by default, all REST/CRUD requests and direct SQL statements are // scanned and identified as potentially able to change the internal SQL/JSON // cache used at SQLite3 database level; but some virtual tables (e.g. // TSQLRestStorageExternal classes defined in mORMotDB) could flush // the database content without proper notification // - this default implementation will just do nothing, but mORMotSQlite3 // unit will call TSQLDataBase.CacheFlush method procedure FlushInternalDBCache; virtual; /// you can call this method in TThread.Execute to ensure that // the thread will be taken into account during process // - caller must specify the TThread instance running // - used e.g. for optExecInMainThread option in TServiceMethodExecute // - this default implementation will call the methods of all its internal // TSQLRestStorage instances // - this method shall be called from the thread just initiated: e.g. // if you call it from the main thread, it may fail to prepare resources procedure BeginCurrentThread(Sender: TThread); override; /// you can call this method just before a thread is finished to ensure // e.g. that the associated external DB connection will be released // - this default implementation will call the methods of all its internal // TSQLRestStorage instances, allowing e.g. TSQLRestStorageExternal // instances to clean their thread-specific connections // - this method shall be called from the thread about to be terminated: e.g. // if you call it from the main thread, it may fail to release resources // - it is set e.g. by TSQLite3HttpServer to be called from HTTP threads, // or by TSQLRestServerNamedPipeResponse for named-pipe server cleaning procedure EndCurrentThread(Sender: TThread); override; /// implement a generic local, piped or HTTP/1.1 provider // - this is the main entry point of the server, from the client side // - default implementation calls protected methods EngineList() Retrieve() // Add() Update() Delete() UnLock() EngineExecute() above, which must be overridden by // the TSQLRestServer child // - for 'GET ModelRoot/TableName', url parameters can be either "select" and // "where" (to specify a SQL Query, from the SQLFromSelectWhere function), // either "sort", "dir", "startIndex", "results", as expected by the YUI // DataSource Request Syntax for data pagination - see // http://developer.yahoo.com/yui/datatable/#data // - execution of this method could be monitored via OnBeforeURI and OnAfterURI // event handlers procedure URI(var Call: TSQLRestURIParams); virtual; /// create an index for the specific FieldName // - will call CreateSQLMultiIndex() internaly function CreateSQLIndex(Table: TSQLRecordClass; const FieldName: RawUTF8; Unique: boolean; const IndexName: RawUTF8=''): boolean; overload; /// create one or multiple index(es) for the specific FieldName(s) function CreateSQLIndex(Table: TSQLRecordClass; const FieldNames: array of RawUTF8; Unique: boolean): boolean; overload; /// create one index for all specific FieldNames at once // - will call any static engine for the index creation of such tables, or // execute a CREATE INDEX IF NOT EXISTS on the main engine // - note that with SQLite3, your database schema should never contain two // indices where one index is a prefix of the other, e.g. if you defined: // ! aServer.CreateSQLMultiIndex(TEmails, ['Email','GroupID'], True); // Then the following index is not mandatory for SQLite3: // ! aServer.CreateSQLIndex(TEmails, 'Email', False); // see "1.6 Multi-Column Indices" in @http://www.sqlite.org/queryplanner.html function CreateSQLMultiIndex(Table: TSQLRecordClass; const FieldNames: array of RawUTF8; Unique: boolean; IndexName: RawUTF8=''): boolean; virtual; /// call this method to add an authentication method to the server // - will return the just created TSQLRestServerAuthentication instance, // or the existing instance if it has already been registered // - you can use this method to tune the authentication, e.g. if you have // troubles with AJAX asynchronous callbacks: // ! (aServer.AuthenticationRegister(TSQLRestServerAuthenticationDefault) as // ! TSQLRestServerAuthenticationDefault).NoTimestampCoherencyCheck := true; // or if you want to customize the session_signature parameter algorithm: // ! (aServer.AuthenticationRegister(TSQLRestServerAuthenticationDefault) as // ! TSQLRestServerAuthenticationDefault).Algorithm := suaMD5; function AuthenticationRegister( aMethod: TSQLRestServerAuthenticationClass): TSQLRestServerAuthentication; overload; /// call this method to add several authentication methods to the server // - if TSQLRestServer.Create() constructor is called with aHandleUserAuthentication // set to TRUE, it will register the two following classes: // ! AuthenticationRegister([TSQLRestServerAuthenticationDefault,TSQLRestServerAuthenticationSSPI]); procedure AuthenticationRegister(const aMethods: array of TSQLRestServerAuthenticationClass); overload; /// call this method to remove an authentication method to the server procedure AuthenticationUnregister(aMethod: TSQLRestServerAuthenticationClass); overload; /// call this method to remove several authentication methods to the server procedure AuthenticationUnregister(const aMethods: array of TSQLRestServerAuthenticationClass); overload; /// call this method to remove all authentication methods to the server procedure AuthenticationUnregisterAll; /// (un)register a banned IPv4 value // - any connection attempt from this IP Address will be rejected by function BanIP(const aIP: RawUTF8; aRemoveBan: boolean=false): boolean; /// (un)register a an IPv4 value to the JWT white list // - by default, a JWT validated by JWTForUnauthenticatedRequest will be accepted // - to avoid MiM (Man-In-the-Middle) attacks, if a JWT white list is defined // using this method, any connection from a non registered IP will be rejected, // even with a valid JWT // - WebSockets connections are secure enough to bypass this list function JWTForUnauthenticatedRequestWhiteIP(const aIP: RawUTF8; aRemoveWhite: boolean=false): boolean; /// add all published methods of a given object instance to the method-based // list of services // - all those published method signature should match TSQLRestServerCallBack procedure ServiceMethodRegisterPublishedMethods(const aPrefix: RawUTF8; aInstance: TObject); /// direct registration of a method for a given low-level event handler procedure ServiceMethodRegister(aMethodName: RawUTF8; const aEvent: TSQLRestServerCallBack; aByPassAuthentication: boolean=false); /// call this method to disable Authentication method check for a given // published method-based service name // - by default, only Auth and Timestamp methods do not require the RESTful // authentication of the URI; you may call this method to add another method // to the list (e.g. for returning some HTML content from a public URI) // - if the supplied aMethodName='', all method-based services will // bypass the authenticaton process // - returns the method index number function ServiceMethodByPassAuthentication(const aMethodName: RawUTF8): integer; /// retrieve detailed statistics about a method-based service use // - will return a reference to the actual alive item: caller should // not free the returned instance property ServiceMethodStat[const aMethod: RawUTF8]: TSynMonitorInputOutput read GetServiceMethodStat; /// used e.g. by IAdministratedDaemon to implement "pseudo-SQL" commands procedure AdministrationExecute(const DatabaseName,SQL: RawUTF8; var result: TServiceCustomAnswer); override; /// compute a JSON description of all available services, and its public URI // - the JSON object matches the TServicesPublishedInterfaces record type // - used by TSQLRestClientURI.ServicePublishOwnInterfaces to register all // the services supported by the client itself // - warning: the public URI should have been set via SetPublicURI() function ServicesPublishedInterfaces: RawUTF8; /// the HTTP server should call this method so that ServicesPublishedInterfaces // registration will be able to work procedure SetPublicURI(const Address,Port: RawUTF8); /// a list of the services associated by all clients of this server instance // - when a client connects to this server, it will publish its own services // (when checking its interface contract), so that they may be identified property AssociatedServices: TServicesPublishedInterfacesList read fAssociatedServices; /// returns a copy of the user associated to a session ID // - returns nil if the session does not exist (e.g. if authentication is // disabled) // - caller MUST release the TSQLAuthUser instance returned (if not nil) // - this method IS thread-safe, and calls internaly Sessions.Lock // (the returned TSQLAuthUser is a private copy from Sessions[].User instance, // in order to be really thread-safe) // - the returned TSQLAuthUser instance will have GroupRights=nil but will // have ID, LogonName, DisplayName, PasswordHashHexa and Data fields available function SessionGetUser(aSessionID: Cardinal): TSQLAuthUser; /// persist all in-memory sessions into a compressed binary file // - you should not call this method it directly, but rather use Shutdown() // with a StateFileName parameter - to be used e.g. for a short maintainance // server shutdown, without loosing the current logged user sessions // - this method IS thread-safe, and call internaly Sessions.Lock procedure SessionsSaveToFile(const aFileName: TFileName); /// re-create all in-memory sessions from a compressed binary file // - typical use is after a server restart, with the file supplied to the // Shutdown() method: it could be used e.g. for a short maintainance server // shutdown, without loosing the current logged user sessions // - WARNING: this method will restore authentication sessions for the ORM, // but not any complex state information used by interface-based services, // like sicClientDriven class instances - DO NOT use this feature with SOA // - this method IS thread-safe, and call internaly Sessions.Lock procedure SessionsLoadFromFile(const aFileName: TFileName; andDeleteExistingFileAfterRead: boolean); /// retrieve all current session information as a JSON array function SessionsAsJson: RawJSON; /// register a Service class on the server side // - this methods expects a class to be supplied, and the exact list of // interfaces to be registered to the server (e.g. [TypeInfo(IMyInterface)]) // and implemented by this class // - class can be any TInterfacedObject, but TInterfacedObjectWithCustomCreate // can be used if you need an overridden constructor // - instance implementation pattern will be set by the appropriate parameter // - will return the first of the registered TServiceFactoryServer created // on success (i.e. the one corresponding to the first item of the aInterfaces // array), or nil if registration failed (e.g. if any of the supplied interfaces // is not implemented by the given class) // - you can use the returned TServiceFactoryServer instance to set the // expected security parameters associated with this interface // - the same implementation class can be used to handle several interfaces // (just as Delphi allows to do natively) function ServiceRegister(aImplementationClass: TInterfacedClass; const aInterfaces: array of PTypeInfo; aInstanceCreation: TServiceInstanceImplementation=sicSingle; const aContractExpected: RawUTF8=''): TServiceFactoryServer; overload; virtual; /// register a Service instance on the server side // - this methods expects a class instance to be supplied, and the exact list // of interfaces to be registered to the server (e.g. [TypeInfo(IMyInterface)]) // and implemented by this shared instance // - as a consequence, instance implementation pattern will always be sicShared // - will return the first of the registered TServiceFactoryServer created // on success (i.e. the one corresponding to the first item of the aInterfaces // array), or nil if registration failed (e.g. if any of the supplied interfaces // is not implemented by the given class) // - you can use the returned TServiceFactoryServer instance to set the // expected security parameters associated with this interface // - the same implementation class can be used to handle several interfaces // (just as Delphi allows to do natively) function ServiceRegister(aSharedImplementation: TInterfacedObject; const aInterfaces: array of PTypeInfo; const aContractExpected: RawUTF8=''): TServiceFactoryServer; overload; virtual; /// register a remote Service via its interface // - this overloaded method will register a remote Service, accessed via the // supplied TSQLRest(ClientURI) instance: it can be available in the main // TSQLRestServer.Services property, but execution will take place on a // remote server - may be used e.g. for dedicated hosting of services (in // a DMZ for instance) // - this methods expects a list of interfaces to be registered to the client // (e.g. [TypeInfo(IMyInterface)]) // - instance implementation pattern will be set by the appropriate parameter // - will return true on success, false if registration failed (e.g. if any of // the supplied interfaces is not correct or is not available on the server) // - that is, server side will be called to check for the availability of // each interface // - you can specify an optional custom contract for the first interface function ServiceRegister(aClient: TSQLRest; const aInterfaces: array of PTypeInfo; aInstanceCreation: TServiceInstanceImplementation=sicSingle; const aContractExpected: RawUTF8=''): boolean; overload; virtual; /// register a Service class on the server side // - this method expects the interface(s) to have been registered previously: // ! TInterfaceFactory.RegisterInterfaces([TypeInfo(IMyInterface),...]); function ServiceDefine(aImplementationClass: TInterfacedClass; const aInterfaces: array of TGUID; aInstanceCreation: TServiceInstanceImplementation=sicSingle; const aContractExpected: RawUTF8=''): TServiceFactoryServer; overload; /// register a Service instance on the server side // - this method expects the interface(s) to have been registered previously: // ! TInterfaceFactory.RegisterInterfaces([TypeInfo(IMyInterface),...]); // - the supplied aSharedImplementation will be owned by this Server instance function ServiceDefine(aSharedImplementation: TInterfacedObject; const aInterfaces: array of TGUID; const aContractExpected: RawUTF8=''): TServiceFactoryServer; overload; /// register a remote Service via its interface // - this method expects the interface(s) to have been registered previously: // ! TInterfaceFactory.RegisterInterfaces([TypeInfo(IMyInterface),...]); function ServiceDefine(aClient: TSQLRest; const aInterfaces: array of TGUID; aInstanceCreation: TServiceInstanceImplementation=sicSingle; const aContractExpected: RawUTF8=''): boolean; overload; /// access or initialize the internal IoC resolver, used for interface-based // remote services, and more generaly any Services.Resolve() call // - create and initialize the internal TServiceContainerServer if no // service interface has been registered yet // - may be used to inject some dependencies, which are not interface-based // remote services, but internal IoC, without the ServiceRegister() // or ServiceDefine() methods - e.g. // ! aRest.ServiceContainer.InjectResolver([TInfraRepoUserFactory.Create(aRest)],true); // - this overriden method will return a TServiceContainerServer instance // - you may enable SOA audit trail for all methods execution: // ! (aRestSOAServer.ServiceContainer as TServiceContainerServer).SetServiceLog( // ! aRestLogServer,TSQLRecordServiceLog); function ServiceContainer: TServiceContainer; override; /// compute the statistics about this server, as JSON // - is a wrapper around the Stats() method-based service function StatsAsJson(Flags: TSQLRestServerAddStats=[withTables..withSessions]): RawUTF8; virtual; /// compute the statistics about this server, as a TDocVariant document // - is a wrapper around the Stats() method-based service function StatsAsDocVariant(Flags: TSQLRestServerAddStats=[withTables..withSessions]): variant; /// read-only access to the internal list of sessions // - ensure you protect its access calling Sessions.Lock/Sessions.Unlock property Sessions: TSynObjectListLocked read fSessions; /// read-only access to the list of registered server-side authentication // methods, used for session creation // - note that the exact number or registered services in this list is // stored in the AuthenticationSchemesCount property property AuthenticationSchemes: TSQLRestServerAuthenticationDynArray read fSessionAuthentication; /// how many authentication methods are registered in AuthenticationSchemes property AuthenticationSchemesCount: integer read GetAuthenticationSchemesCount; /// define if unsecure connections (i.e. not in-process or encrypted // WebSockets) with no session can be authenticated via JWT // - once set, this instance will be owned by the TSQLRestServer // - by definition, such JWT authentication won't identify any mORMot user // nor session (it just has to be valid), so only sicSingle, sicShared or // sicPerThread interface-based services execution are possible // - typical usage is for a public API, in conjunction with // ServiceDefine(...).ResultAsJSONObjectWithoutResult := true on the server // side and TSQLRestClientURI.ServiceDefineSharedAPI() method for the client // - see also JWTForUnauthenticatedRequestWhiteIP() for additional security property JWTForUnauthenticatedRequest: TJWTAbstract read fJWTForUnauthenticatedRequest write fJWTForUnauthenticatedRequest; /// retrieve the TSQLRestStorage instance used to store and manage // a specified TSQLRecordClass in memory // - has been associated by the StaticDataCreate method property StaticDataServer[aClass: TSQLRecordClass]: TSQLRest read GetStaticDataServer; /// retrieve a running TSQLRestStorage virtual table // - associated e.g. to a 'JSON' or 'Binary' virtual table module, or may // return a TSQLRestStorageExternal instance (as defined in mORMotDB) // - this property will return nil if there is no Virtual Table associated // or if the corresponding module is not a TSQLVirtualTable // (e.g. "pure" static tables registered by StaticDataCreate will be // accessible only via StaticDataServer[], not via StaticVirtualTable[]) // - has been associated by the TSQLModel.VirtualTableRegister method or // the VirtualTableExternalRegister() global function property StaticVirtualTable[aClass: TSQLRecordClass]: TSQLRest read GetVirtualTable; /// fast get the associated static server or virtual table, if any // - same as a dual call to StaticDataServer[aClass] + StaticVirtualTable[aClass] property StaticTable[aClass: TSQLRecordClass]: TSQLRest read GetStaticTable; /// the options specified to TSQLRestServer.CreateMissingTables // - as expected by TSQLRecord.InitializeTable methods property CreateMissingTablesOptions: TSQLInitializeTableOptions read fCreateMissingTablesOptions; /// the URI to redirect any plain GET on root URI, without any method // - could be used to ease access from web browsers URI property RootRedirectGet: RawUTF8 read fRootRedirectGet write fRootRedirectGet; /// you can force this property to TRUE so that any Delete() will not // write to the TSQLRecordTableDelete table for TRecordVersion tables // - to be used when applying a TSQLRestBatch instance as returned by // RecordVersionSynchronizeToBatch() property RecordVersionDeleteIgnore: boolean read fRecordVersionDeleteIgnore write fRecordVersionDeleteIgnore; published /// set this property to true to transmit the JSON data in a "not expanded" format // - not directly compatible with Javascript object list decode: not to be // used in AJAX environnement (like in TSQLite3HttpServer) // - but transmitted JSON data is much smaller if set it's set to FALSE, and // if you use a Delphi Client, parsing will be also faster and memory usage // will be lower // - By default, the NoAJAXJSON property is set to TRUE in // TSQLRestServer.ExportServerNamedPipe: if you use named pipes for communication, // you probably won't use javascript because browser communicates via HTTP! // - But otherwise, NoAJAXJSON property is set to FALSE. You could force its // value to TRUE and you'd save some bandwidth if you don't use javascript: // even the parsing of the JSON Content will be faster with Delphi client // if JSON content is not expanded // - the "expanded" or standard/AJAX layout allows you to create pure JavaScript // objects from the JSON content, because the field name / JavaScript object // property name is supplied for every value // - the "not expanded" layout, NoAJAXJSON property is set to TRUE, // reflects exactly the layout of the SQL request - first line contains the // field names, then all next lines are the field content // - is in fact stored in rsoNoAJAXJSON item in Options property property NoAJAXJSON: boolean read GetNoAJAXJSON write SetNoAJAXJSON; /// allow to customize how TSQLRestServer.URI process the requests // - e.g. if HTTP_SUCCESS with no body should be translated into HTTP_NOCONTENT property Options: TSQLRestServerOptions read fOptions write fOptions; /// set to true if the server will handle per-user authentication and // access right management // - i.e. if the associated TSQLModel contains TSQLAuthUser and // TSQLAuthGroup tables (set by constructor) property HandleAuthentication: boolean read fHandleAuthentication; /// allow to by-pass Authentication for a given set of HTTP verbs // - by default, RESTful access to the ORM will follow HandleAuthentication /// setting: but you could define some HTTP verb to this property, which // will by-pass the authentication - may be used e.g. for public GET // of the content by an AJAX client property BypassORMAuthentication: TSQLURIMethods read fBypassORMAuthentication write fBypassORMAuthentication; /// read-only access to the high-level Server statistics // - see ServiceMethodStat[] for information about method-based services, // or TServiceFactoryServer.Stats / Stat[] for interface-based services // - statistics are available remotely as JSON from the Stat() method property Stats: TSQLRestServerMonitor read fStats; /// which level of detailed information is gathered // - by default, contains SERVERDEFAULTMONITORLEVELS, i.e. // ! [mlTables,mlMethods,mlInterfaces,mlSQLite3] // - you can add mlSessions to maintain per-session statistics: this will // lead into a slightly higher memory consumption, for each session property StatLevels: TSQLRestServerMonitorLevels read fStatLevels write fStatLevels; /// could be set to track statistic from Stats information // - it may be e.g. a TSynMonitorUsageRest instance for REST storage property StatUsage: TSynMonitorUsage read fStatUsage write SetStatUsage; /// this property can be left to its TRUE default value, to handle any // TSQLVirtualTableJSON static tables (module JSON or BINARY) with direct // calls to the storage instance // - is set to TRUE by default to enable faster Direct mode // - in Direct mode, GET/POST/PUT/DELETE of individual records (or BLOB fields) // from URI() will call directly the corresponding TSQLRestStorage // instance, for better speed for most used RESTful operations; but complex // SQL requests (e.g. joined SELECT) will rely on the main SQL engine // - if set to false, will use the main SQLite3 engine for all statements // (should not to be used normally, because it will add unnecessary overhead) property StaticVirtualTableDirect: boolean read fVirtualTableDirect write fVirtualTableDirect; /// the class inheriting from TSQLRecordTableDeleted, as defined in the model // - during authentication, this class will be used for storing a trace of // every deletion of table rows containing a TRecordVersion published field property SQLRecordVersionDeleteTable: TSQLRecordTableDeletedClass read fSQLRecordVersionDeleteTable; /// the class inheriting from TAuthSession to handle in-memory sessions // - since all sessions data remain in memory, ensure they are not taking // too much resource (memory or process time) property SessionClass: TAuthSessionClass read fSessionClass write fSessionClass; /// the class inheriting from TSQLAuthUser, as defined in the model // - during authentication, this class will be used for every TSQLAuthUser // table access // - see also the OnAuthenticationUserRetrieve optional event handler property SQLAuthUserClass: TSQLAuthUserClass read fSQLAuthUserClass; /// the class inheriting from TSQLAuthGroup, as defined in the model // - during authentication, this class will be used for every TSQLAuthGroup // table access property SQLAuthGroupClass: TSQLAuthGroupClass read fSQLAuthGroupClass; published { standard method-based services } /// REST service accessible from ModelRoot/Stat URI to gather detailed information // - returns the current execution statistics of this server, as a JSON object // - this method will require an authenticated client, for safety // - by default, will return the high-level information of this server // - will return human-readable JSON layout if ModelRoot/Stat/json is used, or // the corresponding XML content if ModelRoot/Stat/xml is used // - you can define withtables, withmethods, withinterfaces, withsessions or // withsqlite3 additional parameters to return detailed information about // method-based services, interface-based services, per session statistics, // or prepared SQLite3 SQL statement timing (for a TSQLRestServerDB instance) // ! Client.CallBackGet('stat',['withtables',true,'withmethods',true, // ! 'withinterfaces',true,'withsessions',true,'withsqlite3',true],stats); // - defining a 'withall' parameter will retrieve all available statistics // - note that TSQLRestServer.StatLevels property will enable statistics // gathering for tables, methods, interfaces, sqlite3 or sessions // - a specific findservice=ServiceName parameter will not return any // statistics, but matching URIs from the server AssociatedServices list procedure Stat(Ctxt: TSQLRestServerURIContext); /// REST service accessible from ModelRoot/Auth URI // - called by the clients for authentication and session management // - this method won't require an authenticated client, since it is used to // initiate authentication // - this global callback method is thread-safe procedure Auth(Ctxt: TSQLRestServerURIContext); /// REST service accessible from the ModelRoot/Timestamp URI // - returns the server time stamp TTimeLog/Int64 value as UTF-8 text // - this method will not require an authenticated client // - hidden ModelRoot/Timestamp/info command will return basic execution // information, less verbose (and sensitive) than Stat(), calling virtual // InternalInfo() protected method procedure Timestamp(Ctxt: TSQLRestServerURIContext); /// REST service accessible from the ModelRoot/CacheFlush URI // - it will flush the server result cache // - this method shall be called by the clients when the Server cache may be // not consistent any more (e.g. after a direct write to an external database) // - this method will require an authenticated client, for safety // - GET ModelRoot/CacheFlush URI will flush the whole Server cache, // for all tables // - GET ModelRoot/CacheFlush/TableName URI will flush the specified // table cache // - GET ModelRoot/CacheFlush/TableName/TableID URI will flush the content // of the specified record // - POST ModelRoot/CacheFlush/_callback_ URI will be called by the client // to notify the server that an interface callback instance has been released // - POST ModelRoot/CacheFlush/_ping_ URI will be called by the client after // every half session timeout (or at least every hour) to notify the server // that the connection is still alive procedure CacheFlush(Ctxt: TSQLRestServerURIContext); /// REST service accessible from the ModelRoot/Batch URI // - will execute a set of RESTful commands, in a single step, with optional // automatic SQL transaction generation // - this method will require an authenticated client, for safety // - expect input as JSON commands: // & '{"Table":["cmd":values,...]}' // or for multiple tables: // & '["cmd@Table":values,...]' // with cmd in POST/PUT with {object} as value or DELETE with ID // - returns an array of integers: '[200,200,...]' or '["OK"]' if all // returned status codes are 200 (HTTP_SUCCESS) // - URI are either 'ModelRoot/TableName/Batch' or 'ModelRoot/Batch' procedure Batch(Ctxt: TSQLRestServerURIContext); end; /// REST class with direct access to an external database engine // - you can set an alternate per-table database engine by using this class // - this abstract class is to be overridden with a proper implementation // (e.g. TSQLRestStorageInMemory in this unit, or TSQLRestStorageExternal // from mORMotDB unit, or TSQLRestStorageMongoDB from mORMotMongoDB unit) TSQLRestStorage = class(TSQLRest) protected fStoredClass: TSQLRecordClass; fStoredClassProps: TSQLModelRecordProperties; fStoredClassRecordProps: TSQLRecordProperties; fStoredClassMapping: PSQLRecordPropertiesMapping; fStorageLockShouldIncreaseOwnerInternalState: boolean; fStorageLockLogTrace: boolean; fModified: boolean; fOwner: TSQLRestServer; fStorageCriticalSection: TRTLCriticalSection; fStorageCriticalSectionCount: integer; fBasicSQLCount: RawUTF8; fBasicSQLHasRows: array[boolean] of RawUTF8; fStorageVirtual: TSQLVirtualTable; /// any set bit in this field indicates UNIQUE field value fIsUnique: TSQLFieldBits; /// allow to force refresh for a given Static table // - default FALSE means to return the main TSQLRestServer.InternalState // - TRUE indicates that OutInternalState := cardinal(-1) will be returned fOutInternalStateForcedRefresh: boolean; procedure RecordVersionFieldHandle(Occasion: TSQLOccasion; var Decoder: TJSONObjectDecoder); /// override this method if you want to update the refresh state // - returns FALSE if the static table content was not modified (default // method implementation is to always return FALSE) // - returns TRUE if the table has been refreshed and its content was modified: // therefore the client will know he'll need to refresh some content function RefreshedAndModified: boolean; virtual; /// TSQLRestServer.URI use it for Static.EngineList to by-pass virtual table // - this default implementation will return TRUE and replace SQL with // SQLSelectAll[true] if it SQL equals SQLSelectAll[false] (i.e. 'SELECT *') // - this method is called only if the WHERE clause of SQL refers to the // static table name only (not needed to check it twice) function AdaptSQLForEngineList(var SQL: RawUTF8): boolean; virtual; function GetStoredClassName: RawUTF8; function GetCurrentSessionUserID: TID; override; public /// initialize the abstract storage data constructor Create(aClass: TSQLRecordClass; aServer: TSQLRestServer); reintroduce; virtual; /// finalize the storage instance destructor Destroy; override; /// should be called before any access to the storage content // - and protected with a try ... finally StorageUnLock; end section procedure StorageLock(WillModifyContent: boolean; const msg: RawUTF8); virtual; /// should be called after any StorageLock-protected access to the content // - e.g. protected with a try ... finally StorageUnLock; end section procedure StorageUnLock; virtual; /// you can call this method in TThread.Execute to ensure that // the thread will be taken into account during process // - this overridden method will do nothing (should have been already made // at TSQLRestServer caller level) // - children classes may inherit from this method to notify e.g. // a third party process (like proper OLE initialization) procedure BeginCurrentThread(Sender: TThread); override; /// you can call this method just before a thread is finished to ensure // e.g. that the associated external DB connection will be released // - this overridden method will do nothing (should have been already made // at TSQLRestServer caller level) // - children classes may inherit from this method to notify e.g. // a third party process (like proper OLE initialization) procedure EndCurrentThread(Sender: TThread); override; /// implement TSQLRest unlocking (UNLOCK verb) // - to be called e.g. after a Retrieve() with forupdate=TRUE // - locking is handled at (Owner.)Model level // - returns true on success function UnLock(Table: TSQLRecordClass; aID: TID): boolean; override; /// overridden method calling the owner (if any) to guess if this record // can be updated or deleted function RecordCanBeUpdated(Table: TSQLRecordClass; ID: TID; Action: TSQLEvent; ErrorMsg: PRawUTF8 = nil): boolean; override; /// create one index for all specific FieldNames at once // - do nothing method: will return FALSE (aka error) function CreateSQLMultiIndex(Table: TSQLRecordClass; const FieldNames: array of RawUTF8; Unique: boolean; IndexName: RawUTF8=''): boolean; virtual; /// 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 // - faster than OneFieldValues method, which creates a temporary JSON content // - this default implementation will call the overloaded SearchField() // value after conversion of the FieldValue into RawUTF8 function SearchField(const FieldName: RawUTF8; FieldValue: Int64; out ResultID: TIDDynArray): boolean; overload; virtual; /// 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 // - faster than OneFieldValues method, which creates a temporary JSON content function SearchField(const FieldName, FieldValue: RawUTF8; out ResultID: TIDDynArray): boolean; overload; virtual; abstract; /// access or initialize the internal IoC resolver // - this overriden method will return always nil, since IoC only makes // sense at TSQLRestClient and TSQLRestServer level function ServiceContainer: TServiceContainer; override; /// read only access to a boolean value set to true if table data was modified property Modified: boolean read fModified write fModified; /// read only access to the ORM properties of the associated record type // - may be nil if this instance is not associated with a TSQLModel property StoredClassProps: TSQLModelRecordProperties read fStoredClassProps; /// read only access to the RTTI properties of the associated record type property StoredClassRecordProps: TSQLRecordProperties read fStoredClassRecordProps; /// read only access to the TSQLRestServer using this storage engine property Owner: TSQLRestServer read fOwner; /// enable low-level trace of StorageLock/StorageUnlock methods // - may be used to resolve low-level race conditions property StorageLockLogTrace: boolean read fStorageLockLogTrace write fStorageLockLogTrace; /// read only access to the class defining the record type stored in this // REST storage property StoredClass: TSQLRecordClass read fStoredClass; published /// name of the class defining the record type stored in this REST storage property StoredClassName: RawUTF8 read GetStoredClassName; end; /// event prototype called by TSQLRestStorageInMemory.FindWhereEqual() or // TSQLRestStorageInMemory.ForEach() methods // - aDest is an opaque pointer, as supplied to FindWhereEqual(), which may // point e.g. to a result list, or a shared variable to apply the process // - aRec will point to the corresponding item // - aIndex will identify the item index in the internal list TFindWhereEqualEvent = procedure(aDest: pointer; aRec: TSQLRecord; aIndex: integer) of object; /// abstract REST storage exposing some internal TSQLRecord-based methods TSQLRestStorageRecordBased = class(TSQLRestStorage) protected function EngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; override; function EngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; override; public /// manual Add of a TSQLRecord // - returns the ID created on success // - returns -1 on failure (not UNIQUE field value e.g.) // - on success, the Rec instance is added to the Values[] list: caller // doesn't need to Free it function AddOne(Rec: TSQLRecord; ForceID: boolean; const SentData: RawUTF8): TID; virtual; abstract; /// manual Retrieval of a TSQLRecord field values // - an instance of the associated static class is created // - and all its properties are filled from the Items[] values // - caller can modify these properties, then use UpdateOne() if the changes // have to be stored inside the Items[] list // - calller must always free the returned instance // - returns NIL if any error occured, e.g. if the supplied aID was incorrect // - method available since a TSQLRestStorage instance may be created // stand-alone, i.e. without any associated Model/TSQLRestServer function GetOne(aID: TID): TSQLRecord; virtual; abstract; /// manual Update of a TSQLRecord field values // - Rec.ID specifies which record is to be updated // - will update all properties, including BLOB fields and such // - returns TRUE on success, FALSE on any error (e.g. invalid Rec.ID) // - method available since a TSQLRestStorage instance may be created // stand-alone, i.e. without any associated Model/TSQLRestServer function UpdateOne(Rec: TSQLRecord; const SentData: RawUTF8): boolean; overload; virtual; abstract; /// manual Update of a TSQLRecord field values from an array of TSQLVar // - will update all properties, including BLOB fields and such // - returns TRUE on success, FALSE on any error (e.g. invalid Rec.ID) // - method available since a TSQLRestStorage instance may be created // stand-alone, i.e. without any associated Model/TSQLRestServer // - this default implementation will create a temporary TSQLRecord instance // with the supplied Values[], and will call overloaded UpdateOne() method function UpdateOne(ID: TID; const Values: TSQLVarDynArray): boolean; overload; virtual; end; /// class able to handle a O(1) hashed-based search of a property // - used e.g. to hash TSQLRestStorageInMemory field values TSQLRestStorageInMemoryUnique = class protected fHasher: TDynArrayHasher; fOwner: TSQLRestStorageInMemory; fPropInfo: TSQLPropInfo; fCaseInsensitive: boolean; fLastFindHashCode: cardinal; function EventCompare(const A,B): integer; // match TEventDynArraySortCompare function EventHash(const Elem): cardinal; // match TEventDynArrayHashOne public /// initialize a hash for a record array field // - aField maps the "stored AS_UNIQUE" published property constructor Create(aOwner: TSQLRestStorageInMemory; aField: TSQLPropInfo); /// fast search using O(1) internal hash table // - returns -1 if not found or not indexed (self=nil) function Find(Rec: TSQLRecord): integer; /// called by TSQLRestStorageInMemory.AddOne after a precious Find() function AddedAfterFind(Rec: TSQLRecord): boolean; /// the corresponding field RTTI property PropInfo: TSQLPropInfo read fPropInfo; /// if the string comparison shall be case-insensitive property CaseInsensitive: boolean read fCaseInsensitive; /// access to the internal hash table property Hasher: TDynArrayHasher read fHasher; end; /// REST storage with direct access to a TObjectList memory-stored table // - store the associated TSQLRecord values in memory // - handle one TSQLRecord per TSQLRestStorageInMemory instance // - must be registered individualy in a TSQLRestServer to access data from a // common client, by using the TSQLRestServer.StaticDataCreate method: // it allows an unique access for both SQLite3 and Static databases // - handle basic REST commands, no SQL interpreter is implemented: only // valid SQL command is "SELECT Field1,Field2 FROM Table WHERE ID=120;", i.e // a one Table SELECT with one optional "WHERE fieldname = value" statement; // if used within a TSQLVirtualTableJSON, you'll be able to handle any kind of // SQL statement (even joined SELECT or such) with this memory-stored database // - our TSQLRestStorage database engine is very optimized and is a lot // faster than SQLite3 for such queries - but its values remain in RAM, // therefore it is not meant to deal with more than 100,000 rows // - data can be stored and retrieved from a file (JSON format is used by // default, if BinaryFile parameter is left to false; a proprietary compressed // binary format can be used instead) if a file name is supplied at creating // the TSQLRestStorageInMemory instance TSQLRestStorageInMemory = class(TSQLRestStorageRecordBased) protected fValue: TSQLRecordObjArray; fCount: integer; fFileName: TFileName; fCommitShouldNotUpdateFile: boolean; fBinaryFile: boolean; fExpandedJSON: boolean; fUnSortedID: boolean; fSearchRec: TSQLRecord; // temporary record to store the searched value fBasicUpperSQLSelect: array[boolean] of RawUTF8; fUnique: array of TSQLRestStorageInMemoryUnique; fMaxID: TID; fValues: TDynArrayHashed; // hashed by ID function UniqueFieldsUpdateOK(aRec: TSQLRecord; aUpdateIndex: integer): boolean; function GetItem(Index: integer): TSQLRecord; {$ifdef HASINLINE}inline;{$endif} function GetID(Index: integer): TID; procedure SetFileName(const aFileName: TFileName); procedure ComputeStateAfterLoad(var loaded: TPrecisionTimer; binary: boolean); procedure SetBinaryFile(aBinary: boolean); procedure GetJSONValuesEvent(aDest: pointer; aRec: TSQLRecord; aIndex: integer); /// used to create the JSON content from a SELECT parsed command // - WhereField index follows FindWhereEqual / TSynTableStatement.WhereField // - returns the number of data row added (excluding field names) // - this method is very fast and optimized (for search and JSON serializing) function GetJSONValues(Stream: TStream; Expand: boolean; Stmt: TSynTableStatement): PtrInt; /// TSQLRestServer.URI use it for Static.EngineList to by-pass virtual table // - overridden method to handle basic queries as handled by EngineList() function AdaptSQLForEngineList(var SQL: RawUTF8): boolean; override; /// overridden methods for direct in-memory database engine thread-safe process function EngineRetrieve(TableModelIndex: Integer; ID: TID): RawUTF8; override; function EngineList(const SQL: RawUTF8; ForceAJAX: Boolean=false; ReturnedRowCount: PPtrInt=nil): RawUTF8; override; function EngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; override; 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 EngineDeleteWhere(TableModelIndex: integer; const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean; override; function EngineExecute(const aSQL: RawUTF8): boolean; override; public /// initialize the table storage data, reading it from a file if necessary // - data encoding on file is UTF-8 JSON format by default, or // should be some binary format if aBinaryFile is set to true constructor Create(aClass: TSQLRecordClass; aServer: TSQLRestServer; const aFileName: TFileName = ''; aBinaryFile: boolean=false); reintroduce; virtual; /// free used memory // - especially release all fValue[] instances destructor Destroy; override; /// clear all the values of this table // - will reset the associated database file, if any procedure DropValues(andUpdateFile: boolean=true); /// load the values from JSON data // - a temporary copy of aJSON is made to ensure it won't be modified in-place // - consider using the overlaoded PUTF8Char/len method if you don't need this copy procedure LoadFromJSON(const aJSON: RawUTF8); overload; /// load the values from JSON data procedure LoadFromJSON(JSONBuffer: PUTF8Char; JSONBufferLen: integer); overload; /// save the values into JSON data function SaveToJSON(Expand: Boolean): RawUTF8; overload; /// save the values into JSON data procedure SaveToJSON(Stream: TStream; Expand: Boolean); overload; /// load the values from binary file/stream // - the binary format is a custom compressed format (using our SynLZ fast // compression algorithm), with variable-length record storage // - the binary content is first checked for consistency, before loading // - warning: the field layout should be the same at SaveToBinary call; // for instance, it won't be able to read a file content with a renamed // or modified field type // - will return false if the binary content is invalid function LoadFromBinary(Stream: TStream): boolean; overload; /// load the values from binary data // - uses the same compressed format as the overloaded stream/file method // - will return false if the binary content is invalid function LoadFromBinary(const Buffer: RawByteString): boolean; overload; /// load the values from binary resource // - the resource name is expected to be the TSQLRecord class name, // with a resource type of 10 // - uses the same compressed format as the overloaded stream/file method // - you can specify a library (dll) resource instance handle, if needed procedure LoadFromResource(ResourceName: string=''; Instance: THandle=0); /// save the values into a binary file/stream // - the binary format is a custom compressed format (using our SynLZ fast // compression algorithm), with variable-length record storage: e.g. a 27 KB // Dali1.json content is stored into a 6 KB Dali2.data file // (this data has a text redundant field content in its FirstName field); // 502 KB People.json content is stored into a 92 KB People.data file // - returns the number of bytes written into Stream function SaveToBinary(Stream: TStream): integer; overload; /// save the values into a binary buffer // - uses the same compressed format as the overloaded stream/file method function SaveToBinary: RawByteString; overload; /// if file was modified, the file is updated on disk // - this method is called automaticaly when the TSQLRestStorage // instance is destroyed: should should want to call in in some cases, // in order to force the data to be saved regularly // - do nothing if the table content was not modified // - will write JSON content by default, or binary content if BinaryFile // property was set to true procedure UpdateFile; /// will reload all content from the current disk file // - any not saved modification will be lost (e.g. if Updatefile has not // been called since) procedure ReloadFromFile; /// retrieve the index in Items[] of a particular ID // - return -1 if this ID was not found // - use internally fast O(1) hashed search algorithm // - warning: this method should be protected via StorageLock/StorageUnlock function IDToIndex(ID: TID): PtrInt; /// retrieve all IDs stored at once // - will make a thread-safe copy, for unlocked use procedure GetAllIDs(out ID: TIDDynArray); /// low-level Add of a TSQLRecord instance // - returns the ID created on success // - returns -1 on failure (not UNIQUE field value e.g.) // - on success, the Rec instance is added to the Values[] list: caller // doesn't need to Free it, since it will be owned by the storage // - in practice, SentData is used only for OnUpdateEvent/OnBlobUpdateEvent // and the history feature // - warning: this method should be protected via StorageLock/StorageUnlock function AddOne(Rec: TSQLRecord; ForceID: boolean; const SentData: RawUTF8): TID; override; /// manual Retrieval of a TSQLRecord field values // - an instance of the associated static class is created, and filled with // the actual properties values // - and all its properties are filled from the Items[] values // - caller can modify these properties, then use UpdateOne() if the changes // have to be stored inside the Items[] list // - calller must always free the returned instance // - returns NIL if any error occured, e.g. if the supplied aID was incorrect // - method available since a TSQLRestStorage instance may be created // stand-alone, i.e. without any associated Model/TSQLRestServer function GetOne(aID: TID): TSQLRecord; override; /// manual Update of a TSQLRecord field values // - Rec.ID specifies which record is to be updated // - will update all properties, including BLOB fields and such // - returns TRUE on success, FALSE on any error (e.g. invalid Rec.ID) // - method available since a TSQLRestStorage instance may be created // stand-alone, i.e. without any associated Model/TSQLRestServer function UpdateOne(Rec: TSQLRecord; const SentData: RawUTF8): boolean; override; /// manual Update of a TSQLRecord field values from a TSQLVar array // - will update all properties, including BLOB fields and such // - returns TRUE on success, FALSE on any error (e.g. invalid Rec.ID) // - method available since a TSQLRestStorage instance may be created // stand-alone, i.e. without any associated Model/TSQLRestServer function UpdateOne(ID: TID; const Values: TSQLVarDynArray): boolean; override; /// direct deletion of a TSQLRecord, from its index in Values[] // - warning: this method should be protected via StorageLock/StorageUnlock function DeleteOne(aIndex: integer): boolean; virtual; /// overridden method for direct in-memory database engine call // - 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; /// overridden method for direct in-memory database engine call // - made public since a TSQLRestStorage instance may be created // stand-alone, i.e. without any associated Model/TSQLRestServer function EngineUpdateField(TableModelIndex: integer; const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; override; /// overridden method for direct in-memory database engine call // - made public since a TSQLRestStorage instance may be created // stand-alone, i.e. without any associated Model/TSQLRestServer function EngineUpdateFieldIncrement(TableModelIndex: integer; ID: TID; const FieldName: RawUTF8; Increment: Int64): boolean; override; /// overridden method for direct in-memory database engine call function UpdateBlobFields(Value: TSQLRecord): boolean; override; /// overridden method for direct in-memory database engine call function RetrieveBlobFields(Value: TSQLRecord): boolean; override; /// overridden method for direct in-memory database engine call function TableRowCount(Table: TSQLRecordClass): Int64; override; /// overridden method for direct in-memory database engine call function TableHasRows(Table: TSQLRecordClass): boolean; override; /// overridden method for direct in-memory database engine call function MemberExists(Table: TSQLRecordClass; ID: TID): boolean; 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 // - faster than OneFieldValues method, which creates a temporary JSON content function SearchField(const FieldName, FieldValue: RawUTF8; out ResultID: TIDDynArray): boolean; override; /// search for a field value, according to its SQL content representation // - return the found TSQLRecord on success, nil if none did match // - warning: it returns a reference to one item of the unlocked internal // list, so you should NOT use this on a read/write table, but rather // use the slightly slower but safer SearchCopy() method or make explicit // ! StorageLock ... try ... SearchInstance ... finally StorageUnlock end function SearchInstance(const FieldName, FieldValue: RawUTF8): pointer; /// search for a field value, according to its SQL content representation // - return the found TSQLRecord index on success, -1 if none did match // - warning: it returns a reference to the current index of the unlocked // internal list, so you should NOT use without StorageLock/StorageUnlock function SearchIndex(const FieldName, FieldValue: RawUTF8): integer; /// search for a field value, according to its SQL content representation // - return a copy of the found TSQLRecord on success, nil if no match // - you should use SearchCopy() instead of SearchInstance(), unless you // are sure that the internal TSQLRecord list won't change function SearchCopy(const FieldName, FieldValue: RawUTF8): pointer; /// search and count for a field value, according to its SQL content representation // - return the number of found entries on success, 0 if it was not found function SearchCount(const FieldName, FieldValue: RawUTF8): integer; /// search for a field value, according to its SQL content representation // - call the supplied OnFind event on match // - returns the number of found entries // - is just a wrapper around FindWhereEqual() with StorageLock protection function SearchEvent(const FieldName, FieldValue: RawUTF8; OnFind: TFindWhereEqualEvent; Dest: pointer; FoundLimit,FoundOffset: PtrInt): integer; /// optimized search of WhereValue in WhereField (0=RowID,1..=RTTI) // - will use fast O(1) hash for fUnique[] fields // - will use SYSTEMNOCASE case-insensitive search for text values, unless // CaseInsensitive is set to FALSE // - warning: this method should be protected via StorageLock/StorageUnlock function FindWhereEqual(WhereField: integer; const WhereValue: RawUTF8; OnFind: TFindWhereEqualEvent; Dest: pointer; FoundLimit,FoundOffset: PtrInt; CaseInsensitive: boolean=true): PtrInt; overload; /// optimized search of WhereValue in a field, specified by name // - will use fast O(1) hash for fUnique[] fields // - will use SYSTEMNOCASE case-insensitive search for text values, unless // CaseInsensitive is set to FALSE // - warning: this method should be protected via StorageLock/StorageUnlock function FindWhereEqual(const WhereFieldName, WhereValue: RawUTF8; OnFind: TFindWhereEqualEvent; Dest: pointer; FoundLimit,FoundOffset: integer; CaseInsensitive: boolean=true): PtrInt; overload; /// search the maximum value of a given column // - will only handle integer/Int64 kind of column function FindMax(WhereField: integer; out max: Int64): boolean; /// execute a method on every TSQLRecord item // - the loop execution will be protected via StorageLock/StorageUnlock procedure ForEach(WillModifyContent: boolean; OnEachProcess: TFindWhereEqualEvent; Dest: pointer); /// low-level TFindWhereEqualEvent callback doing nothing class procedure DoNothingEvent(aDest: pointer; aRec: TSQLRecord; aIndex: integer); /// low-level TFindWhereEqualEvent callback setting PPointer(aDest)^ := aRec class procedure DoInstanceEvent(aDest: pointer; aRec: TSQLRecord; aIndex: integer); /// low-level TFindWhereEqualEvent callback setting PInteger(aDest)^ := aIndex class procedure DoIndexEvent(aDest: pointer; aRec: TSQLRecord; aIndex: integer); /// low-level TFindWhereEqualEvent callback setting PPointer(aDest)^ := aRec.CreateCopy class procedure DoCopyEvent(aDest: pointer; aRec: TSQLRecord; aIndex: integer); /// low-level TFindWhereEqualEvent callback calling TSynList(aDest).Add(aRec) class procedure DoAddToListEvent(aDest: pointer; aRec: TSQLRecord; aIndex: integer); /// read-only access to the TSQLRecord values, storing the data // - this returns directly the item class instance stored in memory: if you // change the content, it will affect the internal data - so for instance // DO NOT change the ID values, unless you may have unexpected behavior // - warning: this method should be protected via StorageLock/StorageUnlock property Items[Index: integer]: TSQLRecord read GetItem; default; /// direct access to the memory of the internal dynamic array storage // - Items[] is preferred, since it will check the index, but is slightly // slower, e.g. in a loop or after a IDToIndex() call // - warning: this method should be protected via StorageLock/StorageUnlock property Value: TSQLRecordObjArray read fValue; /// read-only access to the ID of a TSQLRecord values // - warning: this method should be protected via StorageLock/StorageUnlock property ID[Index: integer]: TID read GetID; published /// read only access to the file name specified by constructor // - you can call the TSQLRestServer.StaticDataCreate method to // update the file name of an already instanciated static table // - if you change manually the file name from this property, the storage // will be marked as "modified" so that UpdateFile will save the content property FileName: TFileName read fFileName write SetFileName; /// if set to true, file content on disk will expect binary format // - default format on disk is JSON but can be overridden at constructor call // - binary format should be more efficient in term of speed and disk usage, // but can be proprietary // - if you change manually the file format from this property, the storage // will be marked as "modified" so that UpdateFile will save the content property BinaryFile: boolean read fBinaryFile write SetBinaryFile; // JSON writing, can set if the format should be expanded or not // - by default, the JSON will be in the custom non-expanded format, // to save disk space and time // - you can force the JSON to be emitted as an array of objects, // e.g. for better human friendliness (reading and modification) property ExpandedJSON: boolean read fExpandedJSON write fExpandedJSON; /// set this property to TRUE if you want the COMMIT statement not to // update the associated TSQLVirtualTableJSON property CommitShouldNotUpdateFile: boolean read fCommitShouldNotUpdateFile write fCommitShouldNotUpdateFile; /// read-only access to the number of TSQLRecord values property Count: integer read fCount; end; /// a dynamic array of TSQLRestStorageInMemory instances // - used e.g. by TSQLRestServerFullMemory TSQLRestStorageInMemoryDynArray = array of TSQLRestStorageInMemory; /// REST storage with direct access to a memory database, to be used as // an external SQLite3 Virtual table // - this is the kind of in-memory table expected by TSQLVirtualTableJSON, // in order to be consistent with the internal DB cache TSQLRestStorageInMemoryExternal = class(TSQLRestStorageInMemory) public /// initialize the table storage data, reading it from a file if necessary // - data encoding on file is UTF-8 JSON format by default, or // should be some binary format if aBinaryFile is set to true constructor Create(aClass: TSQLRecordClass; aServer: TSQLRestServer; const aFileName: TFileName = ''; aBinaryFile: boolean=false); override; /// this overridden method will notify the Owner when the internal DB content // is known to be invalid // - by default, all REST/CRUD requests and direct SQL statements are // scanned and identified as potentially able to change the internal SQL/JSON // cache used at SQLite3 database level; but TSQLVirtualTableJSON virtual // tables could flush the database content without proper notification // - this overridden implementation will call Owner.FlushInternalDBCache procedure StorageLock(WillModifyContent: boolean; const msg: RawUTF8); override; end; /// REST storage with redirection to another REST instance // - allows redirection of all CRUD operations for a table to another // TSQLRest instance, may be a remote TSQLRestClient or a TSQLRestServer // - will be used by TSQLRestServer.RemoteDataCreate() method TSQLRestStorageRemote = class(TSQLRestStorage) protected fRemoteRest: TSQLRest; fRemoteTableIndex: integer; function EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; override; function EngineList(const SQL: RawUTF8; ForceAJAX: Boolean=false; ReturnedRowCount: PPtrInt=nil): RawUTF8; override; function EngineExecute(const aSQL: RawUTF8): boolean; override; function EngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; override; function EngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; override; function EngineDelete(TableModelIndex: integer; ID: TID): boolean; override; function EngineDeleteWhere(TableModelIndex: integer; const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean; override; 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 EngineUpdateField(TableModelIndex: integer; const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; override; function EngineUpdateFieldIncrement(TableModelIndex: integer; ID: TID; const FieldName: RawUTF8; Increment: Int64): boolean; override; public /// initialize the table storage redirection // - you should not have to use this constructor, but rather the // TSQLRestServer.RemoteDataCreate() method which will create and register // one TSQLRestStorageRemote instance constructor Create(aClass: TSQLRecordClass; aServer: TSQLRestServer; aRemoteRest: TSQLRest); reintroduce; virtual; published /// the remote ORM instance used for data persistence // - may be a TSQLRestClient or a TSQLRestServer instance property RemoteRest: TSQLRest read fRemoteRest; end; /// defines how TSQLRestStorageShard will handle its partioned process TSQLRestStorageShardOption = (ssoNoUpdate, ssoNoUpdateButLastShard, ssoNoDelete, ssoNoDeleteButLastShard, ssoNoBatch, ssoNoList, ssoNoExecute, ssoNoUpdateField, ssoNoConsolidateAtDestroy); /// how TSQLRestStorageShard will handle its partioned process TSQLRestStorageShardOptions = set of TSQLRestStorageShardOption; /// abstract REST storage with redirection to several REST instances, implementing // range ID partitioning for horizontal scaling // - such database shards will allow to scale with typical BigData storage // - this storage will add items on a server, initializing a new server // when the ID reached a defined range // - it will maintain a list of previous storages, then redirect reading and // updates to the server managing this ID (if possible - older shards may // be deleted/ignored to release resources) // - inherited class should override InitShards/InitNewShard to customize the // kind of TSQLRest instances to be used for each shard (which may be local // or remote, a SQLite3 engine or an external SQL/NoSQL database) // - see inherited TSQLRestStorageShardDB as defined in mORMotSQLite3.pas TSQLRestStorageShard = class(TSQLRestStorage) protected fShardRange: TID; fShardOffset: integer; fMaxShardCount: integer; fLastID: TID; fOptions: TSQLRestStorageShardOptions; fShards: array of TSQLRest; fShardLast: integer; fShardLastID: TID; fShardNextID: TID; fShardTableIndex: TIntegerDynArray; fShardBatch: array of TSQLRestBatch; // will set Shards[],fShardLast,fShardLastID,fShardOffset procedure InitShards; virtual; abstract; // should always return non nil shard to contain new added IDs function InitNewShard: TSQLRest; virtual; abstract; procedure InternalAddNewShard; function InternalShardBatch(ShardIndex: integer): TSQLRestBatch; // overriden methods which will handle all ORM process function EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; override; function EngineList(const SQL: RawUTF8; ForceAJAX: Boolean=false; ReturnedRowCount: PPtrInt=nil): RawUTF8; override; function EngineExecute(const aSQL: RawUTF8): boolean; override; function EngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; override; function EngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; override; function EngineDelete(TableModelIndex: integer; ID: TID): boolean; override; function EngineDeleteWhere(TableModelIndex: integer; const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean; override; 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 EngineUpdateField(TableModelIndex: integer; const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; override; function EngineUpdateFieldIncrement(TableModelIndex: integer; ID: TID; const FieldName: RawUTF8; Increment: Int64): boolean; override; function InternalBatchStart(Method: TSQLURIMethod; BatchOptions: TSQLRestBatchOptions): boolean; override; procedure InternalBatchStop; override; public /// initialize the table storage redirection for sharding // - you should not have to use this constructor, but e.g. // TSQLRestStorageShardDB.Create on a main TSQLRestServer.StaticDataAdd() // - the supplied aShardRange should be < 1000 - and once set, you should NOT // change this value on an existing shard, unless process will be broken constructor Create(aClass: TSQLRecordClass; aServer: TSQLRestServer; aShardRange: TID; aOptions: TSQLRestStorageShardOptions; aMaxShardCount: integer); reintroduce; virtual; /// finalize the table storage, including Shards[] instances destructor Destroy; override; /// you may call this method sometimes to consolidate the sharded data // - may e.g. merge/compact shards, depending on scaling expectations // - also called by Destroy - do nothing by default procedure ConsolidateShards; virtual; /// remove a shard database from the current set // - it will allow e.g. to delete a *.dbs file at runtime, without // restarting the server // - this default implementation will free and nil fShard[aShardIndex], // which is enough for most implementations (e.g. TSQLRestStorageShardDB) procedure RemoveShard(aShardIndex: integer); virtual; /// retrieve the ORM shard instance corresponding to an ID // - may return false if the correspondig shard is not available any more // - may return true, and a TSQLRestHookClient or a TSQLRestHookServer instance // with its associated index in TSQLRest.Model.Tables[] // - warning: this method should be protected via StorageLock/StorageUnlock function ShardFromID(aID: TID; out aShardTableIndex: integer; out aShard: TSQLRest; aOccasion: TSQLOccasion=soSelect; aShardIndex: PInteger=nil): boolean; virtual; /// get the row count of a specified table function TableRowCount(Table: TSQLRecordClass): Int64; override; /// check if there is some data rows in a specified table function TableHasRows(Table: TSQLRecordClass): boolean; override; published /// how much IDs should store each ORM shard instance // - once set, you should NEVER change this value on an existing shard, // otherwise the whole ID partition will fail // - each shard will hold [ShardIndex*ShardRange..(ShardIndex+1)*ShardRange-1] IDs property ShardRange: TID read fShardRange; /// how many shards should be maintained at most // - if some older shards are available on disk, they won't be loaded by // InitShards, and newly added shard via InitNewShard will trigger // RemoveShard if the total number of shards property MaxShardCount: integer read fMaxShardCount; /// defines how this instance will handle its sharding process // - by default, update/delete operations or per ID retrieval will take // place on all shards, whereas EngineList and EngineExecute will only run // only on the latest shard (to save resources) property Options: TSQLRestStorageShardOptions read fOptions write fOptions; end; /// class metadata of a Sharding storage engine TSQLRestStorageShardClass = class of TSQLRestStorageShard; /// a REST server using only in-memory tables // - this server will use TSQLRestStorageInMemory instances to handle // the data in memory, and optionally persist the data on disk as JSON or // binary files // - so it will not handle all SQL requests, just basic CRUD commands on // separated tables // - at least, it will compile as a TSQLRestServer without complaining for // pure abstract methods; it can be used to host some services if database // and ORM needs are basic (e.g. if only authentication and CRUD are needed), // without the need to link the SQLite3 engine TSQLRestServerFullMemory = class(TSQLRestServer) protected fFileName: TFileName; fBinaryFile: Boolean; fStaticDataCount: cardinal; fStorage: TSQLRestStorageInMemoryDynArray; function GetStorage(aTable: TSQLRecordClass): TSQLRestStorageInMemory; /// overridden methods which will call fStorage[TableModelIndex] directly function EngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; override; function EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; override; function EngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; override; function EngineDelete(TableModelIndex: integer; ID: TID): boolean; override; function EngineDeleteWhere(TableModelIndex: integer; const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean; override; 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 EngineUpdateField(TableModelIndex: integer; const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; override; function EngineUpdateFieldIncrement(TableModelIndex: integer; ID: TID; const FieldName: RawUTF8; Increment: Int64): boolean; override; /// overridden methods which will return error (no main DB here) function MainEngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; override; function MainEngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; override; function MainEngineList(const SQL: RawUTF8; ForceAJAX: Boolean; ReturnedRowCount: PPtrInt): RawUTF8; override; function MainEngineUpdate(TableModelIndex: integer; aID: TID; const SentData: RawUTF8): boolean; override; function MainEngineDelete(TableModelIndex: integer; ID: TID): boolean; override; function MainEngineDeleteWhere(TableModelIndex: integer; const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean; override; function MainEngineRetrieveBlob(TableModelIndex: integer; aID: TID; BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean; override; function MainEngineUpdateBlob(TableModelIndex: integer; aID: TID; BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean; override; function MainEngineUpdateField(TableModelIndex: integer; const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; override; function MainEngineUpdateFieldIncrement(TableModelIndex: integer; ID: TID; const FieldName: RawUTF8; Increment: Int64): boolean; override; // method not implemented: always return false function EngineExecute(const aSQL: RawUTF8): boolean; override; constructor RegisteredClassCreateFrom(aModel: TSQLModel; aServerHandleAuthentication: boolean; aDefinition: TSynConnectionDefinition); override; public /// initialize an in-memory REST server with no database file constructor Create(aModel: TSQLModel; aHandleUserAuthentication: boolean=false); overload; override; /// initialize an in-memory REST server with a database file // - all classes of the model will be created as TSQLRestStorageInMemory // - then data persistence will be initialized using aFileName, but no // file will be written to disk, unless you call explicitly UpdateToFile // - if aFileName is left void (''), data will not be persistent constructor Create(aModel: TSQLModel; const aFileName: TFileName; aBinaryFile: boolean=false; aHandleUserAuthentication: boolean=false); reintroduce; overload; virtual; /// initialize an in-memory REST server with a temporary Database Model, // and optional authentication by a single user // - a Model will be created with supplied tables, and owned by the server // - if aUserName is set, authentication will be enabled, and the supplied // credentials will be used to authenticate a single user, member of the // 'Supervisor' group - in this case, aHashedPassword value should match // TSQLAuthUser.PasswordHashHexa expectations constructor CreateWithOwnedAuthenticatedModel( const Tables: array of TSQLRecordClass; const aUserName, aHashedPassword: RawUTF8; aRoot: RawUTF8='root'); /// finalize the REST server // - this overridden destructor will write any modification on file (if // needed), and release all used memory destructor Destroy; override; /// save the TSQLRestFullMemory properties into a persistent storage object // - CreateFrom() will expect Definition.ServerName to store the FileName, // and use binary storage if Definition.DatabaseName is not void procedure DefinitionTo(Definition: TSynConnectionDefinition); override; /// Missing tables are created if they don't exist yet for every TSQLRecord // class of the Database Model // - you must call explicitely this before having called StaticDataCreate() // - all table description (even Unique feature) is retrieved from the Model // - this method also create additional fields, if the TSQLRecord definition // has been modified; only field adding is available, field renaming or // field deleting are not allowed in the FrameWork (in such cases, you must // create a new TSQLRecord type) procedure CreateMissingTables(user_version: cardinal=0; Options: TSQLInitializeTableOptions=[]); override; /// load the content from the specified file name // - do nothing if file name was not assigned procedure LoadFromFile; virtual; /// load the content from the supplied resource procedure LoadFromStream(aStream: TStream); virtual; /// write any modification into file // - do nothing if file name was not assigned procedure UpdateToFile; virtual; /// clear all internal storage content procedure DropDatabase; virtual; /// direct access to the storage instances // - you can then access to Storage[Table].Count and Storage[Table].Items[] property Storage[aTable: TSQLRecordClass]: TSQLRestStorageInMemory read GetStorage; /// direct access to the storage instances // - you can then access via Storage[TableIndex].Count and Items[] property Storages: TSQLRestStorageInMemoryDynArray read fStorage; published /// the file name used for data persistence property FileName: TFileName read fFileName write fFileName; /// set if the file content is to be compressed binary, or standard JSON // - it will use TSQLRestStorageInMemory LoadFromJSON/LoadFromBinary // SaveToJSON/SaveToBinary methods for optimized storage property BinaryFile: Boolean read fBinaryFile write fBinaryFile; published /// this method-base service will be accessible from ModelRoot/Flush URI, // and will write any modification into file // - method parameters signature matches TSQLRestServerCallBack type // - do nothing if file name was not assigned // - can be used from a remote client to ensure that any Add/Update/Delete // will be stored to disk, via // ! aClient.CallBackPut('Flush','',dummy) procedure Flush(Ctxt: TSQLRestServerURIContext); end; /// a REST server using another TSQLRest instance for all its ORM process // - this server will use an internal TSQLRest instance to handle all ORM // operations (i.e. access to objects) - e.g. TSQLRestClient for remote access // - it can be used e.g. to host some services on a stand-alone server, with // all ORM and data access retrieved from another server: it will allow to // easily implement a proxy architecture (for instance, as a DMZ for // publishing services, but letting ORM process stay out of scope) // - for per-table redirection, consider using the TSQLRestStorageRemote class // via a call to the TSQLRestServer.RemoteDataCreate() method TSQLRestServerRemoteDB = class(TSQLRestServer) protected fRemoteRest: TSQLRest; fRemoteTableIndex: TIntegerDynArray; function EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; override; function EngineList(const SQL: RawUTF8; ForceAJAX: Boolean=false; ReturnedRowCount: PPtrInt=nil): RawUTF8; override; function EngineExecute(const aSQL: RawUTF8): boolean; override; function EngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; override; function EngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; override; function EngineDelete(TableModelIndex: integer; ID: TID): boolean; override; function EngineDeleteWhere(TableModelIndex: integer; const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean; override; 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 EngineUpdateField(TableModelIndex: integer; const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; override; function EngineUpdateFieldIncrement(TableModelIndex: integer; ID: TID; const FieldName: RawUTF8; Increment: Int64): boolean; override; public /// initialize a REST server associated to a given TSQLRest instance // - the specified TSQLRest will be used for all ORM and data process // - you could use a TSQLRestClient or a TSQLRestServer instance // - the supplied TSQLRest.Model will be used for TSQLRestServerRemoteDB // - note that the TSQLRest instance won't be freed - caller shall ensure // that it will stay available at least until TSQLRestServerRemoteDB.Free constructor Create(aRemoteRest: TSQLRest; aHandleUserAuthentication: boolean=false); reintroduce; virtual; /// this method is called internally after any successfull deletion to // ensure relational database coherency // - this overridden method will just return TRUE: in this remote access, // true coherency will be performed on the ORM server side function AfterDeleteForceCoherency(TableIndex: integer; aID: TID): boolean; override; published /// the remote ORM instance used for data persistence // - may be a TSQLRestClient or a TSQLRestServer instance property RemoteRest: TSQLRest read fRemoteRest; end; /// possible call parameters for TOnTableUpdate Event TOnTableUpdateState = (tusPrepare, tusChanged, tusNoChange); /// used by TSQLRestClientURI.UpdateFromServer() to let the client // perform the rows update (for Marked[] e.g.) TOnTableUpdate = procedure(aTable: TSQLTableJSON; State: TOnTableUpdateState) of object; /// used by TSQLRestClientURI.Update() to let the client // perform the record update (refresh associated report e.g.) TOnRecordUpdate = procedure(Value: TSQLRecord) of object; /// a generic REpresentational State Transfer (REST) client // - is RESTful (i.e. URI) remotely implemented (TSQLRestClientURI e.g.) // - is implemented for direct access to a database (TSQLRestClientDB e.g.) TSQLRestClient = class(TSQLRest) protected fForceBlobTransfert: array of boolean; fOnTableUpdate: TOnTableUpdate; fOnRecordUpdate: TOnRecordUpdate; function GetForceBlobTransfert: Boolean; procedure SetForceBlobTransfert(Value: boolean); function GetForceBlobTransfertTable(aTable: TSQLRecordClass): Boolean; procedure SetForceBlobTransfertTable(aTable: TSQLRecordClass; aValue: Boolean); /// get a member from its ID // - implements REST GET collection // - returns the data of this object as JSON // - override this method for proper data retrieval from the database engine // - this method must be implemented in a thread-safe manner function ClientRetrieve(TableModelIndex: integer; ID: TID; ForUpdate: boolean; var InternalState: cardinal; var Resp: RawUTF8): boolean; virtual; abstract; /// this method is called before updating any record // - should return FALSE to force no update // - can be use to update some field values just before saving to the database // (e.g. for digital signing purpose) // - this default method just return TRUE (i.e. OK to update) function BeforeUpdateEvent(Value: TSQLRecord): Boolean; virtual; /// overridden method which will call ClientRetrieve() function EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; override; /// create a new member // - implements REST POST collection // - URI is 'ModelRoot/TableName' with POST method // - if SendData is true, content of Value is sent to the server as JSON // - if ForceID is true, client sends the Value.ID field to use this ID // - server must return Status 201/HTTP_CREATED on success // - server must send on success an header entry with // $ Location: ModelRoot/TableName/TableID // - on success, returns the new RowID value; on error, returns 0 // - on success, Value.ID is updated with the new RowID // - if aValue is TSQLRecordFTS3, Value.ID is stored to the virtual table // - this overridden method will send BLOB fields, if ForceBlobTransfert is set function InternalAdd(Value: TSQLRecord; SendData: boolean; CustomFields: PSQLFieldBits; ForceID, DoNotAutoComputeFields: boolean): TID; override; public /// update a member // - implements REST PUT collection // - URI is 'ModelRoot/TableName/TableID' with PUT method // - server must return Status 200/HTTP_SUCCESS OK on success // - this overridden method will call BeforeUpdateEvent and also update BLOB // fields, if any ForceBlobTransfert is set and CustomFields=[] function Update(Value: TSQLRecord; const CustomFields: TSQLFieldBits=[]; DoNotAutoComputeFields: boolean=false): boolean; override; /// get a member from its ID // - implements REST GET collection // - URI is 'ModelRoot/TableName/TableID' with GET method // - server must return Status 200/HTTP_SUCCESS OK on success // - if ForUpdate is true, the REST method is LOCK and not GET: it tries to lock // the corresponding record, then retrieve its content; caller has to call // UnLock() method after Value usage, to release the record function Retrieve(aID: TID; Value: TSQLRecord; ForUpdate: boolean=false): boolean; override; /// get a member from its ID // - implements REST GET collection // - URI is 'ModelRoot/TableName/TableID' with GET method // - returns true on server returned 200/HTTP_SUCCESS OK success, false on error // - set Refreshed to true if the content changed function Refresh(aID: TID; Value: TSQLRecord; var Refreshed: boolean): boolean; /// retrieve a list of members as a TSQLTable // - implements REST GET collection // - default SQL statement is 'SELECT ID FROM TableName;' (i.e. retrieve // the list of all ID of this collection members) // - optional SQLSelect parameter to change the returned fields // as in 'SELECT SQLSelect FROM TableName;' // - optional SQLWhere parameter to change the search range or ORDER // as in 'SELECT SQLSelect FROM TableName WHERE SQLWhere;' // - using inlined parameters via :(...): in SQLWhere is always a good idea // - for one TClass, you should better use TSQLRest.MultiFieldValues() function List(const Tables: array of TSQLRecordClass; const SQLSelect: RawUTF8 = 'RowID'; const SQLWhere: RawUTF8 = ''): TSQLTableJSON; virtual; abstract; /// retrieve a list of members as a TSQLTable // - implements REST GET collection // - in this version, the WHERE clause can be created with the same format // as FormatUTF8() function, replacing all '%' chars with Args[] values // - using inlined parameters via :(...): in SQLWhereFormat is always a good idea // - for one TClass, you should better use TSQLRest.MultiFieldValues() // - will call the List virtual method internaly function ListFmt(const Tables: array of TSQLRecordClass; const SQLSelect, SQLWhereFormat: RawUTF8; const Args: array of const): TSQLTableJSON; overload; /// retrieve a list of members as a TSQLTable // - implements REST GET collection // - in this version, the WHERE clause can be created with the same format // as FormatUTF8() function, replacing all '%' chars with Args[], and all '?' // chars with Bounds[] (inlining them with :(...): and auto-quoting strings) // - example of use: // ! Table := ListFmt([TSQLRecord],'Name','ID=?',[],[aID]); // - for one TClass, you should better use TSQLRest.MultiFieldValues() // - will call the List virtual method internaly function ListFmt(const Tables: array of TSQLRecordClass; const SQLSelect, SQLWhereFormat: RawUTF8; const Args, Bounds: array of const): TSQLTableJSON; overload; /// begin a transaction (calls REST BEGIN Member) // - by default, Client transaction will use here a pseudo session // - in aClient-Server environment with multiple Clients connected at the // same time, you should better use BATCH process, specifying a positive // AutomaticTransactionPerRow parameter to BatchStart() function TransactionBegin(aTable: TSQLRecordClass; SessionID: cardinal=CONST_AUTHENTICATION_NOT_USED): boolean; override; /// end a transaction (calls REST END Member) // - by default, Client transaction will use here a pseudo session procedure Commit(SessionID: cardinal=CONST_AUTHENTICATION_NOT_USED; RaiseException: boolean=false); override; /// abort a transaction (calls REST ABORT Member) // - by default, Client transaction will use here a pseudo session procedure RollBack(SessionID: cardinal=CONST_AUTHENTICATION_NOT_USED); override; /// access or initialize the internal IoC resolver, used for interface-based // remote services, and more generaly any Services.Resolve() call // - create and initialize the internal TServiceContainerClient if no // service interface has been registered yet // - may be used to inject some dependencies, which are not interface-based // remote services, but internal IoC, without the ServiceRegister() // or ServiceDefine() methods - e.g. // ! aRest.ServiceContainer.InjectResolver([TInfraRepoUserFactory.Create(aRest)],true); function ServiceContainer: TServiceContainer; override; /// if set to TRUE, all BLOB fields of all tables will be transferred // between the Client and the remote Server // - i.e. Add() Update() will use Blob-related RESTful PUT/POST request // - i.e. Retrieve() will use Blob-related RESTful GET request // - note that the Refresh method won't handle BLOB fields, even if this // property setting is set to TRUE // - by default, this property is set to FALSE, which setting will spare // bandwidth and CPU // - this property is global to all tables of the model - you can also use // ForceBlobTransfertTable[] to force it for a particular table property ForceBlobTransfert: boolean read GetForceBlobTransfert write SetForceBlobTransfert; /// if set to TRUE for a specified table of the model, all BLOB fields of // this tables will be transferred between the Client and the remote Server // - i.e. Add() Update() will use BLOB-related RESTful PUT/POST request for // this table // - i.e. Retrieve() will use BLOB-related RESTful GET request for // this table // - note that the Refresh method won't handle BLOB fields, even if this // property setting is set to TRUE // - by default, all items of this property are set to FALSE, which // setting will spare bandwidth and CPU // - this property is particular to a given tables of the model - you can // also use ForceBlobTransfert to force it for a all tables of this model property ForceBlobTransfertTable[aTable: TSQLRecordClass]: Boolean read GetForceBlobTransfertTable write SetForceBlobTransfertTable; /// this Event is called by UpdateFromServer() to let the Client adapt to // some rows update (for Marked[] e.g.) property OnTableUpdate: TOnTableUpdate read fOnTableUpdate write fOnTableUpdate; /// this Event is called by Update() to let the client // perform the record update (refresh associated report e.g.) property OnRecordUpdate: TOnRecordUpdate read fOnRecordUpdate write fOnRecordUpdate; end; /// used by TSQLRestClientURI.URI() to let the client ask for an User name // and password, in order to retry authentication to the server // - should return TRUE if aUserName and aPassword both contain some entered // values to be sent for remote secure authentication // - should return FALSE if the user pressed cancel or the number of Retry // reached a defined limit // - here input/output parameters are defined as plain string, to match the // type expected by the client's User Interface, via VCL properties, or // e.g. from TLoginForm as defined in mORMotUILogin.pas unit TOnAuthentificationFailed = function(Retry: integer; var aUserName, aPassword: string; out aPasswordHashed: boolean): boolean of object; /// called by TSQLRestClientURI.URI() when an error occurred // - so that you may have a single entry point for all client-side issues // - information will be available in Sender's LastErrorCode and // LastErrorMessage properties // - if the error comes from an Exception, it will be supplied as parameter // - the REST context (if any) will be supplied within the Call parameter, // and in this case Call^.OutStatus=HTTP_NOTIMPLEMENTED indicates a broken // connection TOnClientFailed = procedure(Sender: TSQLRestClientURI; E: Exception; Call: PSQLRestURIParams) of object; /// store information about registered interface callbacks TSQLRestClientCallbackItem = record /// the identifier of the callback, as sent to the server side // - computed from TSQLRestClientURICallbacks.fCurrentID counter ID: integer; /// weak pointer typecast to the associated IInvokable variable Instance: pointer; //// information about the associated IInvokable Factory: TInterfaceFactory; /// set to TRUE if the instance was released from the server ReleasedFromServer: boolean; end; /// points to information about registered interface callbacks PSQLRestClientCallbackItem = ^TSQLRestClientCallbackItem; /// store the references to active interface callbacks on a REST Client TSQLRestClientCallbacks = class(TSynPersistentLock) protected fCurrentID: integer; function UnRegisterByIndex(index: integer): boolean; public /// the associated REST instance Owner: TSQLRestClientURI; /// how many callbacks are registered Count: integer; /// list of registered interface callbacks List: array of TSQLRestClientCallbackItem; /// initialize the storage list constructor Create(aOwner: TSQLRestClientURI); reintroduce; /// register a callback event interface instance from a new computed ID function DoRegister(aInstance: pointer; aFactory: TInterfaceFactory): integer; overload; /// register a callback event interface instance from its supplied ID procedure DoRegister(aID: Integer; aInstance: pointer; aFactory: TInterfaceFactory); overload; /// delete all callback events from the internal list, as specified by its instance // - note that the same IInvokable instance may be registered for several IDs function UnRegister(aInstance: pointer): boolean; overload; /// find the index of the ID in the internal list // - warning: this method should be called within Safe.Lock/Safe.Unlock function FindIndex(aID: integer): integer; /// find a matching callback // - will call FindIndex(aItem.ID) within Safe.Lock/Safe.Unlock // - returns TRUE if aItem.ID was found and aItem filled, FALSE otherwise function FindEntry(var aItem: TSQLRestClientCallbackItem): boolean; /// find a matching entry // - will call FindIndex(aID) within Safe.Lock/Safe.Unlock // - returns TRUE if aID was found and aInstance/aFactory set, FALSE otherwise function FindAndRelease(aID: integer): boolean; end; /// signature e.g. of the TSQLRestClientURI.OnSetUser event handler TOnRestClientNotify = procedure(Sender: TSQLRestClientURI) of object; /// a generic REpresentational State Transfer (REST) client with URI // - URI are standard Collection/Member implemented as ModelRoot/TableName/TableID // - handle RESTful commands GET POST PUT DELETE LOCK UNLOCK TSQLRestClientURI = class(TSQLRestClient) protected fComputeSignature: TSQLRestServerAuthenticationSignedURIComputeSignature; fOnAuthentificationFailed: TOnAuthentificationFailed; fOnSetUser: TOnRestClientNotify; fMaximumAuthentificationRetry: Integer; fRetryOnceOnTimeout: boolean; fLastErrorCode: integer; fLastErrorMessage: RawUTF8; fLastErrorException: ExceptClass; fBatchCurrent: TSQLRestBatch; /// private values created by sucessfull SetUser() method fSessionUser: TSQLAuthUser; fSessionID: cardinal; fSessionIDHexa8: RawUTF8; fSessionPrivateKey: cardinal; fSessionLastTick64: Int64; fSessionAuthentication: TSQLRestServerAuthenticationClass; fSessionHttpHeader: RawUTF8; // e.g. for TSQLRestServerAuthenticationHttpBasic fSessionServer: RawUTF8; fSessionVersion: RawUTF8; fSessionData: RawByteString; fSessionServerTimeout: integer; fSessionHeartbeatSeconds: integer; /// used to make the internal client-side process reintrant fSafe: IAutoLocker; fRemoteLogClass: TSynLog; fRemoteLogOwnedByFamily: boolean; fServicePublishOwnInterfaces: RawUTF8; {$ifdef MSWINDOWS} fServiceNotificationMethodViaMessages: record Wnd: HWND; Msg: UINT; end; {$endif} {$ifndef LVCL} // SyncObjs.TEvent not available in LVCL yet fBackgroundThread: TSynBackgroundThreadEvent; fOnIdle: TOnIdleSynBackgroundThread; fOnFailed: TOnClientFailed; fInternalState: set of (isOpened, isDestroying, isInAuth); fRemoteLogThread: TObject; // private TRemoteLogThread fFakeCallbacks: TSQLRestClientCallbacks; function FakeCallbackRegister(Sender: TServiceFactoryClient; const Method: TServiceMethod; const ParamInfo: TServiceMethodArgument; ParamValue: Pointer): integer; virtual; function FakeCallbackUnregister(Factory: TInterfaceFactory; FakeCallbackID: integer; Instance: pointer): boolean; virtual; procedure OnBackgroundProcess(Sender: TSynBackgroundThreadEvent; ProcessOpaqueParam: pointer); function GetOnIdleBackgroundThreadActive: boolean; {$endif} constructor RegisteredClassCreateFrom(aModel: TSQLModel; aDefinition: TSynConnectionDefinition); override; function GetCurrentSessionUserID: TID; override; procedure SetSessionHeartbeatSeconds(timeout: integer); function InternalRemoteLogSend(const aText: RawUTF8): boolean; procedure InternalNotificationMethodExecute(var Ctxt: TSQLRestURIParams); virtual; procedure SetLastException(E: Exception=nil; ErrorCode: integer=HTTP_BADREQUEST; Call: PSQLRestURIParams=nil); /// will call timestamp/info if the session has currently not been retrieved function GetSessionVersion: RawUTF8; // register the user session to the TSQLRestClientURI instance function SessionCreate(aAuth: TSQLRestServerAuthenticationClass; var aUser: TSQLAuthUser; const aSessionKey: RawUTF8): boolean; procedure SessionRenewEvent(Sender: TSynBackgroundTimer; Event: TWaitResult; const Msg: RawUTF8); /// abstract method to be implemented with a local, piped or HTTP/1.1 provider // - you can specify some POST/PUT data in Call.OutBody (leave '' otherwise) // - return the execution result in Call.OutStatus // - for clients, RestAccessRights is never used procedure InternalURI(var Call: TSQLRestURIParams); virtual; abstract; /// overridden protected method shall check if not connected to reopen it // - shall return TRUE on success, FALSE on any connection error function InternalCheckOpen: boolean; virtual; abstract; /// overridden protected method shall force the connection to be closed, // - a next call to InternalCheckOpen method shall re-open the connection procedure InternalClose; virtual; abstract; /// calls 'ModelRoot/TableName/TableID' with appropriate REST method // - uses GET method if ForUpdate is false // - uses LOCK method if ForUpdate is true function URIGet(Table: TSQLRecordClass; ID: TID; var Resp: RawUTF8; ForUpdate: boolean=false): Int64Rec; // overridden methods function ClientRetrieve(TableModelIndex: integer; ID: TID; ForUpdate: boolean; var InternalState: cardinal; var Resp: RawUTF8): boolean; override; function EngineList(const SQL: RawUTF8; ForceAJAX: Boolean=false; ReturnedRowCount: PPtrInt=nil): RawUTF8; override; function EngineExecute(const SQL: RawUTF8): boolean; override; function EngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; override; function EngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; override; function EngineDelete(TableModelIndex: integer; ID: TID): boolean; override; function EngineDeleteWhere(TableModelIndex: integer; const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean; override; 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 EngineUpdateField(TableModelIndex: integer; const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; override; function EngineBatchSend(Table: TSQLRecordClass; var Data: RawUTF8; var Results: TIDDynArray; ExpectedResultsCount: integer): integer; override; public /// initialize REST client instance constructor Create(aModel: TSQLModel); override; /// release memory and close client connection // - also unlock all still locked records by this client destructor Destroy; override; /// authenticate an User to the current connected Server // - will call the ModelRoot/Auth service, i.e. call TSQLRestServer.Auth() // published method to create a session for this user, with our secure // TSQLRestServerAuthenticationDefault authentication scheme // - returns true on success // - calling this method is optional, depending on your user right policy: // your Server need to handle authentication // - if saoUserByLogonOrID is defined in the server Options, aUserName may // be a TSQLAuthUser.ID integer value and not a TSQLAuthUser.LogonName // - on success, the SessionUser property map the logged user session on the // server side // - if aHashedPassword is TRUE, the aPassword parameter is expected to // contain the already-hashed value, just as stored in PasswordHashHexa // (i.e. SHA256('salt'+Value) as in TSQLAuthUser.SetPasswordPlain method) // - if SSPIAUTH conditional is defined, and aUserName='', a Windows // authentication will be performed via TSQLRestServerAuthenticationSSPI - // in this case, aPassword will contain the SPN domain for Kerberos // (otherwise NTLM will be used), and table TSQLAuthUser shall contain // an entry for the logged Windows user, with the LoginName in form // 'DomainName\UserName' // - you can directly create the class method ClientSetUser() of a given // TSQLRestServerAuthentication inherited class, if neither // TSQLRestServerAuthenticationDefault nor TSQLRestServerAuthenticationSSPI // match your need function SetUser(const aUserName, aPassword: RawUTF8; aHashedPassword: Boolean=false): boolean; /// customize the session_signature signing algorithm with a specific function // - will be used by TSQLRestServerAuthenticationSignedURI classes, // e.g. TSQLRestServerAuthenticationDefault instead of the algorithm // specified by the server at session handshake property ComputeSignature: TSQLRestServerAuthenticationSignedURIComputeSignature read fComputeSignature write fComputeSignature; /// save the TSQLRestClientURI properties into a persistent storage object // - CreateFrom() will expect Definition.UserName/Password to store the // credentials which will be used by SetUser() procedure DefinitionTo(Definition: TSynConnectionDefinition); override; /// clear session and call the /auth service on the server to notify shutdown // - is called by Destroy and SetUser/ClientSetUser methods, so you should // not have usually to call this method directly procedure SessionClose; /// method calling the remote Server via a RESTful command // - calls the InternalURI abstract method, which should be overridden with a // local, piped or HTTP/1.1 provider // - this method will add sign the url with the appropriate digital signature // according to the current SessionUser property // - this method will retry the connection in case of authentication failure // (i.e. if the session was closed by the remote server, for any reason - // mostly a time out) if the OnAuthentificationFailed event handler is set function URI(const url, method: RawUTF8; Resp: PRawUTF8=nil; Head: PRawUTF8=nil; SendData: PRawUTF8=nil): Int64Rec; /// retrieve a list of members as a TSQLTable // - implements REST GET collection // - URI is 'ModelRoot/TableName' with GET method // - SQLSelect and SQLWhere are encoded as 'select=' and 'where=' URL parameters // (using inlined parameters via :(...): in SQLWhere is always a good idea) // - server must return Status 200/HTTP_SUCCESS OK on success function List(const Tables: array of TSQLRecordClass; const SQLSelect: RawUTF8 = 'RowID'; const SQLWhere: RawUTF8 = ''): TSQLTableJSON; override; /// unlock the corresponding record // - URI is 'ModelRoot/TableName/TableID' with UNLOCK method // - returns true on success function UnLock(Table: TSQLRecordClass; aID: TID): boolean; override; /// Execute directly a SQL statement, expecting a list of resutls // - URI is 'ModelRoot' with GET method, and SQL statement sent as UTF-8 // - return a result table on success, nil on failure function ExecuteList(const Tables: array of TSQLRecordClass; const SQL: RawUTF8): TSQLTableJSON; override; /// ask the server for its current internal state revision counter // - this counter is incremented every time the database is modified // - the returned value is 0 if the database doesn't support this feature // - TSQLTable does compare this value with its internal one to check if // its content must be updated function ServerInternalState: cardinal; /// check if the data may have changed of the server for this objects, and // update it if possible // - only working types are TSQLTableJSON and TSQLRecord descendants // - make use of the InternalState function to check the data content revision // - return true if Data is updated successfully, or false on any error // during data retrieval from server (e.g. if the TSQLRecord has been deleted) // - if Data contains only one TSQLTableJSON, PCurrentRow can point to the // current selected row of this table, in order to refresh its value // - use this method to refresh the client UI, e.g. via a timer function UpdateFromServer(const Data: array of TObject; out Refreshed: boolean; PCurrentRow: PInteger=nil): boolean; virtual; /// send a flush command to the remote Server cache // - this method will remotely call the Cache.Flush() methods of the server // instance, to force cohesion of the data // - ServerCacheFlush() with no parameter will flush all stored JSON content // - ServerCacheFlush(aTable) will flush the cache for a given table // - ServerCacheFlush(aTable,aID) will flush the cache for a given record function ServerCacheFlush(aTable: TSQLRecordClass=nil; aID: TID=0): boolean; virtual; /// you can call this method to call the remote URI root/Timestamp // - this can be an handy way of testing the connection, since this method // is always available, even without authentication // - returns TRUE if the client time correction has been retrieved // - returns FALSE on any connection error - check LastErrorMessage and // LastErrorException to find out the exact connection error function ServerTimestampSynchronize: boolean; /// asynchronous call a 'RemoteLog' remote logging method on the server // - as implemented by mORMot's LogView tool in server mode // - to be used via ServerRemoteLogStart/ServerRemoteLogStop methods // - a dedicated background thread will run the transmission process without // blocking the main program execution, gathering log rows in chunks in case // of high activity // - map TOnTextWriterEcho signature, so that you will be able to set e.g.: // ! TSQLLog.Family.EchoCustom := aClient.ServerRemoteLog; function ServerRemoteLog(Sender: TTextWriter; Level: TSynLogInfo; const Text: RawUTF8): boolean; overload; virtual; /// internal method able to emulate a call to TSynLog.Add.Log() // - will compute timestamp and event text, than call the overloaded // ServerRemoteLog() method function ServerRemoteLog(Level: TSynLogInfo; FormatMsg: PUTF8Char; const Args: array of const): boolean; overload; /// start to send all logs to the server 'RemoteLog' method-based service // - will associate the EchoCustom callback of the running log class to the // ServerRemoteLog() method // - if aClientOwnedByFamily is TRUE, this TSQLRestClientURI instance // lifetime will be managed by TSynLogFamily - which is mostly wished // - if aClientOwnedByFamily is FALSE, you should manage this instance // life time, and may call ServerRemoteLogStop to stop remote logging // - warning: current implementation will disable all logging for this // TSQLRestClientURI instance, to avoid any potential concern (e.g. for // multi-threaded process, or in case of communication error): you should // therefore use this TSQLRestClientURI connection only for the remote log // server, e.g. via TSQLHttpClientGeneric.CreateForRemoteLogging() - do // not call ServerRemoteLogStart() from a high-level business client! procedure ServerRemoteLogStart(aLogClass: TSynLogClass; aClientOwnedByFamily: boolean); /// stop sending all logs to the server 'RemoteLog' method-based service // - do nothing if aClientOwnedByFamily was TRUE for ServerRemoteLogStart procedure ServerRemoteLogStop; /// begin a transaction // - implements REST BEGIN collection // - in aClient-Server environment with multiple Clients connected at the // same time, you should better use BATCH process, specifying a positive // AutomaticTransactionPerRow parameter to BatchStart() // - may be used to speed up some SQL statements as Add/Update/Delete methods // - must be ended with Commit on success // - in the current implementation, the aTable parameter is not used yet // - must be aborted with Rollback if any SQL statement failed // - return true if no transaction is active, false otherwise // !if Client.TransactionBegin(TSQLRecordPeopleObject) then // !try // ! // .... modify the database content, raise exceptions on error // ! Client.Commit; // !except // ! Client.RollBack; // in case of error // !end; // - you may use the dedicated TransactionBeginRetry() method in case of // potential Client concurrent access function TransactionBegin(aTable: TSQLRecordClass; SessionID: cardinal=1): boolean; override; /// begin a transaction // - implements REST BEGIN collection // - in aClient-Server environment with multiple Clients connected at the // same time, you should better use BATCH process, specifying a positive // AutomaticTransactionPerRow parameter to BatchStart() // - this version retries a TranslationBegin() to be successfull within // a supplied number of times // - will retry every 100 ms for "Retries" times (excluding the connection // time in this 100 ms time period // - default is to retry 10 times, i.e. within 2 second timeout // - in the current implementation, the aTable parameter is not used yet // - typical usage should be for instance: // !if Client.TransactionBeginRetry(TSQLRecordPeopleObject,20) then // !try // ! // .... modify the database content, raise exceptions on error // ! Client.Commit; // !except // ! Client.RollBack; // in case of error // !end; function TransactionBeginRetry(aTable: TSQLRecordClass; Retries: integer=10): boolean; /// end a transaction // - implements REST END collection // - write all pending SQL statements to the disk } procedure Commit(SessionID: cardinal=CONST_AUTHENTICATION_NOT_USED; RaiseException: boolean=false); override; /// abort a transaction // - implements REST ABORT collection // - restore the previous state of the database, before the call to TransactionBegin } procedure RollBack(SessionID: cardinal=CONST_AUTHENTICATION_NOT_USED); override; /// begin a BATCH sequence to speed up huge database change for a given table // - is a wrapper around TSQLRestBatch.Create() which will be stored in this // TSQLRestClientURI instance - be aware that this won't be thread-safe // - if you need a thread-safe "Unit Of Work" process, please use a private // TSQLRestBatch instance and the overloaded TSQLRest.BatchSend() method // - call BatchStartAny() or set the aTable parameter to nil if you want to // use any kind of TSQLRecord objects within the process, not a single one function BatchStart(aTable: TSQLRecordClass; AutomaticTransactionPerRow: cardinal=0; Options: TSQLRestBatchOptions=[]): boolean; virtual; /// begin a BATCH sequence to speed up huge database change for any table // - will call the BatchStart() method with aTable = nil so that you may be // able to use any kind of TSQLRecord class within the process // - is a wrapper around TSQLRestBatch.Create() which will be stored in this // TSQLRestClientURI instance - be aware that this won't be thread-safe function BatchStartAny(AutomaticTransactionPerRow: cardinal; Options: TSQLRestBatchOptions=[]): boolean; /// create a new member in current BATCH sequence // - is a wrapper around TSQLRestBatch.Add() which will be stored in this // TSQLRestClientURI instance - be aware that this won't be thread safe function BatchAdd(Value: TSQLRecord; SendData: boolean; ForceID: boolean=false; const CustomFields: TSQLFieldBits=[]): integer; /// update a member in current BATCH sequence // - is a wrapper around TSQLRestBatch.Update() which will be stored in this // TSQLRestClientURI instance - be aware that this won't be thread safe // - this method will call BeforeUpdateEvent before TSQLRestBatch.Update function BatchUpdate(Value: TSQLRecord; const CustomFields: TSQLFieldBits=[]; DoNotAutoComputeFields: boolean=false): integer; /// delete a member in current BATCH sequence // - is a wrapper around TSQLRestBatch.Delete() which will be stored in this // TSQLRestClientURI instance - be aware that this won't be thread safe function BatchDelete(ID: TID): integer; overload; /// delete a member in current BATCH sequence // - is a wrapper around TSQLRestBatch.Delete() which will be stored in this // TSQLRestClientURI instance - be aware that this won't be thread safe function BatchDelete(Table: TSQLRecordClass; ID: TID): integer; overload; /// retrieve the current number of pending transactions in the BATCH sequence // - every call to BatchAdd/Update/Delete methods increases this count function BatchCount: integer; /// execute a BATCH sequence started by BatchStart method // - send all pending BatchAdd/Update/Delete statements to the remote server // - URI is 'ModelRoot/TableName/0' with POST (or PUT) method // - will return the URI Status value, i.e. 200/HTTP_SUCCESS OK on success // - a dynamic array of integers will be created in Results, // containing all ROWDID created for each BatchAdd call, 200 (=HTTP_SUCCESS) // for all successfull BatchUpdate/BatchDelete, or 0 on error // - any error during server-side process MUST be checked against Results[] // (the main URI Status is 200 if about communication success, and won't // imply that all statements in the BATCH sequence were successfull function BatchSend(var Results: TIDDynArray): integer; overload; /// abort a BATCH sequence started by BatchStart method // - in short, nothing is sent to the remote server, and current BATCH // sequence is closed // - will Free the TSQLRestBatch stored in this TSQLRestClientURI instance procedure BatchAbort; /// wrapper to the protected URI method to call a method on the server, using // a ModelRoot/[TableName/[ID/]]MethodName RESTful GET request // - returns the HTTP error code (e.g. 200/HTTP_SUCCESS on success) // - this version will use a GET with supplied parameters (which will be encoded // with the URL) function CallBackGet(const aMethodName: RawUTF8; const aNameValueParameters: array of const; out aResponse: RawUTF8; aTable: TSQLRecordClass=nil; aID: TID=0; aResponseHead: PRawUTF8=nil): integer; /// wrapper to the protected URI method to call a method on the server, using // a ModelRoot/[TableName/[ID/]]MethodName RESTful GET request // - returns the UTF-8 decoded JSON result (server must reply with one // "result":"value" JSON object) // - this version will use a GET with supplied parameters (which will be encoded // with the URL) function CallBackGetResult(const aMethodName: RawUTF8; const aNameValueParameters: array of const; aTable: TSQLRecordClass=nil; aID: TID=0): RawUTF8; /// wrapper to the protected URI method to call a method on the server, using // a ModelRoot/[TableName/[ID/]]MethodName RESTful PUT request // - returns the HTTP error code (e.g. 200/HTTP_SUCCESS on success) // - this version will use a PUT with the supplied raw UTF-8 data function CallBackPut(const aMethodName, aSentData: RawUTF8; out aResponse: RawUTF8; aTable: TSQLRecordClass=nil; aID: TID=0; aResponseHead: PRawUTF8=nil): integer; /// wrapper to the protected URI method to call a method on the server, using // a ModelRoot/[TableName/[ID/]]MethodName RESTful with any kind of request // - returns the HTTP error code (e.g. 200/HTTP_SUCCESS on success) // - for GET/PUT methods, you should better use CallBackGet/CallBackPut function CallBack(method: TSQLURIMethod; const aMethodName,aSentData: RawUTF8; out aResponse: RawUTF8; aTable: TSQLRecordClass=nil; aID: TID=0; aResponseHead: PRawUTF8=nil): integer; /// to be called before CallBack() if the client could ignore the answer // - do nothing by default, but overriden e.g. in TSQLHttpClientWebsockets procedure CallbackNonBlockingSetHeader(out Header: RawUTF8); virtual; /// register one or several Services on the client side via their interfaces // - this methods expects a list of interfaces to be registered to the client // (e.g. [TypeInfo(IMyInterface)]) // - instance implementation pattern will be set by the appropriate parameter // - will return true on success, false if registration failed (e.g. if any of // the supplied interfaces is not correct or is not available on the server) // - that is, server side will be called to check for the availability of // each interface // - you can specify an optional custom contract for the first interface function ServiceRegister(const aInterfaces: array of PTypeInfo; aInstanceCreation: TServiceInstanceImplementation=sicSingle; const aContractExpected: RawUTF8=''): boolean; overload; virtual; /// register a Service on the client side via its interface // - this methods expects one interface to be registered to the client, as // ! Client.ServiceRegister(TypeInfo(IMyInterface),sicShared); // - instance implementation pattern will be set by the appropriate parameter // - will return the corresponding fake class factory on success, nil if // registration failed (e.g. if any of supplied interfaces is not correct or // is not available on the server) // - that is, server side will be called to check for the availability of // each interface // - you can specify an optional custom contract for the first interface function ServiceRegister(aInterface: PTypeInfo; aInstanceCreation: TServiceInstanceImplementation=sicSingle; const aContractExpected: RawUTF8=''; aIgnoreAnyException: boolean=true): TServiceFactory; overload; /// register and retrieve the sicClientDriven Service instance // - will return TRUE on success, filling Obj output variable with the // corresponding interface instance // - will return FALSE on error function ServiceRegisterClientDriven(aInterface: PTypeInfo; out Obj; const aContractExpected: RawUTF8=''): boolean; overload; /// register one or several Services on the client side via their interfaces // - this method expects the interface(s) to have been registered previously: // ! TInterfaceFactory.RegisterInterfaces([TypeInfo(IMyInterface),...]); function ServiceDefine(const aInterfaces: array of TGUID; aInstanceCreation: TServiceInstanceImplementation=sicSingle; const aContractExpected: RawUTF8=''): boolean; overload; /// register a Service on the client side via its interface // - this method expects the interface to have been registered previously: // ! TInterfaceFactory.RegisterInterfaces([TypeInfo(IMyInterface),...]); function ServiceDefine(const aInterface: TGUID; aInstanceCreation: TServiceInstanceImplementation=sicSingle; const aContractExpected: RawUTF8=''; aIgnoreAnyException: boolean=true): TServiceFactoryClient; overload; /// register and retrieve the sicClientDriven Service instance // - this method expects the interface to have been registered previously: // ! TInterfaceFactory.RegisterInterfaces([TypeInfo(IMyInterface),...]); function ServiceDefineClientDriven(const aInterface: TGUID; out Obj; const aContractExpected: RawUTF8=''): boolean; /// register a sicShared Service instance communicating via JSON objects // - will force SERVICE_CONTRACT_NONE_EXPECTED, ParamsAsJSONObject=true and // ResultAsJSONObjectWithoutResult=true // - may be used e.g. for accessing a sessionless public REST/JSON API, i.e. // ! TSQLRestServer.ServiceDefine(...).ResultAsJSONObjectWithoutResult := true // - this method expects the interface to have been registered previously: // ! TInterfaceFactory.RegisterInterfaces([TypeInfo(IMyInterface),...]); // - aIgnoreAnyException may be set to TRUE if the server is likely // to not propose this service, and any exception is to be catched function ServiceDefineSharedAPI(const aInterface: TGUID; const aContractExpected: RawUTF8=SERVICE_CONTRACT_NONE_EXPECTED; aIgnoreAnyException: boolean=false): TServiceFactoryClient; /// allow to notify a server the services this client may be actually capable // - when this client will connect to a remote server to access its services, // it will register its own services, supplying its TSQLRestServer instance, // and its corresponding public URI, within its '_contract_' internal call // - it will allow automatic service discovery of Peer To Peer Servers, // without the need of an actual centralized SOA catalog service: any // client could retrieve an associated REST server for a given service, // via the ServiceRetrieveAssociated method procedure ServicePublishOwnInterfaces(OwnServer: TSQLRestServer); /// return all REST server URI associated to this client, for a given // service name, the latest registered in first position // - will lookup for the Interface name without the initial 'I', e.g. // 'Calculator' for ICalculator - warning: research is case-sensitive // - this methods is the reverse from ServicePublishOwnInterfaces: it allows // to guess an associated REST server which may implement a given service function ServiceRetrieveAssociated(const aServiceName: RawUTF8; out URI: TSQLRestServerURIDynArray): boolean; overload; /// return all REST server URI associated to this client, for a given service // - here the service is specified as its TGUID, e.g. IMyInterface // - this method expects the interface to have been registered previously: // ! TInterfaceFactory.RegisterInterfaces([TypeInfo(IMyInterface),...]); // - the URI[] output array contains the matching server URIs, the latest // registered in first position // - this methods is the reverse from ServicePublishOwnInterfaces: it allows // to guess an associated REST server which may implement a given service function ServiceRetrieveAssociated(const aInterface: TGUID; out URI: TSQLRestServerURIDynArray): boolean; overload; {$ifdef MSWINDOWS} /// set a HWND/WM_* pair to let interface-based services notification // callbacks be processed safely in the main UI thread, via Windows messages // - by default callbacks are executed in the transmission thread, e.g. // the WebSockets client thread: using VCL Synchronize() method may // trigger some unexpected race conditions, e.g. when asynchronous // notifications are received during a blocking REST command - this // message-based mechanism will allow safe and easy notification for // any VCL client application // - the associated ServiceNotificationMethodExecute() method shall be // called in the client HWND TForm for the defined WM_* message procedure ServiceNotificationMethodViaMessages(hWnd: HWND; Msg: UINT); /// event to be triggered when a WM_* message is received from // the internal asynchronous notification system, to run the callback // in the main UI thread // - WM_* message identifier should have been set e.g. via the associated // ServiceNotificationMethodViaMessages(Form.Handle,WM_USER) // - message will be sent for any interface-based service method callback // which expects no result (i.e. no out parameter nor function result), // so is safely handled as asynchronous notification // - is defines as a class procedure, since the underlying TSQLRestClientURI // instance has no impact here: a single WM_* handler is enough for // several TSQLRestClientURI instances class procedure ServiceNotificationMethodExecute(var Msg : TMessage); {$endif MSWINDOWS} published /// low-level error code, as returned by server // - check this value about HTTP_* constants // - HTTP_SUCCESS or HTTP_CREATED mean no error // - otherwise, check LastErrorMessage property for additional information // - this property value will record status codes returned by URI() method property LastErrorCode: integer read fLastErrorCode; /// low-level error message, as returned by server // - this property value will record content returned by URI() method in // case of an error, or '' if LastErrorCode is HTTP_SUCCESS or HTTP_CREATED property LastErrorMessage: RawUTF8 read fLastErrorMessage; /// low-level exception class, if any // - will record any Exception class raised within URI() method // - contains nil if URI() execution did not raise any exception (which // is the most expected behavior, since server-side errors are trapped // into LastErrorCode/LastErrorMessage properties property LastErrorException: ExceptClass read fLastErrorException; /// maximum additional retry occurence // - defaut is 1, i.e. will retry once // - set OnAuthentificationFailed to nil in order to avoid any retry property MaximumAuthentificationRetry: Integer read fMaximumAuthentificationRetry write fMaximumAuthentificationRetry; /// if the client shall retry once in case of "408 REQUEST TIMEOUT" error property RetryOnceOnTimeout: Boolean read fRetryOnceOnTimeout write fRetryOnceOnTimeout; /// the current session ID as set after a successfull SetUser() method call // - equals 0 (CONST_AUTHENTICATION_SESSION_NOT_STARTED) if the session // is not started yet - i.e. if SetUser() call failed // - equals 1 (CONST_AUTHENTICATION_NOT_USED) if authentication mode // is not enabled - i.e. after a fresh Create() without SetUser() call property SessionID: cardinal read fSessionID; /// the remote server executable name, as retrieved after a SetUser() success property SessionServer: RawUTF8 read fSessionServer; /// the remote server version, as retrieved after a SetUser() success property SessionVersion: RawUTF8 read fSessionVersion; /// the remote server session tiemout in minutes, as retrieved after // a SetUser() success // - will be used to set SessionHeartbeatSeconds default property SessionServerTimeout: integer read fSessionServerTimeout; /// frequency of Callback/_ping_ calls to maintain session and services // - will be used to call SessionRenewEvent at the specified period, so that // the session and all sicClientDriven instances will be maintained on the // server side as long as the client connection stands // - equals half SessionServerTimeout or 25 minutes (if lower) by default - // 25 minutes matches the default service timeout of 30 minutes // - you may set 0 to disable this SOA-level heartbeat feature property SessionHeartbeatSeconds: integer read fSessionHeartbeatSeconds write SetSessionHeartbeatSeconds; public /// the current user as set by SetUser() method // - contans nil if no User is currently authenticated // - once authenticated, a TSQLAuthUser instance is set, with its ID, // LogonName, DisplayName, PasswordHashHexa and GroupRights (filled with a // TSQLAuthGroup ID casted as a pointer) properties - you can retrieve any // optional binary data associated with this user via RetrieveBlobFields() property SessionUser: TSQLAuthUser read fSessionUser; /// access to the low-level HTTP header used for authentication // - you can force here your own header, e.g. a JWT as authentication bearer // or as in TSQLRestServerAuthenticationHttpAbstract.ClientSetUserHttpOnlyUser property SessionHttpHeader: RawUTF8 read fSessionHttpHeader write fSessionHttpHeader; {$ifndef LVCL} /// set a callback event to be executed in loop during remote blocking // process, e.g. to refresh the UI during a somewhat long request // - if not set, the request will be executed in the current thread, // so may block the User Interface // - you can assign a callback to this property, calling for instance // Application.ProcessMessages, to execute the remote request in a // background thread, but let the UI still be reactive: the // TLoginForm.OnIdleProcess and OnIdleProcessForm methods of // mORMotUILogin.pas will match this property expectations property OnIdle: TOnIdleSynBackgroundThread read fOnIdle write fOnIdle; /// TRUE if the background thread is active, and OnIdle event is called // during process // - to be used e.g. to ensure no re-entrance from User Interface messages property OnIdleBackgroundThreadActive: Boolean read GetOnIdleBackgroundThreadActive; {$endif} /// this Event is called in case of remote authentication failure // - client software can ask the user to enter a password and user name // - if no event is specified, the URI() method will return directly // an HTTP_FORBIDDEN "403 Forbidden" error code property OnAuthentificationFailed: TOnAuthentificationFailed read fOnAuthentificationFailed write fOnAuthentificationFailed; /// this Event is called if URI() was not successfull // - the callback will have all needed information // - e.g. Call^.OutStatus=HTTP_NOTIMPLEMENTED indicates a broken connection property OnFailed: TOnClientFailed read fOnFailed write fOnFailed; /// this Event is called when a user is authenticated // - is called always, on each TSQLRestClientURI.SetUser call // - you can check the Sender.SessionUser property pointing to the current // authenticated user, or nil if authentication failed // - could be used to refresh the User Interface layout according to // current authenticated user rights, or to subscribe to some services // via callbacks property OnSetUser: TOnRestClientNotify read fOnSetUser write fOnSetUser; end; /// Rest client with remote access to a server through a dll // - use only one TURIMapRequest function for the whole communication // - the data is stored in Global system memory, and freed by GlobalFree() TSQLRestClientURIDll = class(TSQLRestClientURI) private /// used by Create(from dll) constructor fLibraryHandle: cardinal; protected Func: TURIMapRequest; /// method calling the RESTful server through a DLL or executable, using // direct memory procedure InternalURI(var Call: TSQLRestURIParams); override; /// overridden protected method do nothing (direct DLL access has no connection) function InternalCheckOpen: boolean; override; /// overridden protected method do nothing (direct DLL access has no connection) procedure InternalClose; override; public /// connect to a server from a remote function constructor Create(aModel: TSQLModel; aRequest: TURIMapRequest); reintroduce; overload; /// connect to a server contained in a shared library // - this dll must contain at least a URIRequest entry // - raise an exception if the shared library is not found or invalid constructor Create(aModel: TSQLModel; const DllName: TFileName); reintroduce; overload; /// release memory and handles destructor Destroy; override; end; /// Rest client with redirection to another TSQLRest instance TSQLRestClientRedirect = class(TSQLRestClientURI) protected fRedirectedServer: TSQLRestServer; fRedirectedClient: TSQLRestClientURI; /// method calling the associated RESTful instance procedure InternalURI(var Call: TSQLRestURIParams); override; /// overridden protected method which returns TRUE if redirection is enabled function InternalCheckOpen: boolean; override; /// this overridden protected method does nothing procedure InternalClose; override; public /// prepare the redirection, to be enabled later via RedirectTo() // - the supplied aModel instance will be owned by this class constructor Create(aModel: TSQLModel); overload; override; /// will pass all client commands to the supplied TSQLRest instance // - aRedirected is expected to be either a TSQLRestClientURI or // a TSQLRestServer // - will make a copy of the aRedirected.Model, and own it constructor Create(aRedirected: TSQLRest); reintroduce; overload; /// will pass all client commands to the supplied TSQLRestServer instance // - aRedirected will be owned by this TSQLRestClientRedirect constructor CreateOwned(aRedirected: TSQLRestServer); reintroduce; /// allows to change redirection to a client on the fly // - if aRedirected is nil, redirection will be disabled and any URI() call // will return an HTTP_GATEWAYTIMEOUT 504 error status procedure RedirectTo(aRedirected: TSQLRest); end; {$ifdef MSWINDOWS} /// Rest client with remote access to a server through Windows messages // - use only one TURIMapRequest function for the whole communication // - the data is sent and received by using the standard and fast WM_COPYDATA message // - named pipes seems to be somewhat better for bigger messages under XP // - this class is thread-safe, since its URI() method is protected by a lock TSQLRestClientURIMessage = class(TSQLRestClientURI) protected /// the HWND of the server process, retrieved by InternalCheckOpen() method fServerWindow: HWND; /// the Window name used of the server process fServerWindowName: string; /// the HWND of the client process, as set by Create() method fClientWindow: HWND; /// the Window name used, if created internaly fClientWindowName: string; /// the time out to be used, in mili seconds fTimeOutMS: cardinal; /// if InternalURI will process the Windows Messages loop fDoNotProcessMessages: boolean; /// the expected current response // - this value is set from the incoming WM_COPYDATA // - this value is set to #0 (i.e. string of one #0 char) while waiting // for a WM_COPYDATA message in URI() method fCurrentResponse: RawUTF8; constructor RegisteredClassCreateFrom(aModel: TSQLModel; aDefinition: TSynConnectionDefinition); override; /// method calling the RESTful server by using Windows WM_COPYDATA messages procedure InternalURI(var Call: TSQLRestURIParams); override; /// overridden protected method to handle Windows Message loop connection function InternalCheckOpen: boolean; override; /// overridden protected method to close Windows Message procedure InternalClose; override; public /// connect to a server from its window name // - ServerWindowName is of UnicodeString type since Delphi 2009 // (direct use of FindWindow()=FindWindowW() Win32 API) // - this version must supply a Client Window handle constructor Create(aModel: TSQLModel; const ServerWindowName: string; ClientWindow: HWND; TimeOutMS: cardinal); reintroduce; overload; /// connect to a server from its window name // - ServerWindowName is of UnicodeString type since Delphi 2009 // (direct use of FindWindow()=FindWindowW() Win32 API) // - this version will instanciante and create a Client Window from // a Window Name, by using low level Win32 API: therefore, the Forms unit // is not needed with this constructor (save some KB) constructor Create(aModel: TSQLModel; const ServerWindowName, ClientWindowName: string; TimeOutMS: cardinal); reintroduce; overload; /// release the internal Window class created, if any destructor Destroy; override; /// save the TSQLRestClientURIMessage properties into a persistent storage object // - CreateFrom() will expect Definition.ServerName to store the // ServerWindowName, and Definition.DatabaseName to be the ClientWindowName procedure DefinitionTo(Definition: TSynConnectionDefinition); override; /// event to be triggered when a WM_COPYDATA message is received from the server // - to be called by the corresponding "message WM_COPYDATA;" method in the // client TForm instance procedure WMCopyData(var Msg : TWMCopyData); message WM_COPYDATA; /// define if the client will process the Windows Messages loop // - set to TRUE if the client is used outside the main GUI application thread property DoNotProcessMessages: boolean read fDoNotProcessMessages write fDoNotProcessMessages; end; /// Rest client with remote access to a server through a Named Pipe // - named pipe is fast and optimized under Windows // - can be accessed localy or remotely // - this class is thread-safe, since its URI() method is protected by a lock TSQLRestClientURINamedPipe = class(TSQLRestClientURI) private /// handle for '\\.\pipe\mORMot_TEST' e.g. fServerPipe: THandle; /// the pipe name fPipeName: TFileName; {$ifndef ANONYMOUSNAMEDPIPE} {$ifndef NOSECURITYFORNAMEDPIPECLIENTS} fPipeSecurityAttributes: TSecurityAttributes; fPipeSecurityDescriptor: array[0..SECURITY_DESCRIPTOR_MIN_LENGTH] of byte; {$endif} {$endif} protected constructor RegisteredClassCreateFrom(aModel: TSQLModel; aDefinition: TSynConnectionDefinition); override; /// method calling the RESTful server through a DLL or executable, by using // a named pipe (faster than TCP/IP or HTTP connection) // - return status code in result.Lo // - return database internal state in result.Hi // - status code 501 HTTP_NOTIMPLEMENTED if no server is available procedure InternalURI(var Call: TSQLRestURIParams); override; /// overridden protected method to handle named-pipe connection function InternalCheckOpen: boolean; override; /// overridden protected method to close named-pipe connection procedure InternalClose; override; public /// connect to a server contained in a running application // - the server must have been declared by a previous // TSQLRestServer.ExportServer(ApplicationName) call // with ApplicationName as user-defined server identifier ('DBSERVER' e.g.) // - ApplicationName is of UnicodeString type since Delphi 2009 // (direct use of Wide Win32 API version) // - this server identifier is appended to '\\.\pipe\mORMot_' to obtain // the full pipe name to connect to ('\\.\pipe\mORMot__DBSERVER' e.g.) // - this server identifier may also contain a remote computer name, and // must be fully qualified ('\\ServerName\pipe\ApplicationName' e.g.) // - raise an exception if the server is not running or invalid constructor Create(aModel: TSQLModel; const ApplicationName: TFileName); reintroduce; /// save the TSQLRestClientURIMessage properties into a persistent storage object // - CreateFrom() will expect Definition.ServerName to store the // expected ApplicationName procedure DefinitionTo(Definition: TSynConnectionDefinition); override; end; {$endif MSWINDOWS} /// will define a validation to be applied to a TSQLRecord field, using // if necessary an associated TSQLRest instance and a TSQLRecord class // - a typical usage is to validate a value to be unique in the table // (implemented in the TSynValidateUniqueField class) // - the optional associated parameters are to be supplied JSON-encoded // - ProcessRest and ProcessRec properties will be filled before Validate // method call by TSQLRecord.Validate() TSynValidateRest = class(TSynValidate) protected fProcessRest: TSQLRest; fProcessRec: TSQLRecord; function DoValidate(aFieldIndex: integer; const Value: RawUTF8; var ErrorMsg: string; aProcessRest: TSQLRest; aProcessRec: TSQLRecord): boolean; virtual; abstract; public function Process(aFieldIndex: integer; const Value: RawUTF8; var ErrorMsg: string): boolean; override; function Validate(aFieldIndex: integer; const Value: RawUTF8; var ErrorMsg: string; aProcessRest: TSQLRest; aProcessRec: TSQLRecord): boolean; /// the associated TSQLRest instance // - this value is updated by Validate with the current // TSQLRest used for the validation // - it can be used in the overridden DoValidate method property ProcessRest: TSQLRest read fProcessRest; /// the associated TSQLRecord instance // - this value is updated by Validate with the current // TSQLRecord instance to be validated // - it can be used in the overridden DoValidate method property ProcessRec: TSQLRecord read fProcessRec; end; /// will define a validation for a TSQLRecord Unique text field // - this class will handle only textual fields, not numeric values // - it will check that the field value is not void // - it will check that the field value is not a duplicate TSynValidateUniqueField = class(TSynValidateRest) protected /// perform the unique field validation action to the specified value function DoValidate(aFieldIndex: integer; const Value: RawUTF8; var ErrorMsg: string; aProcessRest: TSQLRest; aProcessRec: TSQLRecord): boolean; override; end; /// will define an unicity validation for a set of TSQLRecord text fields // - field names should be specified as CSV in the JSON "FieldNames" property // in the constructor, or the Parameters field, e.g. like // ! TSQLSampleRecord.AddFilterOrValidate('propA', // ! TSynValidateUniqueFields.Create('{"FieldNames":"propA,propB"}')); // - this class will handle only textual fields, not numeric values // - it will check that the field values are not a duplicate TSynValidateUniqueFields = class(TSynValidateRest) protected fFieldNames: TRawUTF8DynArray; procedure SetParameters(const Value: RawUTF8); override; /// perform the unique fields validation action to the specified value function DoValidate(aFieldIndex: integer; const Value: RawUTF8; var ErrorMsg: string; aProcessRest: TSQLRest; aProcessRec: TSQLRecord): boolean; override; public /// the validated field names property FieldNames: TRawUTF8DynArray read fFieldNames; end; /// a WHERE constraint as set by the TSQLVirtualTable.Prepare() method TSQLVirtualTablePreparedConstraint = packed record /// Column on left-hand side of constraint // - The first column of the virtual table is column 0 // - The RowID of the virtual table is column -1 // - Hidden columns are counted when determining the column index // - if this field contains VIRTUAL_TABLE_IGNORE_COLUMN (-2), TSQLVirtualTable. // Prepare() should ignore this entry Column: integer; /// The associated expression // - TSQLVirtualTable.Prepare() must set Value.VType to not svtUnknown // (e.g. to svtNull), if an expression is expected at vt_BestIndex() call // - TSQLVirtualTableCursor.Search() will receive an expression value, // to be retrieved e.g. via sqlite3_value_*() functions Value: TSQLVar; /// Constraint operator // - MATCH keyword is parsed into soBeginWith, and should be handled as // soBeginWith, soContains or soSoundsLike* according to the effective // expression text value ('text*', '%text'...) Operation: TCompareOperator; /// If true, the constraint is assumed to be fully handled // by the virtual table and is not checked again by SQLite // - By default (OmitCheck=false), the SQLite core double checks all // constraints on each row of the virtual table that it receives // - TSQLVirtualTable.Prepare() can set this property to true OmitCheck: boolean; end; PSQLVirtualTablePreparedConstraint = ^TSQLVirtualTablePreparedConstraint; /// an ORDER BY clause as set by the TSQLVirtualTable.Prepare() method // - warning: this structure should match exactly TSQLite3IndexOrderBy as // defined in SynSQLite3 TSQLVirtualTablePreparedOrderBy = record /// Column number // - The first column of the virtual table is column 0 // - The RowID of the virtual table is column -1 // - Hidden columns are counted when determining the column index. Column: Integer; /// True for DESCending order, false for ASCending order. Desc: boolean; end; /// abstract planning execution of a query, as set by TSQLVirtualTable.Prepare TSQLVirtualTablePreparedCost = ( costFullScan, costScanWhere, costSecondaryIndex, costPrimaryIndex); /// the WHERE and ORDER BY statements as set by TSQLVirtualTable.Prepare // - Where[] and OrderBy[] are fixed sized arrays, for fast and easy code {$ifdef USERECORDWITHMETHODS}TSQLVirtualTablePrepared = record {$else}TSQLVirtualTablePrepared = object{$endif} public /// number of WHERE statement parameters in Where[] array WhereCount: integer; /// numver of ORDER BY statement parameters in OrderBy[] OrderByCount: integer; /// if true, the ORDER BY statement is assumed to be fully handled // by the virtual table and is not checked again by SQLite // - By default (OmitOrderBy=false), the SQLite core sort all rows of the // virtual table that it receives according in order OmitOrderBy: boolean; /// Estimated cost of using this prepared index // - SQLite uses this value to make a choice between several calls to // the TSQLVirtualTable.Prepare() method with several expressions EstimatedCost: TSQLVirtualTablePreparedCost; /// Estimated number of rows of using this prepared index // - does make sense only if EstimatedCost=costFullScan // - SQLite uses this value to make a choice between several calls to // the TSQLVirtualTable.Prepare() method with several expressions // - is used only starting with SQLite 3.8.2 EstimatedRows: Int64; /// WHERE statement parameters, in TSQLVirtualTableCursor.Search() order Where: array[0..MAX_SQLFIELDS-1] of TSQLVirtualTablePreparedConstraint; /// ORDER BY statement parameters OrderBy: array[0..MAX_SQLFIELDS-1] of TSQLVirtualTablePreparedOrderBy; /// returns TRUE if there is only one ID=? statement in this search function IsWhereIDEquals(CalledFromPrepare: Boolean): boolean; {$ifdef HASINLINE}inline;{$endif} /// returns TRUE if there is only one FieldName=? statement in this search function IsWhereOneFieldEquals: boolean; {$ifdef HASINLINE}inline;{$endif} end; PSQLVirtualTablePrepared = ^TSQLVirtualTablePrepared; TSQLVirtualTableCursor = class; /// class-reference type (metaclass) of a cursor on an abstract Virtual Table TSQLVirtualTableCursorClass = class of TSQLVirtualTableCursor; /// the possible features of a Virtual Table // - vtWrite is to be set if the table is not Read/Only // - vtTransaction if handles vttBegin, vttSync, vttCommit, vttRollBack // - vtSavePoint if handles vttSavePoint, vttRelease, vttRollBackTo // - vtWhereIDPrepared if the ID=? WHERE statement will be handled in // TSQLVirtualTableCursor.Search() TSQLVirtualTableFeature = (vtWrite, vtTransaction, vtSavePoint, vtWhereIDPrepared); /// a set of features of a Virtual Table TSQLVirtualTableFeatures = set of TSQLVirtualTableFeature; /// used to store and handle the main specifications of a TSQLVirtualTableModule TVirtualTableModuleProperties = record /// a set of features of a Virtual Table Features: TSQLVirtualTableFeatures; /// the associated cursor class CursorClass: TSQLVirtualTableCursorClass; /// the associated TSQLRecord class // - used to retrieve the field structure with all collations RecordClass: TSQLRecordClass; /// the associated TSQLRestStorage class used for storage // - is e.g. TSQLRestStorageInMemory for TSQLVirtualTableJSON, // TSQLRestStorageExternal for TSQLVirtualTableExternal, or nil for // TSQLVirtualTableLog StaticClass: TSQLRestStorageClass; /// can be used to customize the extension of the filename // - the '.' is not to be included FileExtension: TFileName; end; /// parent class able to define a Virtual Table module // - in order to implement a new Virtual Table type, you'll have to define a so // called "Module" to handle the fields and data access and an associated // TSQLVirtualTableCursorClass for handling the SELECT statements // - for our framework, the SQLite3 unit will inherit from this class to define // a TSQLVirtualTableModuleSQLite3 class, which will register the associated // virtual table definition into a SQLite3 connection, on the server side // - children should override abstract methods in order to implement the // association with the database engine itself TSQLVirtualTableModule = class protected fModuleName: RawUTF8; fTableClass: TSQLVirtualTableClass; fServer: TSQLRestServer; fFeatures: TVirtualTableModuleProperties; fFilePath: TFileName; public /// create the Virtual Table instance according to the supplied class // - inherited constructors may register the Virtual Table to the specified // database connection constructor Create(aTableClass: TSQLVirtualTableClass; aServer: TSQLRestServer); virtual; /// retrieve the file name to be used for a specific Virtual Table // - returns by default a file located in the executable folder, with the // table name as file name, and module name as extension function FileName(const aTableName: RawUTF8): TFileName; virtual; /// the Virtual Table module features property Features: TSQLVirtualTableFeatures read fFeatures.Features; /// the associated virtual table class property TableClass: TSQLVirtualTableClass read fTableClass; /// the associated virtual table cursor class property CursorClass: TSQLVirtualTableCursorClass read fFeatures.CursorClass; /// the associated TSQLRestStorage class used for storage // - e.g. returns TSQLRestStorageInMemory for TSQLVirtualTableJSON, // or TSQLRestStorageExternal for TSQLVirtualTableExternal, or // either nil for TSQLVirtualTableLog property StaticClass: TSQLRestStorageClass read fFeatures.StaticClass; /// the associated TSQLRecord class // - is mostly nil, e.g. for TSQLVirtualTableJSON // - used to retrieve the field structure for TSQLVirtualTableLog e.g. property RecordClass: TSQLRecordClass read fFeatures.RecordClass; /// the extension of the filename (without any left '.') property FileExtension: TFileName read fFeatures.FileExtension; /// the full path to be used for the filename // - is '' by default, i.e. will use the executable path // - you can specify here a custom path, which will be used by the FileName // method to retrieve the .json/.data full file property FilePath: TFileName read fFilePath write fFilePath; /// the associated Server instance // - may be nil, in case of direct access to the virtual table property Server: TSQLRestServer read fServer; /// the corresponding module name property ModuleName: RawUTF8 read fModuleName; end; /// the available transaction levels TSQLVirtualTableTransaction = ( vttBegin, vttSync, vttCommit, vttRollBack, vttSavePoint, vttRelease, vttRollBackTo); /// abstract class able to access a Virtual Table content // - override the Prepare/Structure abstract virtual methods for reading // access to the virtual table content // - you can optionaly override Drop/Delete/Insert/Update/Rename/Transaction // virtual methods to allow content writing to the virtual table // - the same virtual table mechanism can be used with several database module, // with diverse database engines TSQLVirtualTable = class protected fModule: TSQLVirtualTableModule; fTableName: RawUTF8; fStatic: TSQLRest; fStaticStorage: TSQLRestStorage; fStaticTable: TSQLRecordClass; fStaticTableIndex: integer; public /// create the virtual table access instance // - the created instance will be released when the virtual table will be // disconnected from the DB connection (e.g. xDisconnect method for SQLite3) // - shall raise an exception in case of invalid parameters (e.g. if the // supplied module is not associated to a TSQLRestServer instance) // - aTableName will be checked against the current aModule.Server.Model // to retrieve the corresponding TSQLRecordVirtualTableAutoID class and // create any associated Static: TSQLRestStorage instance constructor Create(aModule: TSQLVirtualTableModule; const aTableName: RawUTF8; FieldCount: integer; Fields: PPUTF8CharArray); virtual; /// release the associated memory, especially the Static instance destructor Destroy; override; /// retrieve the corresponding module name // - will use the class name, triming any T/TSQL/TSQLVirtual/TSQLVirtualTable* // - when the class is instanciated, it will be faster to retrieve the same // value via Module.ModuleName class function ModuleName: RawUTF8; /// a generic method to get a 'CREATE TABLE' structure from a supplied // TSQLRecord class // - is called e.g. by the Structure method class function StructureFromClass(aClass: TSQLRecordClass; const aTableName: RawUTF8): RawUTF8; /// the associated Virtual Table module property Module: TSQLVirtualTableModule read fModule; /// the name of the Virtual Table, as specified following the TABLE keyword // in the CREATE VIRTUAL TABLE statement property TableName: RawUTF8 read fTableName; public { virtual methods to be overridden } /// should return the main specifications of the associated TSQLVirtualTableModule class procedure GetTableModuleProperties( var aProperties: TVirtualTableModuleProperties); virtual; abstract; /// called to determine the best way to access the virtual table // - will prepare the request for TSQLVirtualTableCursor.Search() // - in Where[], Expr must be set to not 0 if needed for Search method, // and OmitCheck to true if double check is not necessary // - OmitOrderBy must be set to true if double sort is not necessary // - EstimatedCost and EstimatedRows should receive the estimated cost // - default implementation will let the DB engine perform the search, // and prepare for ID=? statement if vtWhereIDPrepared was set function Prepare(var Prepared: TSQLVirtualTablePrepared): boolean; virtual; /// should retrieve the format (the names and datatypes of the columns) of // the virtual table, as expected by sqlite3_declare_vtab() // - default implementation is to retrieve the structure for the associated // Module.RecordClass property (as set by GetTableModuleProperties) or // the Static.StoredClass: in both cases, column numbering will follow // the TSQLRecord published field order (TSQLRecord.RecordProps.Fields[]) function Structure: RawUTF8; virtual; /// called when a DROP TABLE statement is executed against the virtual table // - should return true on success, false otherwise // - does nothing by default, and returns false, i.e. always fails function Drop: boolean; virtual; /// called to delete a virtual table row // - should return true on success, false otherwise // - does nothing by default, and returns false, i.e. always fails function Delete(aRowID: Int64): boolean; virtual; /// called to insert a virtual table row content from an array of TSQLVar // - should return true on success, false otherwise // - should return the just created row ID in insertedRowID on success // - does nothing by default, and returns false, i.e. always fails function Insert(aRowID: Int64; var Values: TSQLVarDynArray; out insertedRowID: Int64): boolean; virtual; /// called to update a virtual table row content from an array of TSQLVar // - should return true on success, false otherwise // - does nothing by default, and returns false, i.e. always fails function Update(oldRowID, newRowID: Int64; var Values: TSQLVarDynArray): boolean; virtual; /// called to begin a transaction to the virtual table row // - do nothing by default, and returns false in case of RollBack/RollBackto // - aSavePoint is used for vttSavePoint, vttRelease and vttRollBackTo only // - note that if you don't nest your writing within a transaction, SQLite // will call vttCommit for each INSERT/UPDATE/DELETE, just like a regular // SQLite database - it could make bad written code slow even with Virtual // Tables function Transaction(aState: TSQLVirtualTableTransaction; aSavePoint: integer): boolean; virtual; /// called to rename the virtual table // - by default, returns false, i.e. always fails function Rename(const NewName: RawUTF8): boolean; virtual; /// the associated virtual table storage instance // - can be e.g. a TSQLRestStorageInMemory for TSQLVirtualTableJSON, // or a TSQLRestStorageExternal for TSQLVirtualTableExternal, or nil // for TSQLVirtualTableLog property Static: TSQLRest read fStatic; /// the associated virtual table storage instance, if is a TSQLRestStorage property StaticStorage: TSQLRestStorage read fStaticStorage; /// the associated virtual table storage table property StaticTable: TSQLRecordClass read fStaticTable; /// the associated virtual table storage index in its Model.Tables[] array property StaticTableIndex: integer read fStaticTableIndex; end; /// abstract class able to define a Virtual Table cursor // - override the Search/HasData/Column/Next abstract virtual methods to // implement the search process TSQLVirtualTableCursor = class protected fTable: TSQLVirtualTable; /// used internaly between two Column() method calls for GetFieldSQLVar() fColumnTemp: RawByteString; /// easy set a TSQLVar content for the Column() method procedure SetColumn(var aResult: TSQLVar; aValue: Int64); overload; {$ifdef HASINLINE}inline;{$endif} procedure SetColumn(var aResult: TSQLVar; const aValue: double); overload; {$ifdef HASINLINE}inline;{$endif} procedure SetColumn(var aResult: TSQLVar; const aValue: RawUTF8); overload; {$ifdef HASINLINE}inline;{$endif} procedure SetColumn(var aResult: TSQLVar; aValue: PUTF8Char; aValueLength: integer); overload; {$ifdef HASINLINE}inline;{$endif} procedure SetColumnBlob(var aResult: TSQLVar; aValue: pointer; aValueLength: integer); {$ifdef HASINLINE}inline;{$endif} procedure SetColumnDate(var aResult: TSQLVar; const aValue: TDateTime; aWithMS: boolean); {$ifdef HASINLINE}inline;{$endif} procedure SetColumnCurr64(var aResult: TSQLVar; aValue64: PInt64); {$ifdef HASINLINE}inline;{$endif} public /// create the cursor instance // - it will be destroyed when by the DB engine (e.g. via xClose in SQLite3) constructor Create(aTable: TSQLVirtualTable); virtual; /// the associated Virtual Table class instance property Table: TSQLVirtualTable read fTable; public { abstract methods to be overridden } /// called to begin a search in the virtual table // - the TSQLVirtualTablePrepared parameters were set by // TSQLVirtualTable.Prepare and will contain both WHERE and ORDER BY statements // (retrieved e.g. by x_BestIndex() from a TSQLite3IndexInfo structure) // - Prepared will contain all prepared constraints and the corresponding // expressions in the Where[].Value field // - should move cursor to first 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 Search(const Prepared: TSQLVirtualTablePrepared): boolean; virtual; abstract; /// 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; virtual; abstract; /// called to retrieve a column value of the current data row into a TSQLVar // - if aColumn=-1, should return the row ID as varInt64 into aResult // - should return false in case of an error, true on success function Column(aColumn: integer; var aResult: TSQLVar): boolean; virtual; abstract; /// 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; virtual; abstract; end; /// A generic Virtual Table cursor associated to Current/Max index properties TSQLVirtualTableCursorIndex = class(TSQLVirtualTableCursor) protected fCurrent: integer; fMax: integer; public /// called after Search() to check if there is data to be retrieved // - will return false if reached the end of matching data, according to // the fCurrent/fMax protected properties values function HasData: boolean; override; /// called to go to the next 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) // - will check the fCurrent/fMax protected properties values function Next: boolean; override; /// called to begin a search in the virtual table // - this no-op version will mark EOF, i.e. fCurrent=0 and fMax=-1 function Search(const Prepared: TSQLVirtualTablePrepared): boolean; override; end; /// A Virtual Table cursor for reading a TSQLRestStorageInMemory content // - this is the cursor class associated to TSQLVirtualTableJSON TSQLVirtualTableCursorJSON = class(TSQLVirtualTableCursorIndex) public /// called to begin a search in the virtual table // - 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) // - only handled WHERE clause is for "ID = value" - other request will // return all records in ID order, and let the database engine handle it function Search(const Prepared: TSQLVirtualTablePrepared): boolean; override; /// called to retrieve a column value of the current data row into a TSQLVar // - if aColumn=-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; end; /// A TSQLRestStorageInMemory-based virtual table using JSON storage // - for ORM access, you should use TSQLModel.VirtualTableRegister method to // associated this virtual table module to a TSQLRecordVirtualTableAutoID class // - transactions are not handled by this module // - by default, no data is written on disk: you will need to call explicitly // aServer.StaticVirtualTable[aClass].UpdateToFile for file creation or refresh // - file extension is set to '.json' TSQLVirtualTableJSON = class(TSQLVirtualTable) protected fStaticInMemory: TSQLRestStorageInMemory; public { overridden methods } /// create the virtual table access instance // - the created instance will be released when the virtual table will be // disconnected from the DB connection (e.g. xDisconnect method for SQLite3) // - shall raise an exception in case of invalid parameters (e.g. if the // supplied module is not associated to a TSQLRestServer instance) // - aTableName will be checked against the current aModule.Server.Model // to retrieve the corresponding TSQLRecordVirtualTableAutoID class and // create any associated Static: TSQLRestStorage instance constructor Create(aModule: TSQLVirtualTableModule; const aTableName: RawUTF8; FieldCount: integer; Fields: PPUTF8CharArray); override; /// returns the main specifications of the associated TSQLVirtualTableModule // - this is a read/write table, without transaction, associated to the // TSQLVirtualTableCursorJSON cursor type, with 'JSON' as module name // - no particular class is supplied here, since it will depend on the // associated Static 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() // - only prepared WHERE statement is for "ID = value" // - only prepared ORDER BY statement is for ascending IDs 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 from a TSQLVar array // - column order follows the Structure method, i.e. // StoredClassRecordProps.Fields[] order // - returns true on success, false otherwise // - returns the just created row ID in insertedRowID on success // - does nothing by default, and returns false, i.e. always fails function Insert(aRowID: Int64; var Values: TSQLVarDynArray; out insertedRowID: Int64): boolean; override; /// called to update a virtual table row content from a TSQLVar array // - column order follows the Structure method, i.e. // StoredClassRecordProps.Fields[] order // - returns true on success, false otherwise // - does nothing by default, and returns false, i.e. always fails function Update(oldRowID, newRowID: Int64; var Values: TSQLVarDynArray): boolean; override; end; /// A TSQLRestStorageInMemory-based virtual table using Binary storage // - for ORM access, you should use TSQLModel.VirtualTableRegister method to // associated this virtual table module to a TSQLRecordVirtualTableAutoID class // - transactions are not handled by this module // - by default, no data is written on disk: you will need to call explicitly // aServer.StaticVirtualTable[aClass].UpdateToFile for file creation or refresh // - binary format is more efficient in term of speed and disk usage than // the JSON format implemented by TSQLVirtualTableJSON // - binary format will be set by TSQLVirtualTableJSON.CreateTableInstance // - file extension is set to '.data' TSQLVirtualTableBinary = class(TSQLVirtualTableJSON); /// Implements a read/only virtual table able to access a .log file, as created // by TSynLog // - to be used e.g. by a TSQLRecordLog_Log ('Log_' will identify this 'Log' module) // - the .log file name will be specified by the Table Name, to which a '.log' // file extension will be appended before loading it from the current directory TSQLVirtualTableLog = class(TSQLVirtualTable) protected fLogFile: TSynLogFile; public /// returns the main specifications of the associated TSQLVirtualTableModule // - this is a read only table, with transaction, associated to the // TSQLVirtualTableCursorLog cursor type, with 'Log' as module name, // and associated to TSQLRecordLog_Log table field layout class procedure GetTableModuleProperties( var aProperties: TVirtualTableModuleProperties); override; /// creates the TSQLVirtualTable according to the supplied parameters // - aTableName will be checked against the current aModule.Server.Model // to retrieve the corresponding TSQLRecordVirtualTableAutoID class constructor Create(aModule: TSQLVirtualTableModule; const aTableName: RawUTF8; FieldCount: integer; Fields: PPUTF8CharArray); override; /// release the associated .log file mapping and all internal structures destructor Destroy; override; end; /// A Virtual Table cursor for reading a TSynLogFile content // - this is the cursor class associated to TSQLVirtualTableLog TSQLVirtualTableCursorLog = class(TSQLVirtualTableCursorIndex) public /// called to begin a search in the virtual table function Search(const Prepared: TSQLVirtualTablePrepared): boolean; override; /// called to retrieve a column value of the current data row as TSQLVar function Column(aColumn: integer; var aResult: TSQLVar): boolean; override; end; /// Record associated to a Virtual Table implemented in Delphi, with ID // forced at INSERT // - will use TSQLVirtualTableModule / TSQLVirtualTable / TSQLVirtualTableCursor // classes for a generic Virtual table mechanism on the Server side // - call Model.VirtualTableRegister() before TSQLRestServer.Create on the // Server side (not needed for Client) to associate such a record with a // particular Virtual Table module, otherwise an exception will be raised: // ! Model.VirtualTableRegister(TSQLRecordDali1,TSQLVirtualTableJSON); TSQLRecordVirtualTableForcedID = class(TSQLRecordVirtual); /// Record associated to Virtual Table implemented in Delphi, with ID // generated automatically at INSERT // - will use TSQLVirtualTableModule / TSQLVirtualTable / TSQLVirtualTableCursor // classes for a generic Virtual table mechanism // - call Model.VirtualTableRegister() before TSQLRestServer.Create on the // Server side (not needed for Client) to associate such a record with a // particular Virtual Table module, otherwise an exception will be raised: // ! Model.VirtualTableRegister(TSQLRecordDali1,TSQLVirtualTableJSON); TSQLRecordVirtualTableAutoID = class(TSQLRecordVirtual); /// special comparison function for sorting ftRecord (TRecordReference/RecordRef) // UTF-8 encoded values in the SQLite3 database or JSON content function UTF8CompareRecord(P1,P2: PUTF8Char): PtrInt; /// special comparison function for sorting sftBoolean // UTF-8 encoded values in the SQLite3 database or JSON content function UTF8CompareBoolean(P1,P2: PUTF8Char): PtrInt; /// special comparison function for sorting sftEnumerate, sftSet or sftID // UTF-8 encoded values in the SQLite3 database or JSON content function UTF8CompareUInt32(P1,P2: PUTF8Char): PtrInt; /// special comparison function for sorting sftInteger, sftTID, sftRecordVersion // sftTimeLog/sftModTime/sftCreateTime or sftUnixTime/sftUnixMSTime UTF-8 encoded // values in the SQLite3 database or JSON content function UTF8CompareInt64(P1,P2: PUTF8Char): PtrInt; /// special comparison function for sorting sftCurrency // UTF-8 encoded values in the SQLite3 database or JSON content function UTF8CompareCurr64(P1,P2: PUTF8Char): PtrInt; /// special comparison function for sorting sftFloat // UTF-8 encoded values in the SQLite3 database or JSON content function UTF8CompareDouble(P1,P2: PUTF8Char): PtrInt; /// special comparison function for sorting sftDateTime or sftDateTimeMS // UTF-8 encoded values in the SQLite3 database or JSON content function UTF8CompareISO8601(P1,P2: PUTF8Char): PtrInt; {$ifndef NOVARIANTS} /// low-level function used to convert a JSON Value into a variant, // according to the property type // - for sftObject, sftVariant, sftBlobDynArray and sftUTF8Custom, the // JSON buffer may be an array or an object, so createValueTempCopy can // create a temporary copy before parsing it in-place, to preserve the buffer // - sftUnknown and sftMany will set a varEmpty (Unassigned) value // - typeInfo may be used for sftBlobDynArray conversion to a TDocVariant array procedure ValueVarToVariant(Value: PUTF8Char; ValueLen: integer; fieldType: TSQLFieldType; var result: TVarData; createValueTempCopy: boolean; typeInfo: pointer; options: TDocVariantOptions=JSON_OPTIONS_FAST); /// read an object properties from a TDocVariant object document // - ObjectInstance must be an existing TObject instance // - will return TRUE on success, or FALSE if the supplied aDocVariant was // not a TDocVariant object function ObjectLoadVariant(var ObjectInstance; const aDocVariant: variant; TObjectListItemClass: TClass=nil; Options: TJSONToObjectOptions=[]): boolean; {$endif NOVARIANTS} /// may be used by DatabaseExecute/AdministrationExecute methods to serve // a folder content for remote administration procedure AdministrationExecuteGetFiles(const Folder, Mask: TFileName; const Param: RawUTF8; var Answer: TServiceCustomAnswer); const /// if a TSQLVirtualTablePreparedConstraint.Column is to be ignored VIRTUAL_TABLE_IGNORE_COLUMN = -2; /// if a TSQLVirtualTablePreparedConstraint.Column points to the RowID VIRTUAL_TABLE_ROWID_COLUMN = -1; /// if the TSQLRecordVirtual table kind is a FTS virtual table IS_FTS = [rFTS3, rFTS4, rFTS5]; /// if the TSQLRecordVirtual table kind is not an embedded type // - can be set for a TSQLRecord after a VirtualTableExternalRegister call IS_CUSTOM_VIRTUAL = [rCustomForcedID, rCustomAutoID]; /// if the TSQLRecordVirtual table kind expects the ID to be set on INSERT INSERT_WITH_ID = [rFTS3, rFTS4, rFTS5, rRTree, rRTreeInteger, rCustomForcedID]; /// Supervisor Table access right, i.e. alllmighty over all fields ALL_ACCESS_RIGHTS = [0..MAX_SQLTABLES-1]; /// Complete Database access right, i.e. allmighty over all Tables // - WITH the possibility to remotely execute any SQL statement (reSQL right) // - is used by default by TSQLRestClientDB.URI() method, i.e. for direct // local/in-process access // - is used as reference to create TSQLAuthUser 'Admin' access policy FULL_ACCESS_RIGHTS: TSQLAccessRights = (AllowRemoteExecute: [reSQL,reSQLSelectWithoutTable,reService,reUrlEncodedSQL,reUrlEncodedDelete]; GET: ALL_ACCESS_RIGHTS; POST: ALL_ACCESS_RIGHTS; PUT: ALL_ACCESS_RIGHTS; DELETE: ALL_ACCESS_RIGHTS); /// Supervisor Database access right, i.e. allmighty over all Tables // - but WITHOUT the possibility to remotely execute any SQL statement (reSQL) // - is used as reference to create TSQLAuthUser 'Supervisor' access policy SUPERVISOR_ACCESS_RIGHTS: TSQLAccessRights = (AllowRemoteExecute: [reSQLSelectWithoutTable,reService,reUrlEncodedSQL,reUrlEncodedDelete]; GET: ALL_ACCESS_RIGHTS; POST: ALL_ACCESS_RIGHTS; PUT: ALL_ACCESS_RIGHTS; DELETE: ALL_ACCESS_RIGHTS); /// special TSQLFieldBits value containing all field bits set to 1 ALL_FIELDS: TSQLFieldBits = [0..MAX_SQLFIELDS-1]; /// default hashed password set by TSQLAuthGroup.InitializeTable for all users // - contains TSQLAuthUser.ComputeHashedPassword('synopse') // - override AuthAdminDefaultPassword, AuthSupervisorDefaultPassword and // AuthUserDefaultPassword values to follow your own application expectations DEFAULT_HASH_SYNOPSE = '67aeea294e1cb515236fd7829c55ec820ef888e8e221814d24d83b3dc4d825dd'; /// the Server-side instance implementation patterns without any ID SERVICE_IMPLEMENTATION_NOID = [sicSingle,sicShared]; /// typical TJSONSerializerSQLRecordOptions values for AJAX clients JSONSERIALIZEROPTIONS_AJAX = [jwoAsJsonNotAsString,jwoID_str]; var /// default timeout period set by TSQLAuthGroup.InitializeTable for 'Admin' group // - you can override this value to follow your own application expectations AuthAdminGroupDefaultTimeout: integer = 10; /// default timeout period set by TSQLAuthGroup.InitializeTable for 'Supervisor' group // - you can override this value to follow your own application expectations // - note that clients will maintain the session alive using CacheFlush/_ping_ AuthSupervisorGroupDefaultTimeout: integer = 60; /// default timeout period set by TSQLAuthGroup.InitializeTable for 'User' group // - you can override this value to follow your own application expectations // - note that clients will maintain the session alive using CacheFlush/_ping_ AuthUserGroupDefaultTimeout: integer = 60; /// default timeout period set by TSQLAuthGroup.InitializeTable for 'Guest' group // - you can override this value to follow your own application expectations // - note that clients will maintain the session alive using CacheFlush/_ping_ AuthGuestGroupDefaultTimeout: integer = 60; /// default hashed password set by TSQLAuthGroup.InitializeTable for 'Admin' user // - you can override this value to follow your own application expectations AuthAdminDefaultPassword: RawUTF8 = DEFAULT_HASH_SYNOPSE; /// default hashed password set by TSQLAuthGroup.InitializeTable for 'Supervisor' user // - you can override this value to follow your own application expectations AuthSupervisorDefaultPassword: RawUTF8 = DEFAULT_HASH_SYNOPSE; /// default hashed password set by TSQLAuthGroup.InitializeTable for 'User' user // - you can override this value to follow your own application expectations AuthUserDefaultPassword: RawUTF8 = DEFAULT_HASH_SYNOPSE; const {$ifndef DOMAINAUTH} // fallback is no SSPI library was made available TSQLRestServerAuthenticationSSPI = nil; {$endif DOMAINAUTH} /// timer identifier which indicates we must refresh the current Page // - used for User Interface generation // - is associated with the TSQLRibbonTabParameters.AutoRefresh property, // and is handled in TSQLRibbon.RefreshClickHandled WM_TIMER_REFRESH_SCREEN = 1; /// timer identifier which indicates we must refresh the Report content // - used for User Interface generation // - is handled in TSQLRibbon.RefreshClickHandled WM_TIMER_REFRESH_REPORT = 2; /// the default URI parameters for query paging // - those values are the one expected by YUI components PAGINGPARAMETERS_YAHOO: TSQLRestServerURIPagingParameters = ( Sort: 'SORT='; Dir: 'DIR='; StartIndex: 'STARTINDEX='; Results: 'RESULTS='; Select: 'SELECT='; Where: 'WHERE='; SendTotalRowsCountFmt: ''); /// options to specify no index createon for TSQLRestServer.CreateMissingTables // and TSQLRecord.InitializeTable methods INITIALIZETABLE_NOINDEX: TSQLInitializeTableOptions = [itoNoIndex4ID..itoNoIndex4RecordVersion]; /// default value of TSQLRestServer.StatLevels property // - i.e. gather all statistics, but mlSessions SERVERDEFAULTMONITORLEVELS: TSQLRestServerMonitorLevels = [mlTables,mlMethods,mlInterfaces,mlSQLite3]; /// wrapper to search for a given TSQLRecord by ID in an array of TSQLRecord function ObjArraySearch(const aSQLRecordObjArray; aID: TID): TSQLRecord; /// wrapper to return all TID values of an array of TSQLRecord procedure ObjArrayRecordIDs(const aSQLRecordObjArray; out result: TInt64DynArray); /// wrapper to create a new T*ObjArray with copied instances of a source T*ObjArray // - use internally CopyObject() over aSourceObjArray[] instances // - will clear aDestObjArray before items copy, if aDestObjArrayClear = TRUE procedure ObjArrayCopy(const aSourceObjArray; var aDestObjArray; aDestObjArrayClear: boolean=true); /// safe deletion of a T*InterfaceArray dynamic array item // - similar to InterfaceArrayDelete, but with a safe try .. except block // during the entry deletion (since the system may be unstable) // - will also log a warning with the Interface name (from aLogMsg) and aInstance procedure InterfaceArrayDeleteAfterException(var aInterfaceArray; const aItemIndex: integer; aLog: TSynLogFamily; const aLogMsg: RawUTF8; aInstance: TObject); /// create a TRecordReference with the corresponding parameters function RecordReference(Model: TSQLModel; aTable: TSQLRecordClass; aID: TID): TRecordReference; overload; /// create a TRecordReference with the corresponding parameters function RecordReference(aTableIndex: cardinal; aID: TID): TRecordReference; overload; {$ifdef HASINLINE}inline;{$endif} /// convert a dynamic array of TRecordReference into its corresponding IDs procedure RecordRefToID(var aArray: TInt64DynArray); /// get the order table name from a SQL statement // - return the word following any 'ORDER BY' statement // - return 'RowID' if none found function SQLGetOrder(const SQL: RawUTF8): RawUTF8; {$ifdef PUREPASCAL}{$ifdef HASINLINE} /// this function is published only for class function TSQLRecord.RecordProps() // internal call after inlining function PropsCreate(aTable: TSQLRecordClass): TSQLRecordProperties; {$endif}{$endif} /// low-level function to retrieve the class instance implementing a given interface // - this will work with interfaces stubs generated by the compiler, but also // with TInterfaceFactory.CreateFakeInstance kind of classes // - returns nil if aValue is nil or not recognized function ObjectFromInterface(const aValue: IInterface): TObject; /// low-level function to check if a class instance, retrieved from its // interface variable, does in fact implement a given interface // - this will call ObjectFromInterface(), so will work with interfaces // stubs generated by the compiler, but also with // TInterfaceFactory.CreateFakeInstance kind of classes function ObjectFromInterfaceImplements(const aValue: IInterface; const aInterface: TGUID): boolean; /// assign a Weak interface reference, to be used for circular references // - by default setting aInterface.Field := aValue will increment the internal // reference count of the implementation object: when underlying objects reference // each other via interfaces (e.g. as parent and children), what causes the // reference count to never reach zero, therefore resulting in memory links // - to avoid this issue, use this procedure instead procedure SetWeak(aInterfaceField: PIInterface; const aValue: IInterface); // {$ifdef HASINLINE}inline;{$endif} raise compilation Internal Error C2170 /// assign a Weak interface reference, which will be ZEROed (set to nil) when // the corresponding object will be released // - this function is bit slower than SetWeak, but will avoid any GPF, by // maintaining a list of per-instance weak interface field reference, and // hook the FreeInstance virtual method in order to reset any reference to nil: // FreeInstance will be overridden for this given class VMT only (to avoid // unnecessary slowdown of other classes), calling the previous method afterward // (so will work even with custom FreeInstance implementations) // - for faster possible retrieval, it will assign the unused vmtAutoTable VMT // entry trick (just like TSQLRecord.RecordProps) - note that it will be // compatible also with interfaces implemented via TSQLRecord children // - thread-safe implementation, using a per-class fast lock procedure SetWeakZero(aObject: TObject; aObjectInterfaceField: PIInterface; const aValue: IInterface); {$ifdef ISDELPHIXE} // class helper requires Delphi 2006 or newer but are buggy before XE :( type /// TWeakZeroInterfaceHelper is a class helper that allows you to use // SetWeakZero() in any class without specifying the Self parameter TWeakZeroInterfaceHelper = class helper for TObject protected /// Use SetWeak0 to assign an interface to a weak interface field // - this is just a wrapper around the global SetWeakZero() function procedure SetWeak0(aObjectInterfaceField: PIInterface; const aValue: IInterface); end; {$endif} var /// if this variable is TRUE, the URIRequest() function won't use // Win32 API GlobalAlloc() function, but fastest native Getmem() // - can be also useful for debugg USEFASTMM4ALLOC: boolean = false; /// this function can be exported from a DLL to remotely access to a TSQLRestServer // - use TSQLRestServer.ExportServer to assign a server to this function // - return 501 HTTP_NOTIMPLEMENTED if no TSQLRestServer.ExportServer has been assigned // - memory for Resp and Head are allocated with GlobalAlloc(): client must release // this pointers with GlobalFree() after having retrieved their content // - simply use TSQLRestClientURIDll to access to an exported URIRequest() function function URIRequest(url, method, SendData: PUTF8Char; Resp, Head: PPUTF8Char): Int64Rec; cdecl; threadvar /// this thread-specific variable will be set with the currently running // service context (on the server side) // - note that in case of direct server side execution of the service, this // information won't be filled, so the safest (and slightly faster) access // to the TSQLRestServer instance associated with a service is to inherit your // implementation class from TInjectableObjectRest, and not use this threadvar // - is set by TServiceFactoryServer.ExecuteMethod() just before calling the // implementation method of a service, allowing to retrieve the current // execution context - Request member is set from a client/server execution: // Request.Server is the safe access point to the underlying TSQLRestServer, // in such context - also consider the CurrentServiceContextServer function to // retrieve directly the running TSQLRestServer (if any) // - its content is reset to zero out of the scope of a method execution // - when used, a local copy or a PServiceRunningContext pointer should better // be created, since accessing a threadvar has a non negligible performance // cost - for instance, if you want to use a "with" statement: // ! with PServiceRunningContext(@ServiceContext)^ do // ! ... access TServiceRunningContext members // or as a local variable: // !var context: PServiceRunningContext; // ! inContentType: RawUTF8; // !begin // ! context := @ServiceContext; // threadvar access once // ! ... // ! inContentType := context.Request.Call^.InBodyType; // !end; // - when accessed from a package, use function CurrentServiceContext() // instead, to circumvent a Delphi RTL/compiler restriction (bug?) ServiceContext: TServiceRunningContext; /// wrapper function to retrieve the global ServiceContext threadvar value // - to be used when accessing the value from a package, to circumvent a // Delphi RTL/compiler restriction (bug?) // - for a cleaner SOA/DI approach, consider using TInjectableObjectRest function CurrentServiceContext: TServiceRunningContext; /// wrapper function to retrieve the current REST server instance from // the global ServiceContext threadvar value // - may return nil if ServiceContext.Request is nil: in this case, // you should better implement your service by inheriting the implementation // class from TInjectableObjectRest function CurrentServiceContextServer: TSQLRestServer; /// returns a safe 256-bit hexadecimal nonce, changing every 5 minutes // - as used e.g. by TSQLRestServerAuthenticationDefault.Auth // - this function is very fast, even if cryptographically-level SHA-3 secure function CurrentServerNonce(Previous: boolean=false): RawUTF8; function ToText(ft: TSQLFieldType): PShortString; overload; function ToText(vk: TSQLRecordVirtualKind): PShortString; overload; function ToText(e: TSQLEvent): PShortString; overload; function ToText(he: TSQLHistoryEvent): PShortString; overload; function ToText(o: TSQLOccasion): PShortString; overload; function ToText(dft: TSQLDBFieldType): PShortString; overload; function ToText(si: TServiceInstanceImplementation): PShortString; overload; function ToText(cmd: TSQLRestServerURIContextCommand): PShortString; overload; function ToText(op: TSQLQueryOperator): PShortString; overload; function ToText(V: TInterfaceMockSpyCheck): PShortString; overload; function ToText(m: TSQLURIMethod): PShortString; overload; function ToText(o: TSynTableStatementOperator): PShortString; overload; function ToText(t: TSQLVirtualTableTransaction): PShortString; overload; function ToText(a: TSQLRestServerAuthenticationSignedURIAlgo): PShortString; overload; function ToText(res: TNotifyAuthenticationFailedReason): PShortString; overload; { ************ Logging classes and functions } type /// logging class with enhanced RTTI // - will write TObject/TSQLRecord, enumerations and sets content as JSON // - is the default logging family used by the mORMot framework // - mORMotDB.pas unit will set SynDBLog := TSQLLog // - mORMotSQLite3.pas unit will set SynSQLite3Log := TSQLLog TSQLLog = TSynLog; {$ifdef WITHLOG} var /// TSQLLog class is used for logging for all our ORM related functions // - this global variable can be used to customize it for the whole process // - each TSQLRest.LogClass property is set by default to this SQLite3Log // - you can override the TSQLRest.LogClass property value to customize it // for a given REST instance SQLite3Log: TSynLogClass = TSQLLog; /// TSQLogClass used by overriden SetThreadName() function to name the thread SetThreadNameLog: TSynLogClass = TSQLLog; {$endif} implementation {$ifdef FPC} uses {$ifndef MSWINDOWS} SynFPCLinux, // includes minimal redirection to FPC's TypInfo.pp unit BaseUnix, Unix, {$endif} dynlibs; {$endif FPC} // ************ some RTTI and SQL mapping routines procedure SetID(P: PUTF8Char; var result: TID); {$ifdef CPU64} begin // PtrInt is already int64 -> call PtrInt version result := GetInteger(P); {$else} {$ifdef HASINLINENOTX86} begin {$ifdef VER3_0} // FPC issue woraround SetInt64(P,result); {$else} SetInt64(P,PInt64(@result)^); {$endif} {$else} asm jmp SynCommons.SetInt64 {$endif HASINLINENOTX86} {$endif CPU64} end; procedure SetID(const U: RawByteString; var result: TID); {$ifdef CPU64} begin // PtrInt is already int64 -> call PtrInt version result := GetInteger(pointer(U)); {$else} {$ifdef HASINLINENOTX86} begin SetID(pointer(U),result); {$else} asm jmp SynCommons.SetInt64 {$endif HASINLINENOTX86} {$endif CPU64} end; function TSQLRecordDynArrayCompare(const Item1,Item2): integer; {$ifdef CPUX64} // very efficient branchless asm - rcx/rdi=Item1 rdx/rsi=Item2 {$ifdef FPC}nostackframe; assembler; asm {$else} asm .noframe {$endif FPC} mov rcx, qword ptr[Item1] mov rdx, qword ptr[Item2] mov rcx, qword ptr[rcx+TSQLRecord.fID] mov rdx, qword ptr[rdx+TSQLRecord.fID] xor eax, eax cmp rcx, rdx seta al sbb eax, 0 end; {$else} begin // we assume Item1<>nil and Item2<>nil result := {$ifdef HASINLINE}CompareQWord{$else}SortDynArrayQWord{$endif}( TSQLRecord(Item1).fID,TSQLRecord(Item2).fID); end; {$endif CPUX64} function TSQLRecordDynArrayHashOne(const Elem; Hasher: THasher): cardinal; begin with PQWordRec(@TSQLRecord(Elem).fID)^ do result := crc32cBy4(L,H); end; {$ifdef HASDIRECTTYPEINFO} type Deref = PTypeInfo; {$else} function Deref(Info: PPTypeInfo): PTypeInfo; {$ifdef HASINLINENOTX86} inline; begin if Info=nil then result := pointer(Info) else result := Info^; end; {$else} asm // Delphi is so bad at compiling above code... test eax, eax jz @z mov eax, [eax] ret @z: db $f3 // rep ret end; {$endif HASINLINENOTX86} {$endif HASDIRECTTYPEINFO} {$ifndef FPC} /// no RTTI alignment under Delphi - see also SynFPCTypInfo type AlignToPtr = pointer; AlignTypeData = pointer; AlignTypeDataClean = pointer; UnalignToDouble = Double; {$endif FPC} { some inlined methods } function GetTypeData(const info: TTypeInfo): pointer; {$ifdef HASINLINE}inline;{$endif} begin result := AlignTypeData(PAnsiChar(@info)+2+PByte(PAnsiChar(@info)+1)^); end; function GetTypeDataClean(const info: TTypeInfo): pointer; {$ifdef HASINLINE}inline;{$endif} begin result := AlignTypeDataClean(PAnsiChar(@info)+2+PByte(PAnsiChar(@info)+1)^); end; function TTypeInfo.ClassType: PClassType; {$ifdef HASINLINENOTX86} begin result := GetTypeData(self); end; {$else} asm // very fast code movzx edx, byte ptr[eax].TTypeInfo.Name lea eax, [eax + edx].TTypeInfo.Name[1] end; {$endif} function TTypeInfo.RecordType: PRecordType; {$ifdef HASINLINENOTX86} begin result := GetTypeData(self); end; {$else} asm // very fast code movzx edx, byte ptr[eax].TTypeInfo.Name lea eax, [eax + edx].TTypeInfo.Name[1] end; {$endif} function TTypeInfo.InterfaceType: PInterfaceTypeData; {$ifdef HASINLINENOTX86} begin result := GetTypeData(self); end; {$else} asm // very fast code movzx edx, byte ptr[eax].TTypeInfo.Name lea eax, [eax + edx].TTypeInfo.Name[1] end; {$endif} function TTypeInfo.FloatType: TFloatType; begin result := PFloatType(GetTypeData(self))^; end; function TTypeInfo.OrdType: TOrdType; begin result := POrdType(GetTypeData(self))^; end; function TTypeInfo.IsQWord: boolean; begin {$ifdef FPC} result := Kind=tkQWord; {$else} if @self=TypeInfo(QWord) then result := true else {$ifdef UINICODE}if Kind=tkInt64 then // check MinInt64Value>MaxInt64Value with PHash128Rec(PAnsiChar(@Name[1])+ord(Name[0]))^ do result := Lo>Hi else {$endif} result := false; {$endif} end; function TPropInfo.Getter(Instance: TObject; Call: PMethod): TPropInfoCall; begin if GetProc=0 then begin // no 'read' was defined -> try from 'write' field if (SetProc<>0) and ({$ifdef FPC}(PropProcs shr 2)and 3{$else}PropWrap(SetProc).Kind{$endif}=ptField) then begin Call.Data := pointer(PtrUInt(Instance)+SetProc{$ifndef FPC}and $00ffffff{$endif}); result := picField; end else result := picNone; exit; end else case {$ifdef FPC}integer(PropProcs)and 3{$else}PropWrap(GetProc).Kind{$endif} of ptField: begin // GetProc is an offset to the instance fields Call.Data := pointer(PtrUInt(Instance)+GetProc{$ifndef FPC}and $00ffffff{$endif}); result := picField; exit; end; ptVirtual: // GetProc is an offset to the class VMT Call.Code := PPointer(PPtrUInt(Instance)^+{$ifndef FPC}word{$endif}(GetProc))^; {$ifdef FPC} ptConst: exit(picNone); {$endif} // never happen on properties? else // ptStatic: GetProc is the method code itself Call.Code := pointer(GetProc); end; Call.Data := Instance; if {$ifdef FPC}(PropProcs shr 6)and 1{$else}Index{$endif}<>NO_INDEX then result := picIndexed else result := picMethod; end; function TPropInfo.Setter(Instance: TObject; Call: PMethod): TPropInfoCall; begin if SetProc=0 then begin // no 'write' was defined -> try from 'read' field if (GetProc<>0) and ({$ifdef FPC}integer(PropProcs)and 3{$else}PropWrap(GetProc).Kind{$endif}=ptField) then begin Call.Data := pointer(PtrUInt(Instance)+GetProc{$ifndef FPC}and $00ffffff{$endif}); result := picField; end else result := picNone; exit; end else case {$ifdef FPC}(PropProcs shr 2)and 3{$else}PropWrap(SetProc).Kind{$endif} of ptField: begin // SetProc is an offset to the instance fields Call.Data := pointer(PtrUInt(Instance)+SetProc{$ifndef FPC}and $00ffffff{$endif}); result := picField; exit; end; ptVirtual: // SetProc is an offset to the class VMT Call.Code := PPointer(PPtrUInt(Instance)^+{$ifndef FPC}word{$endif}(SetProc))^; {$ifdef FPC} ptConst: exit(picNone); {$endif} else // ptStatic: SetProc is the method code itself Call.Code := pointer(SetProc); end; Call.Data := Instance; if {$ifdef FPC}(PropProcs shr 6)and 1{$else}Index{$endif}<>NO_INDEX then result := picIndexed else result := picMethod; end; function TPropInfo.GetterAddr(Instance: pointer): pointer; {$ifdef HASINLINENOTX86} begin result := Pointer(PtrUInt(Instance)+GetProc{$ifndef FPC} and $00ffffff{$endif}); end; {$else} asm mov eax, [eax].TPropInfo.GetProc and eax, $00ffffff add eax, edx end; {$endif} function TPropInfo.SetterAddr(Instance: pointer): pointer; begin result := Pointer(PtrUInt(Instance)+SetProc{$ifndef FPC} and $00ffffff{$endif}); end; function TPropInfo.TypeInfo: PTypeInfo; {$ifdef HASINLINENOTX86} begin {$ifndef HASDIRECTTYPEINFO} if PropType<>nil then result := PropType^ else {$endif} result := pointer(PropType); end; {$else} asm // Delphi is so bad at compiling above code... mov eax, [eax].TPropInfo.PropType test eax, eax jz @z mov eax, [eax] ret @z: db $f3 // rep ret end; {$endif HASINLINENOTX86} function TPropInfo.GetterIsField: boolean; begin result := {$ifdef FPC}integer(PropProcs)and 3{$else}PropWrap(GetProc).Kind{$endif}=ptField; end; function TPropInfo.SetterIsField: boolean; begin result := {$ifdef FPC}(PropProcs shr 2)and 3{$else}PropWrap(SetProc).Kind{$endif}=ptField; end; function TPropInfo.GetFieldAddr(Instance: TObject): pointer; begin if not GetterIsField then if not SetterIsField then // both are methods -> returns nil result := nil else // field - Setter is the field offset in the instance data result := SetterAddr(Instance) else // field - Getter is the field offset in the instance data result := GetterAddr(Instance); end; {$ifdef HASINLINENOTX86} function TPropInfo.Next: PPropInfo; begin result := AlignToPtr(PAnsiChar(@Name[0]) + SizeOf(Name[0]) + Length(Name)); end; {$else} function TPropInfo.Next: PPropInfo; asm // very fast code movzx edx, byte ptr[eax].TPropInfo.Name lea eax, [eax + edx].TPropInfo.Name[1] end; {$endif HASINLINENOTX86} function TPropInfo.WriteIsDefined: boolean; begin {$ifdef FPC} // see typinfo.IsWriteableProp result := (SetProc<>0) and ((PropProcs shr 2)and 3 in [ptField..ptVirtual]); {$else} result := SetProc<>0; {$endif} end; function GetInterfaceFromEntry(Instance: TObject; Entry: PInterfaceEntry; out Obj): boolean; {$ifndef FPC} procedure UseImplGetter(Instance: TObject; ImplGetter: PtrInt; var result: IInterface); type // function(Instance: TObject) trick does not work with CPU64 :( TGetProc = function: IInterface of object; var Call: TMethod; begin // sub-procedure to avoid try..finally for TGetProc(): Interface result if PropWrap(ImplGetter).Kind=ptVirtual then Call.Code := PPointer(PPtrInt(Instance)^+SmallInt(ImplGetter))^ else Call.Code := Pointer(ImplGetter); Call.Data := Instance; result := TGetProc(Call); end; {$endif} begin Pointer(Obj) := nil; if Entry<>nil then if Entry^.IOffset <> 0 then begin Pointer(Obj) := Pointer(PtrInt(PtrUInt(Instance))+Entry^.IOffset); if Pointer(Obj)<>nil then IInterface(Obj)._AddRef; end {$ifndef FPC} else if PropWrap(Entry^.ImplGetter).Kind=ptField then IInterface(Obj) := IInterface(PPointer(PtrUInt(Instance)+PtrUInt(Entry^.ImplGetter and $00ffffff))^) else UseImplGetter(Instance,Entry^.ImplGetter,IInterface(Obj)){$endif}; Result := Pointer(Obj)<>nil; end; function InternalMethodInfo(aClassType: TClass; const aMethodName: ShortString): PMethodInfo; var Count, i: integer; begin while aClassType<>nil do begin result := PPointer(PtrInt(PtrUInt(aClassType))+vmtMethodTable)^; if result<>nil then begin {$ifdef FPC} Count := PCardinal(result)^; inc(PCardinal(result)); {$else} Count := PWord(result)^; inc(PWord(result)); {$endif} for i := 0 to Count-1 do if IdemPropName(result^.Name{$ifdef FPC}^{$endif},aMethodName) then exit else {$ifdef FPC} inc(result); {$else} inc(PByte(result),result^.Len); {$endif} end; {$ifdef FPC} aClassType := GetClassParent(aClassType); // vmtParent slot is reference on FPC if aClassType=nil then {$else} if PPointer(PtrInt(aClassType)+vmtParent)^<>nil then aClassType := PPointer(PPointer(PtrInt(aClassType)+vmtParent)^)^ else {$endif} break; end; result := nil; end; function TMethodInfo.MethodAddr: Pointer; begin if @self<>nil then result := Addr else result := @self; end; function TMethodInfo.ReturnInfo: PReturnInfo; begin // see http://hallvards.blogspot.fr/2006/09/extended-class-rtti.html if @self<>nil then begin {$ifdef FPC} result := pointer(PtrUInt(@Addr)+SizeOf(Pointer)); {$else} result := pointer(PAnsiChar(@Name[1])+ord(Name[0])); if PtrUInt(result)-PtrUInt(@self)=Len then result := nil; // no method details available {$endif FPC} end else result := @self; end; function TReturnInfo.Param: PParamInfo; begin result := Pointer(PtrUInt(@self)+SizeOf(TReturnInfo)); end; function TParamInfo.Next: PParamInfo; begin result := AlignToPtr(PAnsiChar(@Name[1])+ord(Name[0])); {$ifdef ISDELPHI2010} inc(PByte(result),PWord(result)^); // ignore optional attributes {$endif} end; function InternalClassProp(ClassType: TClass): PClassProp; {$ifdef FPC} begin with PTypeInfo(PPointer(PtrUInt(ClassType)+vmtTypeInfo)^)^.ClassType^ do result := AlignToPtr(PAnsiChar(@UnitName[1])+ord(UnitName[0])); {$else} {$ifdef HASINLINENOTX86} var PTI: PTypeInfo; begin // code is a bit abstract, but compiles very well PTI := PPointer(PtrInt(ClassType)+vmtTypeInfo)^; if PTI<>nil then // avoid GPF if no RTTI available for this class with PTI^, PClassType(@Name[ord(Name[0])+1])^ do result := PClassProp(@UnitName[ord(UnitName[0])+1]) else result := nil; {$else} asm // this code is the fastest possible mov eax, [eax + vmtTypeInfo] test eax, eax jz @z // avoid GPF if no RTTI available for this class movzx edx, byte ptr[eax].TTypeInfo.Name lea eax, [eax + edx].TTypeInfo.Name[1] movzx edx, byte ptr[eax].TClassType.UnitName lea eax, [eax + edx].TClassType.UnitName[1].TClassProp @z: {$endif HASINLINENOTX86} {$endif FPC} end; function InternalClassPropInfo(ClassType: TClass; out PropInfo: PPropInfo): integer; {$ifdef FPC} var CP: PClassProp; {$endif} begin if ClassType<>nil then begin {$ifdef FPC} CP := InternalClassProp(ClassType); if CP<>nil then begin // no more RTTI information available PropInfo := AlignToPtr(@CP^.PropList); result := CP^.PropCount; {$else} // code is a bit abstract, but compiles very well for Delphi/Kylix inc(PByte(ClassType),vmtTypeInfo); if PPointer(ClassType)^<>nil then // avoid GPF if no RTTI available with PTypeInfo(PPointer(ClassType)^)^, PClassType(@Name[ord(Name[0])+1])^, PClassProp(@UnitName[ord(UnitName[0])+1])^ do begin PropInfo := @PropList; result := PropCount; {$endif FPC} exit; end; end; result := 0; end; function ClassFieldCountWithParents(ClassType: TClass; onlyWithoutGetter: boolean): integer; var CP: PClassProp; P: PPropInfo; i: integer; begin result := 0; while ClassType<>nil do begin CP := InternalClassProp(ClassType); if CP=nil then break; // no RTTI information (e.g. reached TObject level) if onlyWithoutGetter then begin P := AlignToPtr(@CP^.PropList); for i := 1 to CP^.PropCount do begin if P^.GetterIsField then inc(result); P := P^.Next; end; end else inc(result,CP^.PropCount); ClassType := GetClassParent(ClassType); end; end; function ClassHasPublishedFields(ClassType: TClass): boolean; var CP: PClassProp; begin result := true; while ClassType<>nil do begin CP := InternalClassProp(ClassType); if CP=nil then break; // no RTTI information (e.g. reached TObject level) if CP^.PropCount>0 then exit; ClassType := GetClassParent(ClassType); end; result := false; end; function ClassHierarchyWithField(ClassType: TClass): TClassDynArray; procedure InternalAdd(C: TClass; var list: TClassDynArray); var P: PClassProp; begin if C=nil then exit; InternalAdd(GetClassParent(C),list); P := InternalClassProp(C); if (P<>nil) and (P^.PropCount>0) then ObjArrayAdd(list,pointer(C)); end; begin result := nil; InternalAdd(ClassType,result); end; function ClassFieldAllProps(ClassType: TClass; Types: TTypeKinds): PPropInfoDynArray; var CP: PClassProp; P: PPropInfo; i,n: integer; begin n := 0; result := nil; while ClassType<>nil do begin CP := InternalClassProp(ClassType); if CP=nil then break; // no RTTI information (e.g. reached TObject level) if CP^.PropCount>0 then begin SetLength(result,n+CP^.PropCount); P := AlignToPtr(@CP^.PropList); for i := 1 to CP^.PropCount do begin if P^.PropType^.Kind in Types then begin result[n] := P; inc(n); end; P := AlignToPtr(PAnsiChar(@P^.Name[1])+ord(P^.Name[0])); // := P^.Next end; end; ClassType := GetClassParent(ClassType); end; SetLength(result,n); end; function ClassFieldNamesAllProps(ClassType: TClass; IncludePropType: boolean; Types: TTypeKinds): TRawUTF8DynArray; var props: PPropInfoDynArray; n,i: integer; begin result := nil; props := ClassFieldAllProps(ClassType,Types); n := length(props); SetLength(result,n); for i := 0 to n-1 do if IncludePropType then FormatUTF8('%: %',[props[i]^.Name,props[i]^.PropType^.Name],result[i]) else ShortStringToAnsi7String(props[i]^.Name,result[i]); end; function ClassFieldNamesAllPropsAsText(ClassType: TClass; IncludePropType: boolean; Types: TTypeKinds): RawUTF8; begin result := RawUTF8ArrayToCSV( ClassFieldNamesAllProps(ClassType,IncludePropType,Types),', '); end; function ClassFieldProp(ClassType: TClass; const PropName: shortstring): PPropInfo; begin if ClassType<>nil then result := InternalClassProp(ClassType)^.FieldProp(PropName) else result := nil; end; function ClassFieldPropWithParents(aClassType: TClass; const aPropName: shortstring; aCaseSensitive: boolean): PPropInfo; var n, i: integer; begin while aClassType<>nil do begin n := InternalClassPropInfo(aClassType,result); if n<>0 then if aCaseSensitive then for i := 1 to n do if result^.Name=aPropName then exit else result := result^.Next else for i := 1 to n do if (result^.Name[0]=aPropName[0]) and IdemPropNameUSameLen(@result^.Name[1],@aPropName[1],ord(aPropName[0])) then exit else result := result^.Next; aClassType := GetClassParent(aClassType); end; result := nil; end; function ClassFieldPropWithParentsFromUTF8(aClassType: TClass; PropName: PUTF8Char; PropNameLen: integer; aCaseSensitive: boolean): PPropInfo; var n, i: integer; begin if PropNameLen<>0 then while aClassType<>nil do begin n := InternalClassPropInfo(aClassType,result); if n<>0 then if aCaseSensitive then for i := 1 to n do if (result^.Name[0]=AnsiChar(PropNameLen)) and CompareMemFixed(@result^.Name[1],PropName,PropNameLen) then exit else result := result^.Next else for i := 1 to n do if (result^.Name[0]=AnsiChar(PropNameLen)) and IdemPropNameUSameLen(@result^.Name[1],PropName,PropNameLen) then exit else result := result^.Next; aClassType := GetClassParent(aClassType); end; result := nil; end; function ClassFieldPropWithParentsFromClassType(aClassType,aSearchedClassType: TClass): PPropInfo; var i: integer; begin if aSearchedClassType<>nil then while aClassType<>nil do begin for i := 1 to InternalClassPropInfo(aClassType,result) do if (result^.PropType^.Kind=tkClass) and (result^.PropType^.ClassType^.ClassType=aSearchedClassType) then exit else result := result^.Next; aClassType := GetClassParent(aClassType); end; result := nil; end; function ClassFieldPropWithParentsInheritsFromClassType(aClassType,aSearchedClassType: TClass): PPropInfo; var i: integer; begin if aSearchedClassType<>nil then while aClassType<>nil do begin for i := 1 to InternalClassPropInfo(aClassType,result) do if (result^.PropType^.Kind=tkClass) and (result^.PropType^.InheritsFrom(aSearchedClassType)) then exit else result := result^.Next; aClassType := GetClassParent(aClassType); end; result := nil; end; function ClassFieldPropWithParentsFromClassOffset(aClassType: TClass; aSearchedOffset: pointer): PPropInfo; var i: integer; begin if aSearchedOffset<>nil then while aClassType<>nil do begin for i := 1 to InternalClassPropInfo(aClassType,result) do if result^.GetFieldAddr(nil)=aSearchedOffset then exit else result := result^.Next; aClassType := GetClassParent(aClassType); end; result := nil; end; function ClassFieldInstance(Instance: TObject; const PropName: shortstring; PropClassType: TClass; out PropInstance): boolean; var P: PPropInfo; begin result := false; if Instance=nil then exit; P := ClassFieldPropWithParents(PPointer(Instance)^,PropName); if (P=nil) or (P^.PropType^.Kind<>tkClass) or not P^.PropType^.InheritsFrom(PropClassType) then exit; TObject(PropInstance) := P^.GetObjProp(Instance); result := true; end; function ClassFieldInstance(Instance: TObject; PropClassType: TClass; out PropInstance): boolean; var P: PPropInfo; begin result := false; if Instance=nil then exit; P := ClassFieldPropWithParentsFromClassType(PPointer(Instance)^,PropClassType); if P=nil then exit; TObject(PropInstance) := P^.GetObjProp(Instance); result := true; end; function ClassFieldInt64(Instance: TObject; const PropName: ShortString; out PropValue: Int64): boolean; var P: PPropInfo; begin result := false; if Instance=nil then exit; P := ClassFieldPropWithParents(PPointer(Instance)^,PropName); if P=nil then exit; PropValue := P^.GetInt64Value(Instance); result := true; end; function ClassFieldInstances(Instance: TObject; PropClassType: TClass): TObjectDynArray; var nested: PPropInfoDynArray; i: integer; begin result := nil; if Instance=nil then exit; nested := ClassFieldAllProps(PPointer(Instance)^,[tkClass]); for i := 0 to high(nested) do with nested[i]^ do if PropType^.InheritsFrom(PropClassType) then ObjArrayAdd(result,GetObjProp(Instance)); end; function GetObjectComponent(Obj: TPersistent; const ComponentName: shortstring; ComponentClass: TClass): pointer; var P: PPropInfo; begin result := nil; if Obj=nil then exit; P := ClassFieldPropWithParents(Obj.ClassType,ComponentName); if (P<>nil) and (P^.PropType^.Kind=tkClass) then if P^.PropType^.InheritsFrom(ComponentClass) then result := P^.GetObjProp(Obj); end; function GetEnumCaption(aTypeInfo: PTypeInfo; const aIndex): string; begin if (aTypeInfo=nil) or (aTypeInfo^.Kind<>tkEnumeration) then result := '' else result := aTypeInfo^.EnumBaseType^.GetCaption(PByte(@aIndex)^); end; function GetEnumNameTrimed(aTypeInfo: PTypeInfo; const aIndex): RawUTF8; begin if (aTypeInfo=nil) or (aTypeInfo^.Kind<>tkEnumeration) then result := '' else result := aTypeInfo^.EnumBaseType^.GetEnumNameTrimed(aIndex); end; function GetSetNameCSV(aTypeInfo: PTypeInfo; const aValue): RawUTF8; begin if (aTypeInfo=nil) or (aTypeInfo^.Kind<>tkSet) then result := '' else result := aTypeInfo^.SetEnumType^.GetSetNameCSV(integer(aValue)); end; {$ifndef NOVARIANTS} function DocVariantToObject(var doc: TDocVariantData; obj: TObject): boolean; var p: integer; prop: PPropInfo; begin if (doc.Kind=dvObject) and (doc.Count>0) and (obj<>nil) then begin for p := 0 to doc.Count-1 do begin prop := ClassFieldPropWithParentsFromUTF8( PPointer(obj)^,pointer(doc.Names[p]),length(doc.Names[p])); if prop<>nil then prop^.SetFromVariant(obj,doc.Values[p]); end; result := true; end else result := false; end; procedure DocVariantToObjArray(var arr: TDocVariantData; var objArray; objClass: TClass); var instance: TClassInstance; begin instance.Init(objClass); DocVariantToObjArray(arr,objArray,@instance); end; procedure DocVariantToObjArray(var arr: TDocVariantData; var objArray; objClass: PClassInstance); var i: integer; obj: TObjectDynArray absolute objArray; begin if objClass=nil then exit; ObjArrayClear(obj); if not(dvoIsArray in arr.Options) or (arr.Count=0) then exit; SetLength(obj,arr.Count); for i := 0 to arr.Count-1 do begin obj[i] := objClass^.CreateNew; DocVariantToObject(_Safe(arr.Values[i])^,obj[i]); end; end; function ObjectDefaultToVariant(aClass: TClass; aOptions: TDocVariantOptions): variant; var instance: TClassInstance; temp: TObject; json: RawUTF8; begin VarClear(result); instance.Init(aClass); temp := instance.CreateNew; try json := ObjectToJSON(temp,[woDontStoreDefault]); PDocVariantData(@result)^.InitJSONInPlace(pointer(json),aOptions); finally temp.Free; end; end; {$endif} type // those classes will be used to register globally some classes for JSON TJSONSerializerRegisteredClassAbstract = class(TSynList) protected fLastClass: TClass; fSafe: TSynLocker; public constructor Create; override; destructor Destroy; override; end; TJSONSerializerRegisteredClass = class(TJSONSerializerRegisteredClassAbstract) protected public procedure AddOnce(aItemClass: TClass); function Find(JSON: PUTF8Char; AndRegisterClass: boolean): TClass; overload; function Find(aClassName: PUTF8Char; aClassNameLen: integer): TClass; overload; end; var JSONSerializerRegisteredClass: TJSONSerializerRegisteredClass=nil; { TSQLPropInfo } const NULL_SHORTSTRING: string[1] = ''; function TSQLPropInfo.GetSQLFieldTypeName: PShortString; begin if self=nil then result := @NULL_SHORTSTRING else result := ToText(fSQLFieldType); end; function TSQLPropInfo.GetSQLFieldRTTITypeName: RawUTF8; begin result := GetDisplayNameFromClass(ClassType); if IdemPChar(pointer(result),'PROPINFO') then delete(result,1,8); end; function TSQLPropInfo.GetNameDisplay: string; begin GetCaptionFromPCharLen(pointer(fName),result); end; procedure TSQLPropInfo.TextToBinary(Value: PUTF8Char; var result: RawByteString); begin result := BlobToTSQLRawBlob(Value); end; procedure TSQLPropInfo.BinaryToText(var Value: RawUTF8; ToSQL: boolean; wasSQLString: PBoolean); begin if Value='' then begin if wasSQLString<>nil then wasSQLString^ := false; Value := NULL_STR_VAR; end else begin if wasSQLString<>nil then wasSQLString^ := true; if ToSQL then // encode as BLOB literals (e.g. "X'53514C697465'") Value := TSQLRawBlobToBlob(TSQLRawBlob(Value)) else // JSON content is e.g. '\uFFF0base64encodedbinary' Value := BinToBase64WithMagic(Value); end; end; {$ifndef NOVARIANTS} function NullableTypeToSQLFieldType(aType: pointer): TSQLFieldType; begin if aType<>nil then if aType<>TypeInfo(TNullableInteger) then if aType<>TypeInfo(TNullableUTF8Text) then if aType<>TypeInfo(TNullableBoolean) then if aType<>TypeInfo(TNullableFloat) then if aType<>TypeInfo(TNullableCurrency) then if aType<>TypeInfo(TNullableDateTime) then if aType<>TypeInfo(TNullableTimeLog) then begin result := sftUnknown; exit; end else result := sftTimeLog else result := sftDateTime else result := sftCurrency else result := sftFloat else result := sftBoolean else result := sftUTF8Text else result := sftInteger else result := sftUnknown; end; {$endif NOVARIANTS} const SQLFIELDTYPETODBFIELDTYPE: array[TSQLFieldType] of TSQLDBFieldType = (ftUnknown, // sftUnknown ftUTF8, // sftAnsiText ftUTF8, // sftUTF8Text ftInt64, // sftEnumerate ftInt64, // sftSet ftInt64, // sftInteger ftInt64, // sftID = TSQLRecord(aID) ftInt64, // sftRecord = TRecordReference = RecordRef ftInt64, // sftBoolean ftDouble, // sftFloat ftDate, // sftDateTime ftInt64, // sftTimeLog ftCurrency, // sftCurrency ftUTF8, // sftObject {$ifndef NOVARIANTS} ftUTF8, // sftVariant ftNull, // sftNullable {$endif} ftBlob, // sftBlob ftBlob, // sftBlobDynArray ftBlob, // sftBlobCustom ftUTF8, // sftUTF8Custom ftUnknown, // sftMany ftInt64, // sftModTime ftInt64, // sftCreateTime ftInt64, // sftTID ftInt64, // sftRecordVersion = TRecordVersion ftInt64, // sftSessionUserID ftDate, // sftDateTimeMS ftInt64, // sftUnixTime = TUnixTime ftInt64); // sftUnixMSTime = TUnixMSTime function SQLFieldTypeToDBField(aSQLFieldType: TSQLFieldType; aTypeInfo: pointer): TSQLDBFieldType; {$ifdef HASINLINE}inline;{$endif} begin {$ifndef NOVARIANTS} if aSQLFieldType=sftNullable then aSQLFieldType := NullableTypeToSQLFieldType(aTypeInfo); {$endif} result := SQLFIELDTYPETODBFIELDTYPE[aSQLFieldType]; end; constructor TSQLPropInfo.Create(const aName: RawUTF8; aSQLFieldType: TSQLFieldType; aAttributes: TSQLPropInfoAttributes; aFieldWidth, aPropertyIndex: integer); begin if aName='' then EORMException.CreateUTF8('Void name for %.Create',[self]); if aAuxiliaryRTreeField in aAttributes then fName := copy(aName,2,MaxInt) else fName := aName; fNameUnflattened := fName; fSQLFieldType := aSQLFieldType; fSQLFieldTypeStored := aSQLFieldType; fSQLDBFieldType := SQLFIELDTYPETODBFIELDTYPE[fSQLFieldTypeStored]; fAttributes := aAttributes; fFieldWidth := aFieldWidth; fPropertyIndex := aPropertyIndex; end; function TSQLPropInfo.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; var tmp: RawUTF8; begin GetValueVar(Instance,false,tmp,nil); result := crc32c(0,pointer(tmp),length(tmp)); end; procedure TSQLPropInfo.GetJSONValues(Instance: TObject; W: TJSONSerializer); var wasString: boolean; tmp: RawUTF8; begin GetValueVar(Instance,false,tmp,@wasString); if wasString then begin W.Add('"'); if tmp<>'' then W.AddJSONEscape(pointer(tmp)); W.Add('"'); end else W.AddRawJSON(tmp); end; function TSQLPropInfo.GetValue(Instance: TObject; ToSQL: boolean; wasSQLString: PBoolean): RawUTF8; begin GetValueVar(Instance,ToSQL,Result,wasSQLString); end; procedure TSQLPropInfo.SetValueVar(Instance: TObject; const Value: RawUTF8; wasString: boolean); begin SetValue(Instance,pointer(Value),wasString); end; function TSQLPropInfo.SQLDBFieldTypeName: PShortString; begin result := ToText(fSQLDBFieldType); end; procedure TSQLPropInfo.GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar; var temp: RawByteString); begin GetValueVar(Instance,true,RawUTF8(temp),nil); aValue.Options := []; aValue.VType := fSQLDBFieldType; case aValue.VType of ftInt64: SetInt64(pointer(temp),aValue.VInt64); ftCurrency: aValue.VInt64 := StrToCurr64(pointer(temp)); ftDouble: aValue.VDouble := GetExtended(pointer(temp)); ftDate: aValue.VDateTime := Iso8601ToDateTime(temp); ftBlob: if temp='' then aValue.VType := ftNull else begin temp := BlobToTSQLRawBlob(temp); aValue.VBlob := pointer(temp); aValue.VBlobLen := length(temp); end; ftUTF8: aValue.VText := pointer(temp); else aValue.VInt64 := 0; end; end; function TSQLPropInfo.IsValueVoid(Instance: TObject): boolean; var temp: RawUTF8; wasString: boolean; begin GetValueVar(Instance,true,temp,@wasString); if wasString then result := temp='' else result := GetInt64(pointer(temp))=0; end; function TSQLPropInfo.SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; begin case aValue.VType of ftInt64: SetValueVar(Instance,Int64ToUtf8(aValue.VInt64),false); ftCurrency: SetValueVar(Instance,Curr64ToStr(aValue.VInt64),false); ftDouble: SetValueVar(Instance,DoubleToStr(aValue.VDouble),false); ftDate: SetValueVar(Instance,DateTimeToIso8601Text( aValue.VDateTime,'T',svoDateWithMS in aValue.Options),true); ftBlob: SetValueVar(Instance,TSQLRawBlobToBlob(aValue.VBlob,aValue.VBlobLen),true); ftUTF8: SetValue(Instance,aValue.VText,true); else SetValue(Instance,nil,false); end; result := true; end; const NULL_LOW = ord('n')+ord('u')shl 8+ord('l')shl 16+ord('l')shl 24; FALSE_LOW = ord('f')+ord('a')shl 8+ord('l')shl 16+ord('s')shl 24; TRUE_LOW = ord('t')+ord('r')shl 8+ord('u')shl 16+ord('e')shl 24; {$ifndef NOVARIANTS} procedure ValueVarToVariant(Value: PUTF8Char; ValueLen: integer; fieldType: TSQLFieldType; var result: TVarData; createValueTempCopy: boolean; typeInfo: pointer; options: TDocVariantOptions); const /// map our available types for any SQL field property into variant values // - varNull will be used to store a true variant instance from JSON SQL_ELEMENTTYPES: array[TSQLFieldType] of word = ( // sftUnknown, sftAnsiText, sftUTF8Text, sftEnumerate, sftSet, sftInteger, varEmpty, varString, varString, varInteger, varInt64, varInt64, // sftID, sftRecord, sftBoolean, sftFloat, sftDateTime, varInt64,varInt64,varBoolean, varDouble, varDate, // sftTimeLog, sftCurrency, sftObject, varInt64, varCurrency, varNull, // sftVariant, sftNullable sftBlob,sftBlobDynArray, {$ifndef NOVARIANTS} varNull, varNull, {$endif} varString, varNull, // sftBlobCustom, sftUTF8Custom, sftMany, sftModTime, sftCreateTime, sftTID, varString, varString, varEmpty, varInt64, varInt64, varInt64, // sftRecordVersion, sftSessionUserID, sftDateTimeMS, sftUnixTime, sftUnixMSTime varInt64, varInt64, varDate, varInt64, varInt64); procedure Complex; var tmp: TSynTempBuffer; begin if (fieldType=sftBlobDynArray) and (typeInfo<>nil) and (Value<>nil) and (Value^<>'[') and Base64MagicCheckAndDecode(Value,tmp) then Value := pointer(DynArrayBlobSaveJSON(typeInfo,tmp.buf)) else if createValueTempCopy then Value := tmp.Init(Value) else tmp.buf := nil; GetVariantFromJSON(Value,false,variant(result),@options); tmp.Done; end; var err: integer; begin VarClear(variant(result)); result.VType := SQL_ELEMENTTYPES[fieldType]; result.VAny := nil; // avoid GPF case fieldType of sftCurrency: result.VInt64 := StrToCurr64(Value); sftFloat: begin result.VDouble := GetExtended(Value,err); if err<>0 then begin result.VType := varString; FastSetString(RawUTF8(result.VAny),Value,ValueLen); end; end; sftDateTime, sftDateTimeMS: Iso8601ToDateTimePUTF8CharVar(Value,0,result.VDate); sftBoolean: result.VBoolean := not((Value=nil) or (PWord(Value)^=ord('0')) or (PInteger(Value)^=FALSE_LOW)); sftEnumerate: result.VInteger := GetInteger(Value); sftInteger, sftID, sftTID, sftRecord, sftSet, sftRecordVersion, sftSessionUserID, sftTimeLog, sftModTime, sftCreateTime, sftUnixTime, sftUnixMSTime: SetInt64(Value,result.VInt64); sftAnsiText, sftUTF8Text: FastSetString(RawUTF8(result.VAny),Value,ValueLen); sftBlobCustom, sftBlob: BlobToTSQLRawBlob(Value,TSQLRawBlob(result.VAny)); {$ifndef NOVARIANTS}sftVariant, sftNullable,{$endif} sftBlobDynArray, sftObject, sftUTF8Custom: Complex; end; end; function ObjectLoadVariant(var ObjectInstance; const aDocVariant: variant; TObjectListItemClass: TClass; Options: TJSONToObjectOptions): boolean; var tmp: RawUTF8; begin result := false; if _Safe(aDocVariant)^.Kind=dvObject then VariantSaveJSON(aDocVariant,twJSONEscape,tmp) else if VariantToUTF8(aDocVariant, tmp) and (tmp<>'') and (tmp[1]='{') then UniqueRawUTF8(tmp) else exit; JSONToObject(ObjectInstance,pointer(tmp),result,TObjectListItemClass,Options); end; procedure TSQLPropInfo.GetVariant(Instance: TObject; var Dest: Variant); var temp: RawUTF8; begin GetValueVar(Instance,true,temp,nil); ValueVarToVariant(pointer(temp),Length(temp),fSQLFieldTypeStored,TVarData(Dest),false,nil); end; procedure TSQLPropInfo.SetVariant(Instance: TObject; const Source: Variant); begin SetValueVar(Instance,VariantToUTF8(Source), (TVarData(Source).VType=varOleStr) or (TVarData(Source).VType>=varString)); end; {$endif NOVARIANTS} function TSQLPropInfo.CompareValue(Item1, Item2: TObject; CaseInsensitive: boolean): PtrInt; var tmp1,tmp2: RawUTF8; begin if Item1=Item2 then result := 0 else if Item1=nil then result := -1 else if Item2=nil then result := 1 else begin GetValueVar(Item1,false,tmp1,nil); GetValueVar(Item2,false,tmp2,nil); if CaseInsensitive then // slow, always working implementation result := StrIComp(pointer(tmp1),pointer(tmp2)) else result := StrComp(pointer(tmp1),pointer(tmp2)); end; end; procedure TSQLPropInfo.CopyProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); procedure GenericCopy; var tmp: RawUTF8; wasString: boolean; {$ifndef NOVARIANTS} val: variant; {$endif} begin {$ifndef NOVARIANTS} // force JSON serialization, e.g. for dynamic arrays if (DestInfo.SQLFieldType=sftVariant) or (SQLfieldType=sftVariant) then begin GetVariant(Source,val); DestInfo.SetVariant(Dest,val); exit; end; {$endif} GetValueVar(Source,false,tmp,@wasString); DestInfo.SetValueVar(Dest,tmp,wasString); end; var i: integer; begin if (Source=nil) or (DestInfo=nil) or (Dest=nil) then exit; // avoid GPF with TSQLPropInfoRTTI(self) do if fFromRTTI and (fFlattenedProps<>nil) then for i := 0 to length(fFlattenedProps)-1 do Source := fFlattenedProps[i].GetObjProp(Source); with TSQLPropInfoRTTI(DestInfo) do if fFromRTTI and (fFlattenedProps<>nil) then for i := 0 to length(fFlattenedProps)-1 do Dest := fFlattenedProps[i].GetObjProp(Dest); if DestInfo.ClassType=ClassType then CopySameClassProp(Source,DestInfo,Dest) else GenericCopy; end; procedure TSQLPropInfo.CopyValue(Source, Dest: TObject); begin CopySameClassProp(Source,self,Dest); end; procedure TSQLPropInfo.CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); var tmp: RawUTF8; wasString: boolean; begin GetValueVar(Source,false,tmp,@wasString); DestInfo.SetValueVar(Dest,tmp,wasString); end; { TSQLPropInfoRTTI } var SQLPropInfoRegistration: TSynDictionary = nil; class procedure TSQLPropInfoRTTI.RegisterTypeInfo(aTypeInfo: Pointer); begin if SQLPropInfoRegistration=nil then GarbageCollectorFreeAndNil(SQLPropInfoRegistration, TSynDictionary.Create(TypeInfo(TPointerDynArray),TypeInfo(TPointerDynArray))); SQLPropInfoRegistration.AddOrUpdate(aTypeInfo,self); end; class function TSQLPropInfoRTTI.CreateFrom(aPropInfo: PPropInfo; aPropIndex: integer; aOptions: TSQLPropInfoListOptions; const aFlattenedProps: PPropInfoDynArray): TSQLPropInfo; var aSQLFieldType: TSQLFieldType; aType: PTypeInfo; C: TSQLPropInfoRTTIClass; procedure FlattenedPropNameSet; var i,max: Integer; begin // Address.Street1 -> Address_Street1 (result as TSQLPropInfoRTTI).fFlattenedProps := aFlattenedProps; result.fNameUnflattened := result.fName; max := high(aFlattenedProps); for i := max downto 0 do result.fNameUnflattened := ToUTF8(aFlattenedProps[i]^.Name)+'.'+result.fNameUnflattened; if (max>=0) and (aFlattenedProps[max]^.PropType^. ClassFieldCount(pilIgnoreIfGetter in aOptions)=1) then begin // Birth.Date -> Birth or Address.Country.Iso -> Address_Country result.fName := ToUTF8(aFlattenedProps[max]^.Name); dec(max); end; for i := max downto 0 do result.fName := ToUTF8(aFlattenedProps[i]^.Name)+'_'+result.fName; end; begin if aPropInfo=nil then raise EORMException.CreateUTF8('Invalid %.CreateFrom(nil) call',[self]); result := nil; aSQLFieldType := sftUnknown; aType := aPropInfo^.TypeInfo; {$ifndef NOVARIANTS} if aType^.Kind=tkVariant then begin aSQLFieldType := NullableTypeToSQLFieldType(aType); if aSQLFieldType<>sftUnknown then // handle sftNullable type result := TSQLPropInfoRTTIVariant.Create(aPropInfo,aPropIndex,aSQLFieldType,aOptions); end; {$endif NOVARIANTS} if result=nil then begin aSQLFieldType := aType^.GetSQLFieldType; C := nil; if (SQLPropInfoRegistration=nil) or not SQLPropInfoRegistration.FindAndCopy(aType,C) then case aSQLFieldType of sftUnknown, sftBlobCustom: ; // will raise an EORMException sftBoolean, sftEnumerate: C := TSQLPropInfoRTTIEnum; sftTimeLog, sftModTime, sftCreateTime: // specific class for further use C := TSQLPropInfoRTTITimeLog; sftUnixTime: // specific class for further use C := TSQLPropInfoRTTIUnixTime; sftUnixMSTime: C := TSQLPropInfoRTTIUnixMSTime; sftCurrency: C := TSQLPropInfoRTTICurrency; sftDateTime, sftDateTimeMS: C := TSQLPropInfoRTTIDateTime; sftID: // = TSQLRecord(aID) C := TSQLPropInfoRTTIID; sftTID: // = TID or T*ID C := TSQLPropInfoRTTITID; sftSessionUserID: C := TSQLPropInfoRTTIInt64; sftRecord: // = TRecordReference/TRecordReferenceToBeDeleted C := TSQLPropInfoRTTIRecordReference; sftRecordVersion: C := TSQLPropInfoRTTIRecordVersion; sftMany: C := TSQLPropInfoRTTIMany; sftObject: C := TSQLPropInfoRTTIObject; {$ifndef NOVARIANTS} sftVariant: C := TSQLPropInfoRTTIVariant; // sftNullable already handle above {$endif NOVARIANTS} sftBlob: C := TSQLPropInfoRTTIRawBlob; sftBlobDynArray: C := TSQLPropInfoRTTIDynArray; sftUTF8Custom: // will happen only for DELPHI XE5 and up result := TSQLPropInfoCustomJSON.Create(aPropInfo,aPropIndex); else case aType^.Kind of // retrieve matched type from RTTI binary level tkInteger: C := TSQLPropInfoRTTIInt32; tkSet: C := TSQLPropInfoRTTISet; tkChar, tkWChar: C := TSQLPropInfoRTTIChar; tkInt64 {$ifdef FPC}, tkQWord{$endif}: C := TSQLPropInfoRTTIInt64; tkFloat: if aType^.FloatType=ftDoub then C := TSQLPropInfoRTTIDouble; tkLString {$ifdef FPC},tkLStringOld{$endif}: case aType^.AnsiStringCodePage of // recognize optimized UTF-8/UTF-16 CP_UTF8: C := TSQLPropInfoRTTIRawUTF8; CP_UTF16: C := TSQLPropInfoRTTIRawUnicode; else C := TSQLPropInfoRTTIAnsi; // will use the right TSynAnsiConvert end; {$ifdef HASVARUSTRING} tkUString: C := TSQLPropInfoRTTIUnicode; {$endif} tkWString: C := TSQLPropInfoRTTIWide; end; end; if C<>nil then result := C.Create(aPropInfo,aPropIndex,aSQLFieldType,aOptions); end; if result<>nil then begin if aFlattenedProps<>nil then FlattenedPropNameSet; end else if pilRaiseEORMExceptionIfNotHandled in aOptions then raise EORMException.CreateUTF8('%.CreateFrom: Unhandled %/% type for property %', [self,ToText(aSQLFieldType)^,ToText(aType^.Kind)^,aPropInfo^.Name]); end; function TSQLPropInfoRTTI.GetSQLFieldRTTITypeName: RawUTF8; begin result := ToUTF8(fPropType^.Name); end; function TSQLPropInfoRTTI.GetFieldAddr(Instance: TObject): pointer; begin if Instance=nil then result := nil else result := fPropInfo^.GetFieldAddr(Instance); end; function TSQLPropInfoRTTI.Flattened(Instance: TObject): TObject; var i: integer; begin result := Instance; for i := 0 to length(fFlattenedProps)-1 do result := fFlattenedProps[i].GetObjProp(result); end; {$ifndef NOVARIANTS} procedure TSQLPropInfoRTTI.GetVariant(Instance: TObject; var Dest: Variant); var temp: RawUTF8; begin GetValueVar(Instance,true,temp,nil); ValueVarToVariant(pointer(temp),length(temp),fSQLFieldTypeStored,TVarData(Dest),false,fPropInfo); end; {$endif NOVARIANTS} constructor TSQLPropInfoRTTI.Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType; aOptions: TSQLPropInfoListOptions); var attrib: TSQLPropInfoAttributes; begin byte(attrib) := 0; if aPropInfo^.IsStored(nil)=AS_UNIQUE then Include(attrib,aIsUnique); // property MyProperty: RawUTF8 stored AS_UNIQUE; if (pilAuxiliaryFields in aOptions) and (aPropInfo^.Name[1] = '_') then Include(attrib,aAuxiliaryRTreeField); inherited Create(ToUTF8(aPropInfo^.Name),aSQLFieldType,attrib, aPropInfo^.Index,aPropIndex); // property MyProperty: RawUTF8 index 10; -> FieldWidth=10 fPropInfo := aPropInfo; fPropType := aPropInfo^.TypeInfo; if aPropInfo.GetterIsField then begin fGetterIsFieldPropOffset := aPropInfo.GetProc{$ifndef FPC} and $00ffffff{$endif}; if (aPropInfo.SetProc=0) or (aPropInfo.SetProc=fPropInfo.GetProc) then fInPlaceCopySameClassPropOffset := fGetterIsFieldPropOffset; end; fFromRTTI := true; end; { TSQLPropInfoRTTIInt32 } constructor TSQLPropInfoRTTIInt32.Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType; aOptions: TSQLPropInfoListOptions); begin inherited Create(aPropInfo,aPropIndex,aSQLFieldType,aOptions); fUnsigned := fPropType^.OrdType in [otUByte,otUWord,otULong]; end; procedure TSQLPropInfoRTTIInt32.CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); begin TSQLPropInfoRTTIInt32(DestInfo).fPropInfo.SetOrdProp(Dest,fPropInfo.GetOrdProp(Source)); end; procedure TSQLPropInfoRTTIInt32.GetBinary(Instance: TObject; W: TFileBufferWriter); begin W.WriteVarUInt32(cardinal(fPropInfo.GetOrdProp(Instance))); end; function TSQLPropInfoRTTIInt32.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; var v: integer; begin v := fPropInfo.GetOrdProp(Instance); result := crc32cBy4(0,v); // better hash distribution using crc32c end; procedure TSQLPropInfoRTTIInt32.GetJSONValues(Instance: TObject; W: TJSONSerializer); var v: integer; begin v := fPropInfo.GetOrdProp(Instance); if fUnsigned then W.AddU(cardinal(v)) else W.Add(v); end; procedure TSQLPropInfoRTTIInt32.GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); var v: integer; begin if wasSQLString<>nil then wasSQLString^ := false; v := fPropInfo.GetOrdProp(Instance); if fUnsigned then UInt32ToUtf8(cardinal(v),result) else Int32ToUtf8(v,result); end; procedure TSQLPropInfoRTTIInt32.NormalizeValue(var Value: RawUTF8); var err,v: integer; begin v := GetInteger(pointer(Value),err); if err<>0 then Value := '' else if fUnsigned then UInt32ToUtf8(cardinal(v),Value) else Int32ToUtf8(v,Value); end; function TSQLPropInfoRTTIInt32.CompareValue(Item1, Item2: TObject; CaseInsensitive: boolean): PtrInt; var A,B: integer; begin if Item1=Item2 then result := 0 else if Item1=nil then result := -1 else if Item2=nil then result := 1 else begin A := fPropInfo.GetOrdProp(Item1); B := fPropInfo.GetOrdProp(Item2); result := {$ifdef HASINLINE}CompareInteger{$else}SortDynArrayInteger{$endif}(A,B); end; end; function TSQLPropInfoRTTIInt32.SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; var c: cardinal; begin if P<>nil then begin P := pointer(FromVarUInt32Safe(pointer(P),pointer(PEnd),c)); if P<>nil then fPropInfo.SetOrdProp(Instance,integer(c)); end; result := P; end; procedure TSQLPropInfoRTTIInt32.SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); begin fPropInfo.SetOrdProp(Instance,GetInteger(Value)); end; function TSQLPropInfoRTTIInt32.SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; begin if aValue.VType=ftInt64 then begin fPropInfo.SetOrdProp(Instance,aValue.VInt64); result := true; end else result := inherited SetFieldSQLVar(Instance,aValue); end; procedure TSQLPropInfoRTTIInt32.GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar; var temp: RawByteString); var v: integer; begin aValue.Options := []; aValue.VType := ftInt64; v := fPropInfo.GetOrdProp(Instance); if fUnsigned then aValue.VInt64 := cardinal(v) else aValue.VInt64 := v; end; { TSQLPropInfoRTTISet } constructor TSQLPropInfoRTTISet.Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType; aOptions: TSQLPropInfoListOptions); begin inherited Create(aPropInfo,aPropIndex,aSQLFieldType,aOptions); fSetEnumType := fPropType^.SetEnumType; end; { TSQLPropInfoRTTIEnum } constructor TSQLPropInfoRTTIEnum.Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType; aOptions: TSQLPropInfoListOptions); begin inherited Create(aPropInfo,aPropIndex,aSQLFieldType,aOptions); fEnumType := fPropType^.EnumBaseType; end; procedure TSQLPropInfoRTTIEnum.GetJSONValues(Instance: TObject; W: TJSONSerializer); var i: integer; begin i := fPropInfo.GetOrdProp(Instance); if fSQLFieldType=sftBoolean then W.Add(i<>0) else W.Add(i); end; function TSQLPropInfoRTTIEnum.GetCaption(Value: RawUTF8; out IntValue: integer): string; begin NormalizeValue(Value); IntValue := GetInteger(pointer(Value)); if Value='' then result := '' else result := EnumType^.GetCaption(IntValue); end; procedure TSQLPropInfoRTTIEnum.GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); var i: integer; begin if wasSQLString<>nil then wasSQLString^ := false; i := fPropInfo.GetOrdProp(Instance); if (fSQLFieldType=sftBoolean) and not ToSQL then result := BOOL_UTF8[i<>0] else UInt32ToUtf8(i,result); end; procedure TSQLPropInfoRTTIEnum.NormalizeValue(var Value: RawUTF8); var i,err: integer; begin i := GetInteger(pointer(Value),err); if err<>0 then // we allow a value stated as text if fSQLFieldType=sftBoolean then i := Ord(IdemPropNameU(Value,'TRUE') or IdemPropNameU(Value,'YES')) else i := fEnumType^.GetEnumNameValue(pointer(Value),length(Value)) else if fSQLFieldType=sftBoolean then // normalize boolean values range to 0,1 if i<>0 then i := 1; if cardinal(i)>cardinal(fEnumType^.MaxValue) then Value := '' else // only set a valid value UInt32ToUtf8(i,Value); end; procedure TSQLPropInfoRTTIEnum.SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); var i,err,len: integer; begin if Value=nil then i := 0 else begin i := GetInteger(Value,err); if err<>0 then begin // we allow a value stated as text if fSQLFieldType=sftBoolean then begin len := StrLen(Value); i := ord(IdemPropName('TRUE',Value,len) or IdemPropName('YES',Value,len)); end else i := fEnumType^.GetEnumNameValue(Value); // -> convert into integer if cardinal(i)>cardinal(fEnumType^.MaxValue) then i := 0; // only set a valid text value end else if fSQLFieldType=sftBoolean then // normalize boolean values range to 0,1 if i<>0 then i := 1; end; fPropInfo.SetOrdProp(Instance,i); end; { TSQLPropInfoRTTIChar } procedure TSQLPropInfoRTTIChar.GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); var w: WideChar; begin w := WideChar(fPropInfo.GetOrdProp(Instance)); if ToSQL and (w=#0) then begin // 'null' and not #0 to avoid end of SQL text - JSON will escape #0 result := NULL_STR_VAR; if wasSQLString<>nil then wasSQLString^ := false; end else begin RawUnicodeToUtf8(@w,1,result); if wasSQLString<>nil then wasSQLString^ := true; end; end; procedure TSQLPropInfoRTTIChar.NormalizeValue(var Value: RawUTF8); begin // do nothing: should already be UTF-8 encoded end; procedure TSQLPropInfoRTTIChar.SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); var i: integer; begin if (Value=nil) or (PInteger(Value)^=NULL_LOW) then i := 0 else i := GetUTF8Char(Value); fPropInfo.SetOrdProp(Instance,i); end; { TSQLPropInfoRTTIInt64 } constructor TSQLPropInfoRTTIInt64.Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType; aOptions: TSQLPropInfoListOptions); begin inherited Create(aPropInfo,aPropIndex,aSQLFieldType,aOptions); fIsQWord := fPropType^.IsQword; end; procedure TSQLPropInfoRTTIInt64.CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); begin TSQLPropInfoRTTIInt64(DestInfo).fPropInfo.SetInt64Prop(Dest,fPropInfo.GetInt64Prop(Source)); end; procedure TSQLPropInfoRTTIInt64.GetBinary(Instance: TObject; W: TFileBufferWriter); var V64: Int64; begin V64 := fPropInfo.GetInt64Prop(Instance); W.Write(@V64,SizeOf(Int64)); end; function TSQLPropInfoRTTIInt64.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; var V64: TQWordRec; begin if fGetterIsFieldPropOffset<>0 then V64.V := PInt64(PtrUInt(Instance)+fGetterIsFieldPropOffset)^ else V64.V := fPropInfo.GetInt64Prop(Instance); result := crc32cBy4(V64.L,V64.H); // better hash distribution using crc32c end; procedure TSQLPropInfoRTTIInt64.GetJSONValues(Instance: TObject; W: TJSONSerializer); var V64: Int64; begin if fGetterIsFieldPropOffset<>0 then V64 := PInt64(PtrUInt(Instance)+fGetterIsFieldPropOffset)^ else V64 := fPropInfo.GetInt64Prop(Instance); if fIsQWord then W.AddQ(V64) else W.Add(V64); end; procedure TSQLPropInfoRTTIInt64.GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); var V64: Int64; begin if wasSQLString<>nil then wasSQLString^ := false; if fGetterIsFieldPropOffset<>0 then V64 := PInt64(PtrUInt(Instance)+fGetterIsFieldPropOffset)^ else V64 := fPropInfo.GetInt64Prop(Instance); if fIsQWord then UInt64ToUtf8(V64,result) else Int64ToUtf8(V64,result); end; procedure TSQLPropInfoRTTIInt64.NormalizeValue(var Value: RawUTF8); var err: integer; V64: Int64; begin if fIsQWord then begin V64 := GetQWord(pointer(Value),err); if err<>0 then Value := '' else UInt64ToUtf8(V64,Value); end else begin V64 := GetInt64(pointer(Value),err); if err<>0 then Value := '' else Int64ToUtf8(V64,Value); end; end; function TSQLPropInfoRTTIInt64.CompareValue(Item1, Item2: TObject; CaseInsensitive: boolean): PtrInt; var V1,V2: Int64; begin if Item1=Item2 then result := 0 else if Item1=nil then result := -1 else if Item2=nil then result := 1 else begin if fGetterIsFieldPropOffset<>0 then begin V1 := PInt64(PtrUInt(Item1)+fGetterIsFieldPropOffset)^; V2 := PInt64(PtrUInt(Item2)+fGetterIsFieldPropOffset)^; end else begin V1 := fPropinfo.GetInt64Prop(Item1); V2 := fPropinfo.GetInt64Prop(Item2); end; if fIsQWord then result := {$ifdef HASINLINE}CompareQWord{$else}SortDynArrayQWord{$endif}(V1,V2) else result := {$ifdef HASINLINE}CompareInt64{$else}SortDynArrayInt64{$endif}(V1,V2); end; end; function TSQLPropInfoRTTIInt64.SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; begin if P=nil then result := nil else begin result := P+SizeOf(Int64); if result>PEnd then result := nil else fPropInfo.SetInt64Prop(Instance,PInt64(P)^); end; end; procedure TSQLPropInfoRTTIInt64.SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); var V64: Int64; begin if fIsQWord then SetQWord(Value,PQword(@V64)^) else SetInt64(Value,V64); fPropInfo.SetInt64Prop(Instance,V64); end; function TSQLPropInfoRTTIInt64.SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; begin if aValue.VType=ftInt64 then begin fPropInfo.SetInt64Prop(Instance,aValue.VInt64); result := true; end else result := inherited SetFieldSQLVar(Instance,aValue); end; procedure TSQLPropInfoRTTIInt64.GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar; var temp: RawByteString); begin aValue.Options := []; aValue.VType := ftInt64; aValue.VInt64 := fPropInfo.GetInt64Prop(Instance); end; { TSQLPropInfoRTTIDouble } procedure TSQLPropInfoRTTIDouble.CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); begin TSQLPropInfoRTTIDouble(DestInfo).fPropInfo.SetDoubleProp(Dest, fPropInfo.GetDoubleProp(Source)); end; procedure TSQLPropInfoRTTIDouble.GetJSONValues(Instance: TObject; W: TJSONSerializer); begin W.AddDouble(fPropInfo.GetDoubleProp(Instance)); end; procedure TSQLPropInfoRTTIDouble.GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); begin DoubleToStr(fPropInfo.GetDoubleProp(Instance),result); if wasSQLString<>nil then wasSQLString^ := (result='') or not (result[1] in ['0'..'9']); end; procedure TSQLPropInfoRTTIDouble.NormalizeValue(var Value: RawUTF8); var VFloat: TSynExtended; err: integer; begin VFloat := GetExtended(pointer(Value),err); if err<>0 then Value := '' else DoubleToStr(VFloat,Value); end; procedure TSQLPropInfoRTTIDouble.SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); var V: double; err: integer; begin if Value=nil then V := 0 else begin V := GetExtended(pointer(Value),err); if err<>0 then V := 0; end; fPropInfo.SetDoubleProp(Instance,V); end; function TSQLPropInfoRTTIDouble.CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; begin if Item1=Item2 then result := 0 else if Item1=nil then result := -1 else if Item2=nil then result := 1 else result := CompareFloat(fPropInfo.GetDoubleProp(Item1),fPropInfo.GetDoubleProp(Item2)); end; function TSQLPropInfoRTTIDouble.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; var V: double; begin V := fPropInfo.GetDoubleProp(Instance); with PQWordRec(@V)^ do result := crc32cBy4(L,H); // better hash distribution using crc32c end; procedure TSQLPropInfoRTTIDouble.GetBinary(Instance: TObject; W: TFileBufferWriter); var V: double; begin V := fPropInfo.GetDoubleProp(Instance); W.Write(@V,SizeOf(V)); end; {$ifndef FPC_REQUIRES_PROPER_ALIGNMENT} type unaligned = Double; {$endif} function TSQLPropInfoRTTIDouble.SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; begin if P=nil then result := nil else begin result := P+SizeOf(double); if result>PEnd then result := nil else fPropInfo.SetDoubleProp(Instance,unaligned(PDouble(P)^)); end; end; function TSQLPropInfoRTTIDouble.SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; var V: double; begin case aValue.VType of ftCurrency: V := aValue.VCurrency; ftDouble, ftDate: V := aValue.VDouble; ftInt64: V := aValue.VInt64; else begin result := inherited SetFieldSQLVar(Instance,aValue); exit; end; end; fPropInfo.SetDoubleProp(Instance,V); result := true; end; procedure TSQLPropInfoRTTIDouble.GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar; var temp: RawByteString); begin aValue.Options := []; aValue.VType := ftDouble; aValue.VDouble := fPropInfo.GetDoubleProp(Instance); end; { TSQLPropInfoRTTICurrency } procedure TSQLPropInfoRTTICurrency.CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); begin TSQLPropInfoRTTICurrency(DestInfo).fPropInfo.SetCurrencyProp(Dest, fPropInfo.GetCurrencyProp(Source)); end; procedure TSQLPropInfoRTTICurrency.GetJSONValues(Instance: TObject; W: TJSONSerializer); begin W.AddCurr64(fPropInfo.GetCurrencyProp(Instance)); end; procedure TSQLPropInfoRTTICurrency.GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); begin if wasSQLString<>nil then wasSQLString^ := false; result := CurrencyToStr(fPropInfo.GetCurrencyProp(Instance)); end; procedure TSQLPropInfoRTTICurrency.NormalizeValue(var Value: RawUTF8); begin Value := Curr64ToStr(StrToCurr64(pointer(Value))); end; procedure TSQLPropInfoRTTICurrency.SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); var tmp: Int64; begin tmp := StrToCurr64(Value,nil); fPropInfo.SetCurrencyProp(Instance,PCurrency(@tmp)^); end; function TSQLPropInfoRTTICurrency.CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; var V1, V2: currency; begin if Item1=Item2 then result := 0 else if Item1=nil then result := -1 else if Item2=nil then result := 1 else begin V1 := fPropInfo.GetCurrencyProp(Item1); V2 := fPropInfo.GetCurrencyProp(Item2); result := CompareInt64(PInt64(@V1)^,PInt64(@V2)^); end; end; function TSQLPropInfoRTTICurrency.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; var V: currency; begin V := fPropInfo.GetCurrencyProp(Instance); with PQWordRec(@V)^ do result := crc32cBy4(L,H); // better hash distribution using crc32c end; procedure TSQLPropInfoRTTICurrency.GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar; var temp: RawByteString); begin aValue.Options := []; aValue.VType := ftCurrency; aValue.VCurrency := fPropInfo.GetCurrencyProp(Instance); end; function TSQLPropInfoRTTICurrency.SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; var V: Currency; begin case aValue.VType of ftDouble, ftDate: V := aValue.VDouble; ftInt64: V := aValue.VInt64; ftCurrency: V := aValue.VCurrency; else begin result := inherited SetFieldSQLVar(Instance,aValue); exit; end; end; fPropInfo.SetCurrencyProp(Instance,V); result := true; end; procedure TSQLPropInfoRTTICurrency.GetBinary(Instance: TObject; W: TFileBufferWriter); var V: Currency; begin V := fPropInfo.GetCurrencyProp(Instance); W.Write(@V,SizeOf(V)); end; function TSQLPropInfoRTTICurrency.SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; begin if P=nil then result := nil else begin result := P+SizeOf(Currency); if result>PEnd then result := nil else fPropInfo.SetCurrencyProp(Instance,PCurrency(P)^); end; end; { TSQLPropInfoRTTIDateTime } procedure TSQLPropInfoRTTIDateTime.GetJSONValues(Instance: TObject; W: TJSONSerializer); begin W.Add('"'); W.AddDateTime(fPropInfo.GetDoubleProp(Instance),fSQLFieldType=sftDateTimeMS); W.Add('"'); end; function TSQLPropInfoRTTIDateTime.CompareValue(Item1,Item2: TObject; CaseInsensitive: boolean): PtrInt; const PRECISION: array[boolean] of double = (1/SecsPerDay, 1/MSecsPerDay); var V1, V2: double; begin if Item1=Item2 then result := 0 else if Item1=nil then result := -1 else if Item2=nil then result := 1 else begin V1 := fPropInfo.GetDoubleProp(Item1); V2 := fPropInfo.GetDoubleProp(Item2); if SynCommons.SameValue(V1,V2,PRECISION[fSQLFieldType=sftDateTimeMS]) then result := 0 else if V1>V2 then result := 1 else result := -1; end; end; procedure TSQLPropInfoRTTIDateTime.GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); begin if wasSQLString<>nil then wasSQLString^ := true; DateTimeToIso8601TextVar(fPropInfo.GetDoubleProp(Instance), 'T',result,fSQLFieldType=sftDateTimeMS); end; procedure TSQLPropInfoRTTIDateTime.NormalizeValue(var Value: RawUTF8); begin DateTimeToIso8601TextVar(Iso8601ToDateTime(Value),'T',Value,fSQLFieldType=sftDateTimeMS); end; procedure TSQLPropInfoRTTIDateTime.SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); var V: TDateTime; begin Iso8601ToDateTimePUTF8CharVar(Value,0,V); fPropInfo.SetDoubleProp(Instance,V); end; procedure TSQLPropInfoRTTIDateTime.GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar; var temp: RawByteString); begin if fSQLFieldType=sftDateTimeMS then aValue.Options := [svoDateWithMS] else aValue.Options := []; aValue.VType := ftDate; aValue.VDouble := fPropInfo.GetDoubleProp(Instance); end; { TSQLPropInfoRTTIMany } // TSQLRecordMany stores nothing within the table procedure TSQLPropInfoRTTIMany.GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); begin result := ''; end; procedure TSQLPropInfoRTTIMany.SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); begin end; procedure TSQLPropInfoRTTIMany.GetBinary(Instance: TObject; W: TFileBufferWriter); begin end; function TSQLPropInfoRTTIMany.SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; begin result := P; end; { TSQLPropInfoRTTIInstance } constructor TSQLPropInfoRTTIInstance.Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType; aOptions: TSQLPropInfoListOptions); begin inherited Create(aPropInfo,aPropIndex,aSQLFieldType,aOptions); fObjectClass := fPropType^.ClassType^.ClassType; end; function TSQLPropInfoRTTIInstance.GetInstance(Instance: TObject): TObject; begin result := fPropInfo.GetObjProp(Instance); end; procedure TSQLPropInfoRTTIInstance.SetInstance(Instance, Value: TObject); begin fPropInfo.SetOrdProp(Instance,PtrInt(Value)); end; { TSQLPropInfoRTTIRecordReference } constructor TSQLPropInfoRTTIRecordReference.Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType; aOptions: TSQLPropInfoListOptions); begin inherited Create(aPropInfo,aPropIndex,aSQLFieldType,aOptions); fCascadeDelete := IdemPropName(fPropType^.Name,'TRecordReferenceToBeDeleted') end; { TSQLPropInfoRTTITID } constructor TSQLPropInfoRTTITID.Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType; aOptions: TSQLPropInfoListOptions); var TypeName: PShortString; ItemClass: TClass; begin inherited Create(aPropInfo,aPropIndex,aSQLFieldType,aOptions); TypeName := @fPropType^.Name; if IdemPropName(TypeName^,'TID') or (ord(TypeName^[1]) and $df<>ord('T')) or // expect T...ID pattern (PWord(@TypeName^[ord(TypeName^[0])-1])^ and $dfdf<>ord('I')+ord('D') shl 8) or (JSONSerializerRegisteredClass=nil) then exit; if (ord(TypeName^[0])>13) and IdemPropName('ToBeDeletedID',@TypeName^[ord(TypeName^[0])-12],13) then begin // 'TSQLRecordClientToBeDeletedID' -> TSQLRecordClient + CascadeDelete=true fCascadeDelete := true; ItemClass := JSONSerializerRegisteredClass.Find(@TypeName^[1],ord(TypeName^[0])-13); end else // 'TSQLRecordClientID' -> TSQLRecordClient ItemClass := JSONSerializerRegisteredClass.Find(@TypeName^[1],ord(TypeName^[0])-2); if (ItemClass<>nil) and ItemClass.InheritsFrom(TSQLRecord) then fRecordClass := pointer(ItemClass); end; { TSQLPropInfoRTTIID } procedure TSQLPropInfoRTTIID.SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); begin if TSQLRecord(Instance).fFill.JoinedFields then raise EORMException.CreateUTF8('%(%).SetValue after Create*Joined',[self,Name]); inherited SetValue(Instance,Value,wasString); end; procedure TSQLPropInfoRTTIID.GetJSONValues(Instance: TObject; W: TJSONSerializer); var ID: PtrUInt; begin ID := fPropInfo.GetOrdProp(Instance); if TSQLRecord(Instance).fFill.JoinedFields then ID := TSQLRecord(ID).fID; W.AddU(ID); end; { TSQLPropInfoRTTIIObject } procedure TSQLPropInfoRTTIObject.CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); var S,D: TObject; begin // generic case: copy also class content (create instances) S := GetInstance(Source); D := TSQLPropInfoRTTIObject(DestInfo).GetInstance(Dest); {$ifndef LVCL} if S.InheritsFrom(TCollection) then CopyCollection(TCollection(S),TCollection(D)) else {$endif} if S.InheritsFrom(TStrings) and D.InheritsFrom(TStrings) then CopyStrings(TStrings(S),TStrings(D)) else begin D.Free; // release previous instance TSQLPropInfoRTTIObject(DestInfo).SetInstance(Dest,CopyObject(S)); end; end; procedure TSQLPropInfoRTTIObject.SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); var valid: boolean; tmp: TSynTempBuffer; begin tmp.Init(Value); // private copy since the buffer will be modified try PropInfo^.ClassFromJSON(Instance,tmp.buf,valid,JSONTOOBJECT_TOLERANTOPTIONS); finally tmp.Done; end; end; procedure TSQLPropInfoRTTIObject.GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); begin if wasSQLString<>nil then wasSQLString^ := true; result := ObjectToJSON(GetInstance(Instance)); end; procedure TSQLPropInfoRTTIObject.GetBinary(Instance: TObject; W: TFileBufferWriter); begin // serialize object as JSON UTF-8 TEXT - not fast, but works W.Write(ObjectToJSON(GetInstance(Instance))); end; function TSQLPropInfoRTTIObject.SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; var valid: boolean; tmp: TSynTempBuffer; begin // unserialize object from JSON UTF-8 TEXT - not fast, but works FromVarString(PByte(P),PByte(PEnd),tmp); try PropInfo^.ClassFromJSON(Instance,tmp.buf,valid,JSONTOOBJECT_TOLERANTOPTIONS); finally tmp.Done; end; if valid then result := P else result := nil; end; function TSQLPropInfoRTTIObject.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; var tmp: RawUTF8; begin // JSON is case-sensitive by design -> ignore CaseInsensitive parameter tmp := ObjectToJSON(GetInstance(Instance)); result := crc32c(0,pointer(tmp),length(tmp)); end; procedure TSQLPropInfoRTTIObject.NormalizeValue(var Value: RawUTF8); begin // do nothing: should already be normalized end; procedure TSQLPropInfoRTTIObject.GetJSONValues(Instance: TObject; W: TJSONSerializer); begin if jwoAsJsonNotAsString in W.fSQLRecordOptions then W.WriteObject(GetInstance(Instance)) else W.WriteObjectAsString(GetInstance(Instance)); end; { TSQLPropInfoRTTIAnsi } constructor TSQLPropInfoRTTIAnsi.Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType; aOptions: TSQLPropInfoListOptions); begin inherited; fEngine := TSynAnsiConvert.Engine(aPropInfo^.PropType^.AnsiStringCodePage); end; procedure TSQLPropInfoRTTIAnsi.CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); var Value: RawByteString; begin if (TSQLPropInfoRTTIAnsi(DestInfo).fEngine=fEngine) then begin fPropInfo.GetLongStrProp(Source,Value); TSQLPropInfoRTTIAnsi(DestInfo).fPropInfo.SetLongStrProp(Dest,Value); end else begin GetValueVar(Source,false,RawUTF8(Value),nil); DestInfo.SetValueVar(Dest,Value,true); end; end; procedure TSQLPropInfoRTTIAnsi.GetBinary(Instance: TObject; W: TFileBufferWriter); var Value: RawByteString; begin fPropInfo.GetLongStrProp(Instance,Value); W.Write(Value); end; function TSQLPropInfoRTTIAnsi.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; var Up: array[byte] of AnsiChar; // avoid slow heap allocation Value: RawByteString; begin fPropInfo.GetLongStrProp(Instance,Value); if CaseInsensitive then if fEngine.CodePage=CODEPAGE_US then result := crc32c(0,Up,UpperCopyWin255(Up,Value)-Up) else result := crc32c(0,Up,UpperCopy255Buf(Up,pointer(Value),length(Value))-Up) else result := crc32c(0,pointer(Value),length(Value)); end; procedure TSQLPropInfoRTTIAnsi.GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); var tmp: RawByteString; begin if wasSQLString<>nil then wasSQLString^ := true; fPropInfo.GetLongStrProp(Instance,tmp); result := fEngine.AnsiBufferToRawUTF8(pointer(tmp),length(tmp)); end; procedure TSQLPropInfoRTTIAnsi.NormalizeValue(var Value: RawUTF8); begin // do nothing: should already be UTF-8 encoded end; function TSQLPropInfoRTTIAnsi.CompareValue(Item1, Item2: TObject; CaseInsensitive: boolean): PtrInt; var tmp1,tmp2: RawByteString; begin if Item1=Item2 then result := 0 else if Item1=nil then result := -1 else if Item2=nil then result := 1 else begin fPropInfo.GetLongStrProp(Item1,tmp1); fPropInfo.GetLongStrProp(Item2,tmp2); if CaseInsensitive then if fEngine.CodePage=CODEPAGE_US then result := AnsiIComp(pointer(tmp1),pointer(tmp2)) else result := StrIComp(pointer(tmp1),pointer(tmp2)) else result := StrComp(pointer(tmp1),pointer(tmp2)); end; end; function TSQLPropInfoRTTIAnsi.SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; var tmp: RawByteString; begin FromVarString(PByte(P),PByte(PEnd),tmp,fEngine.CodePage); fPropInfo.SetLongStrProp(Instance,tmp); result := P; end; procedure TSQLPropInfoRTTIAnsi.SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); begin if Value=nil then fPropInfo.SetLongStrProp(Instance,'') else fPropInfo.SetLongStrProp(Instance,fEngine.UTF8BufferToAnsi(Value,StrLen(Value))); end; procedure TSQLPropInfoRTTIAnsi.SetValueVar(Instance: TObject; const Value: RawUTF8; wasString: boolean); begin fPropInfo.SetLongStrProp(Instance,fEngine.UTF8ToAnsi(Value)); end; procedure TSQLPropInfoRTTIAnsi.GetJSONValues(Instance: TObject; W: TJSONSerializer); var tmp: RawByteString; begin W.Add('"'); fPropInfo.GetLongStrProp(Instance,tmp); if tmp<>'' then W.AddAnyAnsiString(tmp,twJSONEscape,fEngine.CodePage); W.Add('"'); end; function TSQLPropInfoRTTIAnsi.SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; var tmp: RawByteString; begin case aValue.VType of ftNull: ; // leave tmp='' ftUTF8: fEngine.UTF8BufferToAnsi(aValue.VText,StrLen(aValue.VText),tmp); else begin result := inherited SetFieldSQLVar(Instance,aValue); exit; end; end; fPropInfo.SetLongStrProp(Instance,tmp); result := True; end; procedure TSQLPropInfoRTTIAnsi.GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar; var temp: RawByteString); begin fPropInfo.GetLongStrProp(Instance,temp); temp := fEngine.AnsiToUTF8(temp); aValue.Options := []; aValue.VType := ftUTF8; aValue.VText := pointer(temp); end; procedure TSQLPropInfoRTTIAnsi.CopyValue(Source, Dest: TObject); begin // avoid temporary variable use, for simple fields with no getter/setter if fInPlaceCopySameClassPropOffset=0 then fPropInfo.CopyLongStrProp(Source,Dest) else PRawByteString(PtrUInt(Dest)+fInPlaceCopySameClassPropOffset)^ := PRawByteString(PtrUInt(Source)+fInPlaceCopySameClassPropOffset)^; end; { TSQLPropInfoRTTIRawUTF8 } procedure TSQLPropInfoRTTIRawUTF8.CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); var Value: RawByteString; begin // don't know why, but fInPlaceCopySameClassPropOffset trick leaks memory :( fPropInfo.GetLongStrProp(Source,Value); TSQLPropInfoRTTIRawUTF8(DestInfo).fPropInfo.SetLongStrProp(Dest,Value); end; function TSQLPropInfoRTTIRawUTF8.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; var Up: array[byte] of AnsiChar; // avoid slow heap allocation Value: RawByteString; begin fPropInfo.GetLongStrProp(Instance,Value); if CaseInsensitive then result := crc32c(0,Up,UTF8UpperCopy255(Up,Value)-Up) else result := crc32c(0,pointer(Value),length(Value)); end; procedure TSQLPropInfoRTTIRawUTF8.GetJSONValues(Instance: TObject; W: TJSONSerializer); var tmp: RawByteString; begin fPropInfo.GetLongStrProp(Instance,tmp); if fPropType=TypeInfo(RawJSON) then W.AddRawJSON(tmp) else begin W.Add('"'); if tmp<>'' then W.AddJSONEscape(pointer(tmp)); W.Add('"'); end; end; procedure TSQLPropInfoRTTIRawUTF8.GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); begin if wasSQLString<>nil then wasSQLString^ := fPropType<>TypeInfo(RawJSON); fPropInfo.GetLongStrProp(Instance,RawByteString(result)); end; function TSQLPropInfoRTTIRawUTF8.SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; begin fPropInfo.SetLongStrProp(Instance,FromVarString(PByte(P),PByte(PEnd))); result := P; end; function TSQLPropInfoRTTIRawUTF8.SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; var tmp: RawByteString; begin case aValue.VType of ftNull: ; // leave tmp='' ftUTF8: SetString(tmp,PAnsiChar(aValue.VText),StrLen(aValue.VText)); else begin result := inherited SetFieldSQLVar(Instance,aValue); exit; end; end; fPropInfo.SetLongStrProp(Instance,tmp); result := True; end; procedure TSQLPropInfoRTTIRawUTF8.GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar; var temp: RawByteString); begin fPropInfo.GetLongStrProp(Instance,temp); aValue.Options := []; aValue.VType := ftUTF8; aValue.VText := Pointer(temp); end; function TSQLPropInfoRTTIRawUTF8.CompareValue(Item1, Item2: TObject; CaseInsensitive: boolean): PtrInt; function CompareWithLocalTempCopy: PtrInt; var tmp1,tmp2: RawByteString; begin fPropInfo.GetLongStrProp(Item1,tmp1); fPropInfo.GetLongStrProp(Item2,tmp2); if CaseInsensitive then result := UTF8IComp(pointer(tmp1),pointer(tmp2)) else result := StrComp(pointer(tmp1),pointer(tmp2)); end; var offs: PtrUInt; p1,p2: pointer; begin if Item1=Item2 then result := 0 else if Item1=nil then result := -1 else if Item2=nil then result := 1 else begin offs := fGetterIsFieldPropOffset; if offs<>0 then begin // avoid any temporary variable p1 := PPointer(PtrUInt(Item1)+offs)^; p2 := PPointer(PtrUInt(Item2)+offs)^; if CaseInsensitive then result := UTF8IComp(p1,p2) else result := StrComp(p1,p2); end else result := CompareWithLocalTempCopy; end; end; procedure TSQLPropInfoRTTIRawUTF8.SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); var tmp: RawUTF8; begin if Value<>nil then FastSetString(tmp,Value,StrLen(Value)); fPropInfo.SetLongStrProp(Instance,tmp); end; procedure TSQLPropInfoRTTIRawUTF8.SetValueVar(Instance: TObject; const Value: RawUTF8; wasString: boolean); begin fPropInfo.SetLongStrProp(Instance,Value); end; { TSQLPropInfoRTTIRawUnicode } procedure TSQLPropInfoRTTIRawUnicode.CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); var Value: RawByteString; begin fPropInfo.GetLongStrProp(Source,Value); TSQLPropInfoRTTIRawUnicode(DestInfo).fPropInfo.SetLongStrProp(Dest,Value); end; function TSQLPropInfoRTTIRawUnicode.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; var Up: array[byte] of AnsiChar; // avoid slow heap allocation Value: RawByteString; begin fPropInfo.GetLongStrProp(Instance,Value); if CaseInsensitive then result := crc32c(0,Up,UpperCopy255W(Up,pointer(Value),length(Value)shr 1)-Up) else result := crc32c(0,pointer(Value),length(Value)); end; procedure TSQLPropInfoRTTIRawUnicode.GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); var tmp: RawByteString; begin if wasSQLString<>nil then wasSQLString^ := true; fPropInfo.GetLongStrProp(Instance,tmp); RawUnicodeToUTF8(pointer(tmp),length(tmp)shr 1,result); end; function TSQLPropInfoRTTIRawUnicode.CompareValue(Item1, Item2: TObject; CaseInsensitive: boolean): PtrInt; var tmp1,tmp2: RawByteString; begin if Item1=Item2 then result := 0 else if Item1=nil then result := -1 else if Item2=nil then result := 1 else begin fPropInfo.GetLongStrProp(Item1,tmp1); fPropInfo.GetLongStrProp(Item2,tmp2); if CaseInsensitive then result := AnsiICompW(pointer(tmp1),pointer(tmp2)) else result := StrCompW(pointer(tmp1),pointer(tmp2)); end; end; procedure TSQLPropInfoRTTIRawUnicode.SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); begin if Value=nil then fPropInfo.SetLongStrProp(Instance,'') else fPropInfo.SetLongStrProp(Instance,Utf8DecodeToRawUnicode(Value,StrLen(Value))); end; procedure TSQLPropInfoRTTIRawUnicode.SetValueVar(Instance: TObject; const Value: RawUTF8; wasString: boolean); begin fPropInfo.SetLongStrProp(Instance,Utf8DecodeToRawUnicode(Value)); end; { TSQLPropInfoRTTIRawBlob } procedure TSQLPropInfoRTTIRawBlob.CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); var Value: RawByteString; begin fPropInfo.GetLongStrProp(Source,Value); TSQLPropInfoRTTIRawBlob(DestInfo).fPropInfo.SetLongStrProp(Dest,Value); end; function TSQLPropInfoRTTIRawBlob.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; var Value: RawByteString; begin fPropInfo.GetLongStrProp(Instance,Value); result := crc32c(0,pointer(Value),length(Value)); // binary -> case sensitive end; procedure TSQLPropInfoRTTIRawBlob.GetJSONValues(Instance: TObject; W: TJSONSerializer); var tmp: RawByteString; begin fPropInfo.GetLongStrProp(Instance,tmp); W.WrBase64(pointer(tmp),length(tmp),true); end; procedure TSQLPropInfoRTTIRawBlob.GetBlob(Instance: TObject; var Blob: RawByteString); begin fPropInfo.GetLongStrProp(Instance,Blob); end; procedure TSQLPropInfoRTTIRawBlob.SetBlob(Instance: TObject; const Blob: RawByteString); begin fPropInfo.SetLongStrProp(Instance,Blob); end; function TSQLPropInfoRTTIRawBlob.IsNull(Instance: TObject): Boolean; var Blob: RawByteString; begin fPropInfo.GetLongStrProp(Instance,Blob); result := (Blob=''); end; procedure TSQLPropInfoRTTIRawBlob.GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); begin fPropInfo.GetLongStrProp(Instance,RawByteString(result)); BinaryToText(result,ToSQL,wasSQLString); end; function TSQLPropInfoRTTIRawBlob.CompareValue(Item1, Item2: TObject; CaseInsensitive: boolean): PtrInt; var tmp1,tmp2: RawByteString; begin if Item1=Item2 then result := 0 else if Item1=nil then result := -1 else if Item2=nil then result := 1 else begin fPropInfo.GetLongStrProp(Item1,tmp1); fPropInfo.GetLongStrProp(Item2,tmp2); // BLOB is binary so always case sensitive result := StrComp(pointer(tmp1),pointer(tmp2)); end; end; procedure TSQLPropInfoRTTIRawBlob.SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); begin fPropInfo.SetLongStrProp(Instance,BlobToTSQLRawBlob(Value)); end; procedure TSQLPropInfoRTTIRawBlob.SetValueVar(Instance: TObject; const Value: RawUTF8; wasString: boolean); begin fPropInfo.SetLongStrProp(Instance,BlobToTSQLRawBlob(Value)); end; function TSQLPropInfoRTTIRawBlob.SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; var tmp: RawByteString; begin case aValue.VType of ftBlob: begin SetString(tmp,PAnsiChar(aValue.VBlob),aValue.VBlobLen); fPropInfo.SetLongStrProp(Instance,tmp); result := true; end; ftNull: begin fPropInfo.SetLongStrProp(Instance,''); result := true; end; else result := inherited SetFieldSQLVar(Instance,aValue); end; end; procedure TSQLPropInfoRTTIRawBlob.GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar; var temp: RawByteString); begin fPropInfo.GetLongStrProp(Instance,temp); aValue.Options := []; if temp='' then aValue.VType := ftNull else begin aValue.VType := ftBlob; aValue.VBlob := pointer(temp); aValue.VBlobLen := length(temp); end; end; { TSQLPropInfoRTTIWide } procedure TSQLPropInfoRTTIWide.CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); var Value: WideString; begin fPropInfo.GetWideStrProp(Source,Value); TSQLPropInfoRTTIWide(DestInfo).fPropInfo.SetWideStrProp(Dest,Value); end; procedure TSQLPropInfoRTTIWide.GetBinary(Instance: TObject; W: TFileBufferWriter); var Value: WideString; begin fPropInfo.GetWideStrProp(Instance,Value); W.Write(WideStringToUTF8(Value)); end; function TSQLPropInfoRTTIWide.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; var Up: array[byte] of AnsiChar; // avoid slow heap allocation Value: WideString; begin fPropInfo.GetWideStrProp(Instance,Value); if CaseInsensitive then result := crc32c(0,Up,UpperCopy255W(Up,pointer(Value),length(Value))-Up) else result := crc32c(0,pointer(Value),length(Value)*2); end; procedure TSQLPropInfoRTTIWide.GetJSONValues(Instance: TObject; W: TJSONSerializer); var Value: WideString; begin W.Add('"'); fPropInfo.GetWideStrProp(Instance,Value); if pointer(Value)<>nil then W.AddJSONEscapeW(pointer(Value),0); W.Add('"'); end; procedure TSQLPropInfoRTTIWide.GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); var Value: WideString; begin fPropInfo.GetWideStrProp(Instance,Value); result := WideStringToUTF8(Value); if wasSQLString<>nil then wasSQLString^ := true; end; procedure TSQLPropInfoRTTIWide.CopyValue(Source, Dest: TObject); begin // avoid temporary variable use, for simple fields with no getter/setter if fInPlaceCopySameClassPropOffset=0 then CopySameClassProp(Source,self,Dest) else PWideString(PtrUInt(Dest)+fInPlaceCopySameClassPropOffset)^ := PWideString(PtrUInt(Source)+fInPlaceCopySameClassPropOffset)^; end; function TSQLPropInfoRTTIWide.CompareValue(Item1, Item2: TObject; CaseInsensitive: boolean): PtrInt; var tmp1,tmp2: WideString; begin if Item1=Item2 then result := 0 else if Item1=nil then result := -1 else if Item2=nil then result := 1 else begin fPropInfo.GetWideStrProp(Item1,tmp1); fPropInfo.GetWideStrProp(Item2,tmp2); if CaseInsensitive then result := AnsiICompW(pointer(tmp1),pointer(tmp2)) else result := StrCompW(pointer(tmp1),pointer(tmp2)); end; end; function TSQLPropInfoRTTIWide.SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; begin fPropInfo.SetWideStrProp(Instance, UTF8ToWideString(FromVarString(PByte(P),pointer(PEnd)))); result := P; end; procedure TSQLPropInfoRTTIWide.SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); var Wide: WideString; begin if Value<>nil then UTF8ToWideString(Value,StrLen(Value),Wide); fPropInfo.SetWideStrProp(Instance,Wide); end; {$ifdef HASVARUSTRING} { TSQLPropInfoRTTIUnicode } procedure TSQLPropInfoRTTIUnicode.CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); var tmp: UnicodeString; begin fPropInfo.GetUnicodeStrProp(Source,tmp); TSQLPropInfoRTTIUnicode(DestInfo).fPropInfo.SetUnicodeStrProp(Dest,tmp); end; procedure TSQLPropInfoRTTIUnicode.GetBinary(Instance: TObject; W: TFileBufferWriter); var tmp: UnicodeString; begin fPropInfo.GetUnicodeStrProp(Instance,tmp); W.Write(UnicodeStringToUtf8(tmp)); end; procedure TSQLPropInfoRTTIUnicode.CopyValue(Source, Dest: TObject); begin // avoid temporary variable use, for simple fields with no getter/setter if fInPlaceCopySameClassPropOffset=0 then CopySameClassProp(Source,self,Dest) else PUnicodeString(PtrUInt(Dest)+fInPlaceCopySameClassPropOffset)^ := PUnicodeString(PtrUInt(Source)+fInPlaceCopySameClassPropOffset)^; end; function TSQLPropInfoRTTIUnicode.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; var Up: array[byte] of AnsiChar; // avoid slow heap allocation Value: UnicodeString; begin fPropInfo.GetUnicodeStrProp(Instance,Value); if CaseInsensitive then result := crc32c(0,Up,UpperCopy255W(Up,pointer(Value),length(Value))-Up) else result := crc32c(0,pointer(Value),length(Value)*2); end; procedure TSQLPropInfoRTTIUnicode.GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); var tmp: UnicodeString; begin fPropInfo.GetUnicodeStrProp(Instance,tmp); RawUnicodeToUtf8(pointer(tmp),length(tmp),result); if wasSQLString<>nil then wasSQLString^ := true; end; procedure TSQLPropInfoRTTIUnicode.GetJSONValues(Instance: TObject; W: TJSONSerializer); var tmp: UnicodeString; begin W.Add('"'); fPropInfo.GetUnicodeStrProp(Instance,tmp); if tmp<>'' then W.AddJSONEscapeW(pointer(tmp),0); W.Add('"'); end; function TSQLPropInfoRTTIUnicode.CompareValue(Item1, Item2: TObject; CaseInsensitive: boolean): PtrInt; var tmp1,tmp2: UnicodeString; begin if Item1=Item2 then result := 0 else if Item1=nil then result := -1 else if Item2=nil then result := 1 else begin fPropInfo.GetUnicodeStrProp(Item1,tmp1); fPropInfo.GetUnicodeStrProp(Item2,tmp2); if CaseInsensitive then result := AnsiICompW(pointer(tmp1),pointer(tmp2)) else result := StrCompW(pointer(tmp1),pointer(tmp2)); end; end; function TSQLPropInfoRTTIUnicode.SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; begin fPropInfo.SetUnicodeStrProp(Instance, UTF8DecodeToUnicodeString(FromVarString(PByte(P),pointer(PEnd)))); result := P; end; procedure TSQLPropInfoRTTIUnicode.SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); var tmp: UnicodeString; begin if Value<>nil then UTF8DecodeToUnicodeString(Value,StrLen(Value),tmp); fPropInfo.SetUnicodeStrProp(Instance,tmp); end; procedure TSQLPropInfoRTTIUnicode.SetValueVar(Instance: TObject; const Value: RawUTF8; wasString: boolean); begin fPropInfo.SetUnicodeStrProp(Instance,UTF8DecodeToUnicodeString(Value)); end; function TSQLPropInfoRTTIUnicode.SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; var tmp: UnicodeString; begin case aValue.VType of ftNull: ; // leave tmp='' ftUTF8: UTF8DecodeToUnicodeString(aValue.VText,StrLen(aValue.VText),tmp); else begin result := inherited SetFieldSQLVar(Instance,aValue); exit; end; end; fPropInfo.SetUnicodeStrProp(Instance,tmp); result := True; end; procedure TSQLPropInfoRTTIUnicode.GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar; var temp: RawByteString); var tmp: UnicodeString; begin fPropInfo.GetUnicodeStrProp(Instance,tmp); RawUnicodeToUtf8(pointer(tmp),length(tmp),RawUTF8(temp)); aValue.Options := []; aValue.VType := ftUTF8; aValue.VText := Pointer(temp); end; {$endif HASVARUSTRING} type TObjArraySerializer = class(TPointerClassHashed) protected procedure DefaultCustomWriter(const aWriter: TTextWriter; const aValue); function DefaultCustomReader(P: PUTF8Char; var aValue; out aValid: Boolean{$ifndef NOVARIANTS}; CustomVariantOptions: PDocVariantOptions{$endif}): PUTF8Char; public Instance: TClassInstance; CustomReader: TDynArrayJSONCustomReader; CustomWriter: TDynArrayJSONCustomWriter; constructor Create(aInfo: pointer; aItem: TClass; aReader: TDynArrayJSONCustomReader; aWriter: TDynArrayJSONCustomWriter); reintroduce; end; PTObjArraySerializer = ^TObjArraySerializer; constructor TObjArraySerializer.Create(aInfo: pointer; aItem: TClass; aReader: TDynArrayJSONCustomReader; aWriter: TDynArrayJSONCustomWriter); begin inherited Create(aInfo); Instance.Init(aItem); if Assigned(aReader) then CustomReader := aReader else CustomReader := DefaultCustomReader; if Assigned(aWriter) then CustomWriter := aWriter else CustomWriter := DefaultCustomWriter; end; function HasDefaultObjArrayWriter(var dyn: TDynArray): boolean; var CustomReader: TDynArrayJSONCustomReader; CustomWriter, DefaultWriter: TDynArrayJSONCustomWriter; begin result := TTextWriter.GetCustomJSONParser(dyn,CustomReader,CustomWriter); if result then begin DefaultWriter := TObjArraySerializer(nil).DefaultCustomWriter; result := PMethod(@CustomWriter)^.Code=PMethod(@DefaultWriter)^.Code; end; end; procedure TObjArraySerializer.DefaultCustomWriter(const aWriter: TTextWriter; const aValue); var opt: TTextWriterWriteObjectOptions; begin opt := DEFAULT_WRITEOPTIONS[twoIgnoreDefaultInRecord in aWriter.CustomOptions]; if twoEnumSetsAsTextInRecord in aWriter.CustomOptions then include(opt,woEnumSetsAsText); aWriter.WriteObject(TObject(aValue),opt); end; function TObjArraySerializer.DefaultCustomReader(P: PUTF8Char; var aValue; out aValid: Boolean{$ifndef NOVARIANTS}; CustomVariantOptions: PDocVariantOptions{$endif}): PUTF8Char; begin if TObject(aValue)=nil then TObject(aValue) := Instance.CreateNew; result := JSONToObject(aValue,P,aValid,nil,JSONTOOBJECT_TOLERANTOPTIONS); end; function InternalIsObjArray(aDynArrayTypeInfo: pointer): TPointerClassHashed; begin result := ObjArraySerializers.Find(aDynArrayTypeInfo); end; { TSQLPropInfoRTTIDynArray } constructor TSQLPropInfoRTTIDynArray.Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType; aOptions: TSQLPropInfoListOptions); var dummy: pointer; begin inherited Create(aPropInfo,aPropIndex,aSQLFieldType,aOptions); fObjArray := aPropInfo^.DynArrayIsObjArrayInstance; if fObjArray<>nil then fSQLDBFieldType := ftUTF8; // matches GetFieldSQLVar() below if fGetterIsFieldPropOffset=0 then raise EModelException.CreateUTF8('%.Create(%) getter!',[self,fPropType^.Name]); fWrapper.Init(fPropType,dummy); fWrapper.IsObjArray := fObjArray<>nil; fWrapper.HasCustomJSONParser; // set fWrapper.fParser end; procedure TSQLPropInfoRTTIDynArray.GetDynArray(Instance: TObject; var result: TDynArray); begin // fast assignment of fWrapper pre-initialized RTTI result.InitFrom(fWrapper,pointer(PtrUInt(Instance)+fGetterIsFieldPropOffset)^); end; procedure TSQLPropInfoRTTIDynArray.Serialize(Instance: TObject; var data: RawByteString; ExtendedJson: boolean); var da: TDynArray; temp: TTextWriterStackBuffer; begin GetDynArray(Instance,da); if da.Count=0 then data := '' else if fObjArray<>nil then with TJSONSerializer.CreateOwnedStream(temp) do try if ExtendedJson then include(fCustomOptions,twoForceJSONExtended); // smaller content AddDynArrayJSON(da); SetText(RawUTF8(data)); finally Free; end else data := da.SaveTo; end; procedure TSQLPropInfoRTTIDynArray.CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); var sda,dda: TDynArray; begin GetDynArray(Source,sda); TSQLPropInfoRTTIDynArray(DestInfo).GetDynArray(Dest,dda); if (fObjArray<>nil) or (TSQLPropInfoRTTIDynArray(DestInfo).fObjArray<>nil) or (sda.ArrayType<>dda.ArrayType) then dda.LoadFromJSON(pointer(sda.SaveToJSON)) else dda.Copy(sda); end; procedure TSQLPropInfoRTTIDynArray.GetBinary(Instance: TObject; W: TFileBufferWriter); var Value: RawByteString; begin Serialize(Instance,Value,true); if fObjArray<>nil then W.Write(Value) else W.WriteBinary(Value); end; function TSQLPropInfoRTTIDynArray.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; var tmp: RawByteString; begin Serialize(Instance,tmp,true); result := crc32c(0,pointer(tmp),length(tmp)); end; procedure TSQLPropInfoRTTIDynArray.GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); begin Serialize(Instance,RawByteString(result),false); if fObjArray=nil then BinaryToText(result,ToSQL,wasSQLString); end; {$ifndef NOVARIANTS} procedure TSQLPropInfoRTTIDynArray.GetVariant(Instance: TObject; var Dest: Variant); var json: RawUTF8; da: TDynArray; begin GetDynArray(Instance,da); json := da.SaveToJSON; VarClear(Dest); TDocVariantData(Dest).InitJSONInPlace(pointer(json),JSON_OPTIONS_FAST); end; procedure TSQLPropInfoRTTIDynArray.SetVariant(Instance: TObject; const Source: Variant); var json: RawUTF8; da: TDynArray; begin GetDynArray(Instance,da); VariantSaveJSON(Source,twJSONEscape,json); da.LoadFromJSON(pointer(json)); end; {$endif NOVARIANTS} procedure TSQLPropInfoRTTIDynArray.NormalizeValue(var Value: RawUTF8); begin // do nothing: should already be normalized end; function TSQLPropInfoRTTIDynArray.CompareValue(Item1, Item2: TObject; CaseInsensitive: boolean): PtrInt; var da1,da2: TDynArray; i: integer; begin if Item1=Item2 then result := 0 else if Item1=nil then result := -1 else if Item2=nil then result := 1 else begin GetDynArray(Item1,da1); GetDynArray(Item2,da2); result := da1.Count-da2.Count; if result<>0 then exit; result := PtrInt(Item1)-PtrInt(Item2); // pseudo comparison if fObjArray<>nil then begin for i := 0 to da1.Count-1 do if not ObjectEquals(PObjectArray(da1.Value^)[i],PObjectArray(da2.Value^)[i]) then exit; end else if not da1.Equals(da2) then exit; result := 0; end; end; function TSQLPropInfoRTTIDynArray.SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; var tmp: TSynTempBuffer; // LoadFromJSON() may change the input buffer da: TDynArray; begin GetDynArray(Instance,da); if fObjArray<>nil then begin FromVarString(PByte(P),PByte(PEnd),tmp); try // T*ObjArray use JSON serialization da.LoadFromJSON(tmp.buf); finally tmp.Done; end; result := P; end else // regular dynamic arrays use our binary encoding result := da.LoadFrom(P,nil,{nohash=}true,PEnd); end; procedure TSQLPropInfoRTTIDynArray.SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); var tmp: TSynTempBuffer; da: TDynArray; begin GetDynArray(Instance,da); if Value=nil then da.Clear else try if (fObjArray=nil) and Base64MagicCheckAndDecode(Value,tmp) then da.LoadFrom(tmp.buf,nil,{nohash=}true,PAnsiChar(tmp.buf)+tmp.len) else da.LoadFromJSON(tmp.Init(Value)); finally tmp.Done; end; end; function TSQLPropInfoRTTIDynArray.SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; var da: TDynArray; begin if aValue.VType=ftBlob then begin GetDynArray(Instance,da); result := da.LoadFrom(aValue.VBlob,nil,{nohash=}true, PAnsiChar(aValue.VBlob)+aValue.VBlobLen)<>nil; end else result := inherited SetFieldSQLVar(Instance,aValue); end; procedure TSQLPropInfoRTTIDynArray.GetJSONValues(Instance: TObject; W: TJSONSerializer); var tmp: RawByteString; begin if jwoAsJsonNotAsString in W.fSQLRecordOptions then W.AddDynArrayJSON(fPropType,GetFieldAddr(Instance)^) else if fObjArray<>nil then W.AddDynArrayJSONAsString(fPropType,GetFieldAddr(Instance)^) else begin Serialize(Instance,tmp,false); W.WrBase64(pointer(tmp),Length(tmp),true); // withMagic=true -> add "" end; end; procedure TSQLPropInfoRTTIDynArray.GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar; var temp: RawByteString); begin Serialize(Instance,temp,false); aValue.Options := []; if temp='' then aValue.VType := ftNull else if fObjArray<>nil then begin aValue.VType := ftUTF8; // JSON aValue.VText := pointer(temp); end else begin aValue.VType := ftBlob; // binary aValue.VBlob := pointer(temp); aValue.VBlobLen := length(temp); end; end; function TSQLPropInfoRTTIDynArray.GetDynArrayElemType: PTypeInfo; begin result := fWrapper.ElemType; end; {$ifndef NOVARIANTS} { TSQLPropInfoRTTIVariant } constructor TSQLPropInfoRTTIVariant.Create(aPropInfo: PPropInfo; aPropIndex: integer; aSQLFieldType: TSQLFieldType; aOptions: TSQLPropInfoListOptions); begin inherited; if aSQLFieldType=sftVariant then fDocVariantOptions := JSON_OPTIONS_FAST else fSQLFieldType := sftNullable; // TNullable* will use fSQLFieldTypeStored end; procedure TSQLPropInfoRTTIVariant.CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); var value: Variant; begin fPropInfo.GetVariantProp(Source,value); TSQLPropInfoRTTIVariant(DestInfo).fPropInfo.SetVariantProp(Dest,value); end; procedure TSQLPropInfoRTTIVariant.GetBinary(Instance: TObject; W: TFileBufferWriter); var value: Variant; begin fPropInfo.GetVariantProp(Instance,value); W.Write(value); end; function TSQLPropInfoRTTIVariant.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; var value: Variant; begin fPropInfo.GetVariantProp(Instance,value); result := VariantHash(value,CaseInsensitive); end; procedure TSQLPropInfoRTTIVariant.GetJSONValues(Instance: TObject; W: TJSONSerializer); var value: Variant; backup: TTextWriterOptions; begin fPropInfo.GetVariantProp(Instance,value); backup := W.CustomOptions; if jwoAsJsonNotAsString in W.fSQLRecordOptions then W.CustomOptions := backup+[twoForceJSONStandard]-[twoForceJSONExtended]; W.AddVariant(value,twJSONEscape); // even sftNullable should escape strings W.CustomOptions := backup; end; procedure TSQLPropInfoRTTIVariant.GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); var wasString: boolean; value: Variant; begin fPropInfo.GetVariantProp(Instance,value); VariantToUTF8(value,result,wasString); if wasSQLString<>nil then if fSQLFieldType=sftNullable then // only TNullableUTF8Text and TNullableDateTime will be actual text wasSQLString^ := (fSQLDBFieldType in TEXT_DBFIELDS) and not VarIsEmptyOrNull(value) else // from SQL point of view, variant columns are TEXT or NULL wasSQLString^ := not VarIsEmptyOrNull(value); end; procedure TSQLPropInfoRTTIVariant.GetVariant(Instance: TObject; var Dest: Variant); begin fPropInfo.GetVariantProp(Instance,Dest); end; procedure TSQLPropInfoRTTIVariant.NormalizeValue(var Value: RawUTF8); begin // content should be already normalized end; function TSQLPropInfoRTTIVariant.CompareValue(Item1, Item2: TObject; CaseInsensitive: boolean): PtrInt; function CompareWithLocalTempCopy: PtrInt; var V1,V2: variant; begin fPropInfo.GetVariantProp(Item1,V1); fPropInfo.GetVariantProp(Item2,V2); result := SortDynArrayVariantComp(TVarData(V1),TVarData(V2),CaseInsensitive); end; begin if Item1=Item2 then result := 0 else if Item1=nil then result := -1 else if Item2=nil then result := 1 else if fGetterIsFieldPropOffset<>0 then // avoid any temporary variable result := SortDynArrayVariantComp(PVarData(PtrUInt(Item1)+fGetterIsFieldPropOffset)^, PVarData(PtrUInt(Item2)+fGetterIsFieldPropOffset)^,CaseInsensitive) else result := CompareWithLocalTempCopy; end; function TSQLPropInfoRTTIVariant.SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; var value: Variant; begin // use our VariantLoad() binary serialization if fSQLFieldType=sftNullable then result := VariantLoad(value,P,nil,PEnd) else result := VariantLoad(value,P,@DocVariantOptions,PEnd); fPropInfo.SetVariantProp(Instance,value); end; procedure TSQLPropInfoRTTIVariant.SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); begin SetValuePtr(Instance,Value,StrLen(Value),wasString); end; procedure TSQLPropInfoRTTIVariant.SetValueVar(Instance: TObject; const Value: RawUTF8; wasString: boolean); begin SetValuePtr(Instance,pointer(Value),length(Value),wasString); end; procedure TSQLPropInfoRTTIVariant.SetValuePtr(Instance: TObject; Value: PUTF8Char; ValueLen: integer; wasString: boolean); var tmp: TSynTempBuffer; V: Variant; begin if ValueLen>0 then begin tmp.Init(Value,ValueLen); try if fSQLFieldType=sftNullable then if fSQLDBFieldType=ftDate then begin // decode as date/time variant TVarData(V).VType := varDate; TVarData(V).VDate := Iso8601ToDateTimePUTF8Char(Value,ValueLen); end else GetVariantFromJSON(tmp.buf,wasString,V,nil) else begin if wasString and (GotoNextNotSpace(Value)^ in ['{','[']) then wasString := false; // allow to create a TDocVariant stored as DB text GetVariantFromJSON(tmp.buf,wasString,V,@DocVariantOptions); end; fPropInfo.SetVariantProp(Instance,V); finally tmp.Done; end; end else begin TVarData(V).VType := varNull; // TEXT or NULL: see GetValueVar() fPropInfo.SetVariantProp(Instance,V); end; end; procedure TSQLPropInfoRTTIVariant.SetVariant(Instance: TObject; const Source: Variant); begin fPropInfo.SetVariantProp(Instance,Source); end; {$endif NOVARIANTS} { TSQLPropInfoCustom } function TSQLPropInfoCustom.GetFieldAddr(Instance: TObject): pointer; begin if Instance=nil then result := nil else result := PAnsiChar(Instance)+fOffset; end; constructor TSQLPropInfoCustom.Create(const aName: RawUTF8; aSQLFieldType: TSQLFieldType; aAttributes: TSQLPropInfoAttributes; aFieldWidth, aPropIndex: integer; aProperty: pointer; aData2Text: TOnSQLPropInfoRecord2Text; aText2Data: TOnSQLPropInfoRecord2Data); begin inherited Create(aName,aSQLFieldType,aAttributes,aFieldWidth,aPropIndex); fOffset := PtrUInt(aProperty); if (Assigned(aData2Text) and not Assigned(aText2Data)) or (Assigned(aText2Data) and not Assigned(aData2Text)) then raise EModelException.CreateUTF8( 'Invalid %.Create: expecting both Data2Text/Text2Data',[self]); fData2Text := aData2Text; fText2Data := aText2Data; end; procedure TSQLPropInfoCustom.TextToBinary(Value: PUTF8Char; var result: RawByteString); begin if Assigned(fText2Data) then fText2Data(Value,result) else result := BlobToTSQLRawBlob(Value); end; procedure TSQLPropInfoCustom.BinaryToText(var Value: RawUTF8; ToSQL: boolean; wasSQLString: PBoolean); begin if Assigned(fData2Text) then fData2Text(UniqueRawUTF8(Value),length(Value),Value) else inherited BinaryToText(Value,ToSQL,wasSQLString); end; { TSQLPropInfoRecordRTTI } procedure TSQLPropInfoRecordRTTI.CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); begin if TSQLPropInfoRecordRTTI(DestInfo).fTypeInfo=fTypeInfo then RecordCopy(TSQLPropInfoRecordRTTI(DestInfo).GetFieldAddr(Dest)^, GetFieldAddr(Source)^,fTypeInfo) else inherited CopySameClassProp(Source,DestInfo,Dest); end; function TSQLPropInfoRecordRTTI.GetSQLFieldRTTITypeName: RawUTF8; begin if fTypeInfo=nil then result := inherited GetSQLFieldRTTITypeName else result := ToUTF8(fTypeInfo^.Name); end; constructor TSQLPropInfoRecordRTTI.Create(aRecordInfo: PTypeInfo; const aName: RawUTF8; aPropertyIndex: integer; aPropertyPointer: pointer; aAttributes: TSQLPropInfoAttributes; aFieldWidth: integer; aData2Text: TOnSQLPropInfoRecord2Text; aText2Data: TOnSQLPropInfoRecord2Data); begin if (aRecordInfo=nil) or not(aRecordInfo^.Kind in tkRecordTypes) then raise EORMException.CreateUTF8('%.Create: Invalid type information for %',[self,aName]); inherited Create(aName,sftBlobCustom,aAttributes,aFieldWidth,aPropertyIndex, aPropertyPointer,aData2Text,aText2Data); fTypeInfo := aRecordInfo; end; procedure TSQLPropInfoRecordRTTI.GetBinary(Instance: TObject; W: TFileBufferWriter); var Value: RawByteString; begin Value := RecordSave(GetFieldAddr(Instance)^,fTypeInfo); W.Write(pointer(Value),length(Value)); end; function TSQLPropInfoRecordRTTI.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; var tmp: TSynTempBuffer; begin RecordSave(GetFieldAddr(Instance)^,tmp,fTypeInfo); result := crc32c(0,tmp.buf,tmp.len); tmp.Done; end; {$ifndef NOVARIANTS} procedure TSQLPropInfoRecordRTTI.GetVariant(Instance: TObject; var Dest: Variant); begin RawByteStringToVariant(RecordSave(GetFieldAddr(Instance)^,fTypeInfo),Dest); end; procedure TSQLPropInfoRecordRTTI.SetVariant(Instance: TObject; const Source: Variant); var tmp: RawByteString; begin VariantToRawByteString(Source,tmp); RecordLoad(GetFieldAddr(Instance)^,tmp,fTypeInfo); end; {$endif NOVARIANTS} procedure TSQLPropInfoRecordRTTI.NormalizeValue(var Value: RawUTF8); begin // a BLOB should already be normalized end; function TSQLPropInfoRecordRTTI.CompareValue(Item1, Item2: TObject; CaseInsensitive: boolean): PtrInt; begin if RecordEquals(GetFieldAddr(Item1)^,GetFieldAddr(Item2)^,fTypeInfo) then result := 0 else result := PtrInt(Item1)-PtrInt(Item2); // pseudo comparison end; function TSQLPropInfoRecordRTTI.SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; begin // use our RecordLoad() binary serialization result := RecordLoad(GetFieldAddr(Instance)^,P,fTypeInfo,nil,PEnd); end; procedure TSQLPropInfoRecordRTTI.SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); var data: RawByteString; begin TextToBinary(Value,data); RecordLoad(GetFieldAddr(Instance)^,data,fTypeInfo); end; procedure TSQLPropInfoRecordRTTI.GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); begin result := RecordSave(GetFieldAddr(Instance)^,fTypeInfo); BinaryToText(result,ToSQL,wasSQLString); end; function TSQLPropInfoRecordRTTI.SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; begin if aValue.VType=ftBlob then result := RecordLoad(GetFieldAddr(Instance)^,aValue.VBlob,fTypeInfo)<>nil else result := inherited SetFieldSQLVar(Instance,aValue); end; procedure TSQLPropInfoRecordRTTI.GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar; var temp: RawByteString); begin temp := RecordSave(GetFieldAddr(Instance)^,fTypeInfo); aValue.Options := []; aValue.VType := ftBlob; aValue.VBlob := pointer(temp); aValue.VBlobLen := length(temp); end; { TSQLPropInfoRecordFixedSize } procedure TSQLPropInfoRecordFixedSize.CopySameClassProp(Source: TObject; DestInfo: TSQLPropInfo; Dest: TObject); begin if TSQLPropInfoRecordFixedSize(DestInfo).fTypeInfo=fTypeInfo then MoveFast(GetFieldAddr(Source)^, TSQLPropInfoRecordFixedSize(DestInfo).GetFieldAddr(Dest)^,fRecordSize) else inherited CopySameClassProp(Source,DestInfo,Dest); end; function TSQLPropInfoRecordFixedSize.GetSQLFieldRTTITypeName: RawUTF8; begin if fTypeInfo=nil then result := inherited GetSQLFieldRTTITypeName else result := ToUTF8(fTypeInfo^.Name); end; constructor TSQLPropInfoRecordFixedSize.Create(aRecordSize: cardinal; const aName: RawUTF8; aPropertyIndex: integer; aPropertyPointer: pointer; aAttributes: TSQLPropInfoAttributes; aFieldWidth: integer; aData2Text: TOnSQLPropInfoRecord2Text; aText2Data: TOnSQLPropInfoRecord2Data); begin if integer(aRecordSize)<=0 then raise EORMException.CreateUTF8('%.Create: invalid % record size',[self,aRecordSize]); fRecordSize := aRecordSize; inherited Create(aName,sftBlobCustom,aAttributes,aFieldWidth,aPropertyIndex, aPropertyPointer,aData2Text,aText2Data); end; procedure TSQLPropInfoRecordFixedSize.GetBinary(Instance: TObject; W: TFileBufferWriter); begin W.Write(GetFieldAddr(Instance),fRecordSize); end; function TSQLPropInfoRecordFixedSize.GetHash(Instance: TObject; CaseInsensitive: boolean): cardinal; begin result := crc32c(0,GetFieldAddr(Instance),fRecordSize); end; procedure TSQLPropInfoRecordFixedSize.GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); begin FastSetString(result,GetFieldAddr(Instance),fRecordSize); BinaryToText(result,ToSQL,wasSQLString); end; {$ifndef NOVARIANTS} procedure TSQLPropInfoRecordFixedSize.GetVariant(Instance: TObject; var Dest: Variant); var tmp: RawByteString; begin SetString(tmp,PAnsiChar(GetFieldAddr(Instance)),fRecordSize); Dest := tmp; end; procedure TSQLPropInfoRecordFixedSize.SetVariant(Instance: TObject; const Source: Variant); begin if TVarData(Source).VType=varString then MoveFast(TVarData(Source).VAny^,GetFieldAddr(Instance)^,fRecordSize) else FillCharFast(GetFieldAddr(Instance)^,fRecordSize,0); end; {$endif NOVARIANTS} procedure TSQLPropInfoRecordFixedSize.NormalizeValue(var Value: RawUTF8); begin // a BLOB should already be normalized end; function TSQLPropInfoRecordFixedSize.CompareValue(Item1, Item2: TObject; CaseInsensitive: boolean): PtrInt; var i: Integer; P1,P2: PByteArray; begin if (Item1=Item2) or (fRecordSize=0) then result := 0 else if Item1=nil then result := -1 else if Item2=nil then result := 1 else begin result := 0; P1 := GetFieldAddr(Item1); P2 := GetFieldAddr(Item2); for i := 0 to fRecordSize-1 do begin result := P1^[i]-P2^[i]; if result<>0 then exit; end; end; end; function TSQLPropInfoRecordFixedSize.SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; begin result := P+fRecordSize; if result>PEnd then result := nil else MoveFast(P^,GetFieldAddr(Instance)^,fRecordSize); end; procedure TSQLPropInfoRecordFixedSize.SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); var data: RawByteString; begin TextToBinary(Value,data); Value := pointer(data); if Value=nil then FillCharFast(GetFieldAddr(Instance)^,fRecordSize,0) else MoveFast(Value^,GetFieldAddr(Instance)^,fRecordSize); end; function TSQLPropInfoRecordFixedSize.SetFieldSQLVar(Instance: TObject; const aValue: TSQLVar): boolean; begin if aValue.VType=ftBlob then begin result := aValue.VBlobLen=fRecordSize; if result then MoveFast(aValue.VBlob^,GetFieldAddr(Instance)^,fRecordSize) end else result := inherited SetFieldSQLVar(Instance,aValue); end; procedure TSQLPropInfoRecordFixedSize.GetFieldSQLVar(Instance: TObject; var aValue: TSQLVar; var temp: RawByteString); begin SetString(temp,PAnsiChar(GetFieldAddr(Instance)),fRecordSize); aValue.Options := []; aValue.VType := ftBlob; aValue.VBlob := pointer(temp); aValue.VBlobLen := length(temp); end; { TSQLPropInfoCustomJSON } constructor TSQLPropInfoCustomJSON.Create(aPropInfo: PPropInfo; aPropIndex: integer); var attrib: TSQLPropInfoAttributes; begin byte(attrib) := 0; if aPropInfo^.IsStored(nil)=AS_UNIQUE then Include(attrib,aIsUnique); // property MyProperty: RawUTF8 stored AS_UNIQUE;ieldWidth=10 Create(aPropInfo^.TypeInfo,ToUTF8(aPropInfo^.Name), aPropIndex,aPropInfo^.GetFieldAddr(nil),attrib,aPropInfo^.Index); end; constructor TSQLPropInfoCustomJSON.Create(aTypeInfo: PTypeInfo; const aName: RawUTF8; aPropertyIndex: integer; aPropertyPointer: pointer; aAttributes: TSQLPropInfoAttributes; aFieldWidth: integer); begin inherited Create(aName,sftUTF8Custom,aAttributes,aFieldWidth,aPropertyIndex, aPropertyPointer,nil,nil); fTypeInfo := aTypeInfo; SetCustomParser(TJSONCustomParserRTTI.CreateFromRTTI(aName,aTypeInfo,0)); end; constructor TSQLPropInfoCustomJSON.Create(const aTypeName, aName: RawUTF8; aPropertyIndex: integer; aPropertyPointer: pointer; aAttributes: TSQLPropInfoAttributes; aFieldWidth: integer); begin inherited Create(aName,sftUTF8Custom,aAttributes,aFieldWidth,aPropertyIndex, aPropertyPointer,nil,nil); SetCustomParser(TJSONCustomParserRTTI.CreateFromTypeName(aName,aTypeName)); end; function TSQLPropInfoCustomJSON.GetSQLFieldRTTITypeName: RawUTF8; begin if fTypeInfo=nil then result := inherited GetSQLFieldRTTITypeName else result := ToUTF8(fTypeInfo^.Name); end; procedure TSQLPropInfoCustomJSON.SetCustomParser( aCustomParser: TJSONCustomParserRTTI); begin if aCustomParser=nil then raise EORMException.CreateUTF8('%.SetCustomParser: Invalid type information for %', [self,Name]); fCustomParser := aCustomParser; end; destructor TSQLPropInfoCustomJSON.Destroy; begin inherited; fCustomParser.Free; end; procedure TSQLPropInfoCustomJSON.GetBinary(Instance: TObject; W: TFileBufferWriter); var JSON: RawUTF8; begin GetValueVar(Instance,false,JSON,nil); W.Write(JSON); end; function TSQLPropInfoCustomJSON.SetBinary(Instance: TObject; P,PEnd: PAnsiChar): PAnsiChar; var tmp: TSynTempBuffer; begin // stored as JSON VarString in the binary stream if FromVarString(PByte(P),PByte(PEnd),tmp) then try SetValue(Instance,tmp.buf,false); finally tmp.Done; end else P := nil; result := P; end; procedure TSQLPropInfoCustomJSON.NormalizeValue(var Value: RawUTF8); begin // do nothing: should already be normalized end; procedure TSQLPropInfoCustomJSON.GetJSONValues(Instance: TObject; W: TJSONSerializer); var Data: PByte; begin Data := GetFieldAddr(Instance); fCustomParser.WriteOneLevel(W,Data, [soReadIgnoreUnknownFields,soCustomVariantCopiedByReference]); end; procedure TSQLPropInfoCustomJSON.GetValueVar(Instance: TObject; ToSQL: boolean; var result: RawUTF8; wasSQLString: PBoolean); var W: TJSONSerializer; temp: TTextWriterStackBuffer; begin W := TJSONSerializer.CreateOwnedStream(temp); try GetJSONValues(Instance,W); W.SetText(result); if wasSQLString<>nil then wasSQLString^ := (result<>'') and (result[1]='"'); finally W.Free; end; end; procedure TSQLPropInfoCustomJSON.SetValue(Instance: TObject; Value: PUTF8Char; wasString: boolean); var Data: PByte; B: PUTF8Char; len: PtrInt; tmp: RawUTF8; begin Data := GetFieldAddr(Instance); if Value<>nil then begin // exact JSON string, array of objet ? B := GotoNextJSONObjectOrArray(Value); if (B=nil) and (Value^='"') then begin B := GotoEndOfJSONString(Value); if B^<>'"' then B := nil; end; len := StrLen(Value); if (B=nil) or (B-Value<>len) then begin QuotedStrJSON(Value,len,tmp); // need escaping as JSON string Value := pointer(tmp); end; end; fCustomParser.ReadOneLevel(Value,Data, [soReadIgnoreUnknownFields,soCustomVariantCopiedByReference],nil); end; { TSQLPropInfoList } constructor TSQLPropInfoList.Create(aTable: TClass; aOptions: TSQLPropInfoListOptions); begin fTable := aTable; fOptions := aOptions; if aTable.InheritsFrom(TSQLRecordRTreeAbstract) then include(fOptions,pilAuxiliaryFields); if pilSubClassesFlattening in fOptions then InternalAddParentsFirst(aTable,nil) else InternalAddParentsFirst(aTable); end; destructor TSQLPropInfoList.Destroy; var i: integer; begin for i := 0 to fCount-1 do fList[i].Free; inherited; end; procedure TSQLPropInfoList.InternalAddParentsFirst(aClassType: TClass; aFlattenedProps: PPropInfoDynArray); var P: PPropInfo; i,prev: Integer; begin if aClassType=nil then exit; // no RTTI information (e.g. reached TObject level) if not (pilSingleHierarchyLevel in fOptions) then InternalAddParentsFirst(GetClassParent(aClassType),aFlattenedProps); for i := 1 to InternalClassPropInfo(aClassType,P) do begin if (P^.PropType^.Kind=tkClass) and (P^.PropType^.ClassSQLFieldType in [sftObject,sftUnknown]) then begin prev := PtrArrayAdd(aFlattenedProps,P); InternalAddParentsFirst(P^.PropType^.ClassType^.ClassType,aFlattenedProps); SetLength(aFlattenedProps,prev); end else if not(pilIgnoreIfGetter in fOptions) or P^.GetterIsField then Add(TSQLPropInfoRTTI.CreateFrom(P,Count,fOptions,aFlattenedProps)); P := P^.Next; end; end; procedure TSQLPropInfoList.InternalAddParentsFirst(aClassType: TClass); var P: PPropInfo; i: Integer; begin if aClassType=nil then exit; // no RTTI information (e.g. reached TObject level) if not (pilSingleHierarchyLevel in fOptions) then InternalAddParentsFirst(GetClassParent(aClassType)); for i := 1 to InternalClassPropInfo(aClassType,P) do begin Add(TSQLPropInfoRTTI.CreateFrom(P,Count,fOptions,nil)); P := P^.Next; end; end; function TSQLPropInfoList.Add(aItem: TSQLPropInfo): integer; var f: integer; begin if aItem=nil then begin result := -1; exit; end; // check that this property is not an ID/RowID (handled separately) if IsRowID(pointer(aItem.Name)) and not (pilAllowIDFields in fOptions) then raise EModelException.CreateUTF8( '%.Add: % should not include a [%] published property',[self,fTable,aItem.Name]); // check that this property name is not already defined for f := 0 to fCount-1 do if IdemPropNameU(fList[f].Name,aItem.Name) then raise EModelException.CreateUTF8('%.Add: % has duplicated name [%]', [self,fTable,aItem.Name]); // add to the internal list result := fCount; if result>=length(fList) then SetLength(fList,NextGrow(result)); inc(fCount); fList[result] := aItem; fOrderedByName := nil; // force recompute sorted name array end; function TSQLPropInfoList.GetItem(aIndex: integer): TSQLPropInfo; begin if cardinal(aIndex)>=Cardinal(fCount) then EORMException.Create('Invalid TSQLPropInfoList index'); result := fList[aIndex]; end; procedure TSQLPropInfoList.QuickSortByName(L,R: PtrInt); var I,J,P,Tmp: PtrInt; pivot: PUTF8Char; begin if L0 do dec(J); if I <= J then begin Tmp := fOrderedByName[J]; fOrderedByName[J] := fOrderedByName[I]; fOrderedByName[I] := Tmp; if P=I then P := J else if P=J then P := I; inc(I); dec(J); end; until I>J; if J - L < R - I then begin // use recursion only for smaller range if L < J then QuickSortByName(L, J); L := I; end else begin if I < R then QuickSortByName(I, R); R := J; end; until L >= R; end; function TSQLPropInfoList.ByRawUTF8Name(const aName: RawUTF8): TSQLPropInfo; var i: integer; begin i := IndexByName(pointer(aName)); if i<0 then result := nil else result := fList[i]; end; function TSQLPropInfoList.ByName(aName: PUTF8Char): TSQLPropInfo; var i: integer; begin i := IndexByName(aName); if i<0 then result := nil else result := fList[i]; end; function TSQLPropInfoList.IndexByName(aName: PUTF8Char): integer; var cmp,L,R: integer; begin if (self<>nil) and (aName<>nil) and (fCount>0) then if fCount<5 then begin for result := 0 to fCount-1 do if StrIComp(pointer(fList[result].fName),aName)=0 then exit; end else begin if fOrderedByName=nil then begin SetLength(fOrderedByName,fCount); FillIncreasing(pointer(fOrderedByName),0,fCount); QuickSortByName(0,fCount-1); end; L := 0; R := fCount-1; repeat // fast O(log(n)) binary search result := (L+R)shr 1; cmp := StrIComp(pointer(fList[fOrderedByName[result]].fName),aName); if cmp=0 then begin result := fOrderedByName[result]; exit; end; if cmp<0 then L := result+1 else R := result-1; until L>R; end; result := -1; end; function TSQLPropInfoList.IndexByName(const aName: RawUTF8): integer; begin result := IndexByName(pointer(aName)); end; function TSQLPropInfoList.IndexByNameOrExcept(const aName: RawUTF8): integer; begin if IsRowID(pointer(aName)) then result := -1 else begin result := IndexByName(pointer(aName)); // fast O(log(n)) binary search if result<0 then raise EORMException.CreateUTF8( '%.IndexByNameOrExcept(%): unkwnown field in %',[self,aName,fTable]); end; end; procedure TSQLPropInfoList.IndexesByNamesOrExcept(const aNames: array of RawUTF8; const aIndexes: array of PInteger); var i: integer; begin if high(aNames)<>high(aIndexes) then raise EORMException.CreateUTF8('%.IndexesByNamesOrExcept(?)',[self]); for i := 0 to high(aNames) do if aIndexes[i]=nil then raise EORMException.CreateUTF8('%.IndexesByNamesOrExcept(aIndexes[%]=nil)',[self,aNames[i]]) else aIndexes[i]^ := IndexByNameOrExcept(aNames[i]); end; procedure TSQLPropInfoList.NamesToRawUTF8DynArray(var Names: TRawUTF8DynArray); var i: integer; begin SetLength(Names,Count); for i := 0 to Count-1 do Names[i] := fList[i].Name; end; function TSQLPropInfoList.IndexByNameUnflattenedOrExcept(const aName: RawUTF8): integer; begin if pilSubClassesFlattening in fOptions then begin for result := 0 to Count-1 do if IdemPropNameU(List[result].NameUnflattened,aName) then // O(n) iteration exit; end else begin result := IndexByName(pointer(aName)); // faster O(log(n)) binary search if result>=0 then exit; end; raise EORMException.CreateUTF8( '%.IndexByNameUnflattenedOrExcept(%): unkwnown field in %',[self,aName,fTable]); end; procedure StatusCodeToErrorMsgBasic(Code: integer; var result: RawUTF8); begin // only basic verbs here -> SynCrtSock.StatusCodeToReason() used instead case Code of HTTP_CONTINUE: result := 'Continue'; HTTP_SUCCESS: result := 'OK'; HTTP_CREATED: result := 'Created'; HTTP_NOCONTENT: result := 'No Content'; HTTP_MOVEDPERMANENTLY: result := 'Moved Permanently'; HTTP_NOTMODIFIED: result := 'Not Modified'; HTTP_BADREQUEST: result := 'Bad Request'; HTTP_UNAUTHORIZED: result := 'Unauthorized'; HTTP_FORBIDDEN: result := 'Forbidden'; HTTP_NOTFOUND: result := 'Not Found'; HTTP_NOTALLOWED: result := 'Method Not Allowed'; HTTP_NOTACCEPTABLE: result := 'Not Acceptable'; HTTP_TIMEOUT: result := 'Request Timeout'; HTTP_SERVERERROR: result := 'Internal Server Error'; HTTP_NOTIMPLEMENTED: result := 'Not Implemented'; HTTP_GATEWAYTIMEOUT: result := 'Gateway Timeout'; HTTP_UNAVAILABLE: result := 'Service Unavailable'; else if StatusCodeIsSuccess(Code) then result := 'Success' else result := 'Invalid Request'; end; end; function StatusCodeToErrorMsg(Code: integer): shortstring; var msg: RawUTF8; begin StatusCodeToErrorMessage(Code,msg); FormatShort('HTTP Error % - %',[Code,msg],result); end; function StatusCodeIsSuccess(Code: integer): boolean; begin result := (Code>=HTTP_SUCCESS) and (Codenil do begin ctp := GetClassParent(ClassType); for i := 1 to InternalClassPropInfo(ClassType,nfo) do begin k := MonitorPropUsageValue(nfo); if k<>smvUndefined then begin SetLength(Props,n+1); p := @Props[n]; p^.Info := nfo; p^.Kind := k; ShortStringToAnsi7String(nfo^.Name,p^.Name); if (ctp<>nil) and (FindPropName(['Bytes','MicroSec'],p^.Name)>=0) then ToText(ctp,p^.Name); // meaningful property name = parent name for g := low(p^.Values) to high(p^.Values) do SetLength(p^.Values[g],USAGE_VALUE_LEN[g]); p^.ValueLast := nfo^.GetInt64Value(Instance); inc(n); end; nfo := nfo^.Next; end; ClassType := ctp; end; end; var i,n: integer; instanceName: RawUTF8; begin result := -1; if Instance=nil then exit; // nothing to track if (Name='') and Instance.InheritsFrom(TSynMonitor) then instanceName := TSynMonitor(Instance).Name else instanceName := Name; if instanceName='' then ToText(Instance.ClassType,instanceName); fSafe.Lock; try n := length(fTracked); for i := 0 to n-1 do if fTracked[i].Instance=Instance then exit else if IdemPropNameU(fTracked[i].Name,instanceName) then raise ESynException.CreateUTF8('%.Track("%") name already exists',[self,instanceName]); SetLength(fTracked,n+1); fTracked[n].Instance := Instance; fTracked[n].Name := instanceName; ClassTrackProps(PPointer(Instance)^,fTracked[n].Props); if fTracked[n].Props=nil then // nothing to track SetLength(fTracked,n) else begin result := n; // returns the index of the added item if fPrevious.Value<>0 then LoadTrack(fTracked[n]); end; finally fSafe.UnLock; end; end; procedure TSynMonitorUsage.Track(const Instances: array of TSynMonitor); var i: integer; begin if self<>nil then for i := 0 to high(Instances) do Track(Instances[i],Instances[i].Name); end; function TSynMonitorUsage.TrackPropLock(Instance: TObject; Info: PPropInfo): PSynMonitorUsageTrackProp; var i,j: integer; begin fSafe.Lock; for i := 0 to length(fTracked)-1 do if fTracked[i].Instance=Instance then with fTracked[i] do begin for j := 0 to length(Props)-1 do if Props[j].Info=Info then begin result := @Props[j]; exit; // returned found entry locked end; break; end; fSafe.UnLock; result := nil; end; const // maps TTimeLogbits mask TL_MASK_SECONDS = pred(1 shl 6); TL_MASK_MINUTES = pred(1 shl 12); TL_MASK_HOURS = pred(1 shl 17); TL_MASK_DAYS = pred(1 shl 22); TL_MASK_MONTHS = pred(1 shl 26); // truncates a TTimeLogbits value to a granularity AS_MINUTES = not TL_MASK_SECONDS; AS_HOURS = not TL_MASK_MINUTES; AS_DAYS = not TL_MASK_HOURS; AS_MONTHS = not TL_MASK_DAYS; AS_YEARS = not TL_MASK_MONTHS; function TSynMonitorUsage.Modified(Instance: TObject): integer; begin if self<>nil then result := Modified(Instance,[]) else result := 0; end; procedure TSynMonitorUsage.SetCurrentUTCTime(out minutes: TTimeLogBits); begin minutes.FromUTCTime; end; function TSynMonitorUsage.Modified(Instance: TObject; const PropNames: array of RawUTF8; ModificationTime: TTimeLog): integer; procedure save(const track: TSynMonitorUsageTrack); function scope({$ifndef CPU64}var{$endif} prev,current: Int64): TSynMonitorUsageGranularity; begin if prev and AS_YEARS<>current and AS_YEARS then result := mugYear else if prev and AS_MONTHS<>current and AS_MONTHS then result := mugMonth else if prev and AS_DAYS<>current and AS_DAYS then result := mugDay else if prev and AS_HOURS<>current and AS_HOURS then result := mugHour else if prev<>current then result := mugMinute else result := mugUndefined; end; var j,k,min: integer; time: TTimeLogBits; v,diff: Int64; begin if ModificationTime=0 then SetCurrentUTCTime(time) else time.Value := ModificationTime; time.Value := time.Value and AS_MINUTES; // save every minute if fPrevious.Value<>time.Value then begin if fPrevious.Value=0 then // startup? Load(time) else SavePrevious(scope(fPrevious.Value,time.Value)); fPrevious.Value := time.Value; end; min := time.Minute; for j := 0 to length(track.Props)-1 do with track.Props[j] do if (high(PropNames)<0) or (FindPropName(PropNames,Name)>=0) then begin v := Info^.GetInt64Value(Instance); diff := v-ValueLast; if diff<>0 then begin inc(result); ValueLast := v; if Kind in SYNMONITORVALUE_CUMULATIVE then begin inc(Values[mugHour][min],diff); inc(Values[mugDay][time.Hour],diff); // propagate inc(Values[mugMonth][time.Day-1],diff); inc(Values[mugYear][time.Month-1],diff); end else for k := min to 59 do // make instant values continuous Values[mugHour][k] := v; end; end; end; var i: integer; begin result := 0; if Instance=nil then exit; fSafe.Lock; try for i := 0 to length(fTracked)-1 do if fTracked[i].Instance=Instance then begin save(fTracked[i]); exit; end; if Instance.InheritsFrom(TSynMonitor) and (TSynMonitor(Instance).Name<>'') then begin i := Track(Instance,TSynMonitor(Instance).Name); if i>=0 then save(fTracked[i]); exit; end; finally fSafe.UnLock; end; end; destructor TSynMonitorUsage.Destroy; begin SavePrevious(mugUndefined); // save pending values for all granularities inherited Destroy; end; procedure TSynMonitorUsage.SavePrevious(Scope: TSynMonitorUsageGranularity); var g: TSynMonitorUsageGranularity; id: TSynMonitorUsageID; begin id.FromTimeLog(fPrevious.Value); Save(id,mugHour,Scope); // always save current minutes values for g := mugDay to mugYear do if (Scope<>mugUndefined) and (g>Scope) then break else // mugUndefined from Destroy Save(id,g,Scope); end; procedure TSynMonitorUsage.Save(ID: TSynMonitorUsageID; Gran,Scope: TSynMonitorUsageGranularity); var t,n,p: Integer; track: PSynMonitorUsageTrack; data,val: TDocVariantData; begin if Gran fTracked[].Props[].Values[] for g := low(fValues) to high(fValues) do with _Safe(fValues[g])^ do begin val := GetAsDocVariantSafe(Track.Name); if val<>nil then for p := 0 to length(Track.Props)-1 do with Track.Props[p] do if val^.GetAsDocVariant(Name,int) and (int^.Count>0) and (dvoIsArray in int^.Options) then begin for v := 0 to length(Values[g])-1 do if vUSAGE_ID_MAX[mugHour] then result := 0; // stored fake USAGE_ID_HOURMARKER[mugDay..mugYear] value end; end; end; function TSynMonitorUsageID.Granularity: TSynMonitorUsageGranularity; var h: integer; begin h := Value and USAGE_ID_MASK[mugHour]; if cardinal(h)>USAGE_ID_MAX[mugHour] then begin for result := mugDay to mugYear do if USAGE_ID_HOURMARKER[result]=h then exit; result := mugUndefined; // should not happen end else result := mugHour; end; procedure TSynMonitorUsageID.Truncate(gran: TSynMonitorUsageGranularity); begin if gran>mugHour then Value := (Value and not USAGE_ID_MASK[mugHour]) or USAGE_ID_HOURMARKER[gran]; end; procedure TSynMonitorUsageID.SetTime(gran: TSynMonitorUsageGranularity; aValue: integer); begin case gran of mugYear: dec(aValue,USAGE_ID_YEAROFFSET); mugDay, mugMonth: dec(aValue); mugHour: ; else raise ERangeError.CreateFmt('SetValue(%s)',[ToText(gran)^]); end; if cardinal(aValue)>USAGE_ID_MAX[gran] then raise ERangeError.CreateFmt('%s should be 0..%d',[ToText(gran)^,USAGE_ID_MAX[gran]]); Value := (Value and not (USAGE_ID_MASK[gran] shl USAGE_ID_SHIFT[gran])) or (aValue shl USAGE_ID_SHIFT[gran]); end; function TSynMonitorUsageID.Text(Expanded: boolean; FirstTimeChar: AnsiChar): RawUTF8; var bits: TTimeLogBits; begin bits.Value := ToTimeLog; result := bits.Text(Expanded,FirstTimeChar); end; function TSynMonitorUsageID.ToTimeLog: TTimeLog; begin PTimeLogBits(@result)^.From( GetTime(mugYear),GetTime(mugMonth),GetTime(mugDay),GetTime(mugHour),0,0); end; { ************ main ORM / SOA classes and types } { TSQLTable } function TSQLTable.FieldIndex(FieldName: PUTF8Char): integer; begin if (self<>nil) and (fResults<>nil) and (FieldName<>nil) and (FieldCount>0) then if IsRowID(FieldName) then begin // will work for both 'ID' or 'RowID' result := fFieldIndexID; exit; end else if FieldCount<4 then begin for result := 0 to FieldCount-1 do if StrIComp(fResults[result],FieldName)=0 then exit; end else begin if fFieldNameOrder=nil then QuickSortIndexedPUTF8Char(fResults,FieldCount,fFieldNameOrder); result := FastFindIndexedPUTF8Char(fResults,FieldCount-1,fFieldNameOrder, FieldName,@StrIComp); exit; end; result := -1; end; function TSQLTable.FieldIndex(const FieldName: RawUTF8): integer; begin result := FieldIndex(Pointer(FieldName)); end; function TSQLTable.FieldIndexExisting(const FieldName: RawUTF8): integer; begin result := FieldIndex(Pointer(FieldName)); if result<0 then raise ESQLTableException.CreateUTF8('%.FieldIndexExisting("%")',[self,FieldName]); end; function TSQLTable.FieldIndex(const FieldNames: array of RawUTF8; const FieldIndexes: array of PInteger): integer; var i: PtrInt; begin result := 0; if high(FieldNames)<0 then exit; if high(FieldNames)<>high(FieldIndexes) then raise ESQLTableException.CreateUTF8('%.FieldIndex() argument count',[self]); for i := 0 to high(FieldNames) do if FieldIndexes[i]=nil then raise ESQLTableException.CreateUTF8( '%.FieldIndex() FieldIndexes["%"]=nil',[self,FieldNames[i]]) else begin FieldIndexes[i]^ := FieldIndex(pointer(FieldNames[i])); if FieldIndexes[i]^>=0 then inc(result); end; end; procedure TSQLTable.FieldIndexExisting(const FieldNames: array of RawUTF8; const FieldIndexes: array of PInteger); var i: PtrInt; begin if high(FieldNames)<0 then exit; if high(FieldNames)<>high(FieldIndexes) then raise ESQLTableException.CreateUTF8('%.FieldIndexExisting() argument count',[self]); for i := 0 to high(FieldNames) do if FieldIndexes[i]=nil then raise ESQLTableException.CreateUTF8( '%.FieldIndexExisting() FieldIndexes["%"]=nil',[self,FieldNames[i]]) else FieldIndexes[i]^ := FieldIndexExisting(FieldNames[i]); end; function TSQLTable.FieldNames: TRawUTF8DynArray; begin if length(fFieldNames)<>fFieldCount then InitFieldNames; result := fFieldNames; end; function TSQLTable.FieldValue(const FieldName: RawUTF8; Row: integer): PUTF8Char; var Index: integer; begin Index := FieldIndex(pointer(FieldName)); if (Index<0) or (cardinal(Row-1)>=cardinal(fRowCount)) then result := nil else result := fResults[Index+Row*FieldCount]; end; procedure TSQLTable.SortBitsFirst(var Bits); var oldIDColumn, oldResults: TPUTF8CharDynArray; i, j, nSet, n: integer; R: PPUTF8Char; begin if fIDColumn<>nil then begin n := length(fIDColumn); SetLength(oldIDColumn,n); MoveFast(fIDColumn[0],oldIDColumn[0],n*SizeOf(PUTF8Char)); end; i := (fRowCount+1)*FieldCount; SetLength(oldResults,i); MoveFast(fResults[0],oldResults[0],i*SizeOf(PUTF8Char)); // put marked IDs first n := 1; // copy row data (first row=0 i.e. idents is left as it is) R := @fResults[FieldCount]; j := FieldCount; for i := 1 to fRowCount do begin if GetBitPtr(@Bits,i-1) then begin if fIDColumn<>nil then fIDColumn[n] := oldIDColumn[i]; MoveFast(oldResults[j],R^,FieldCount*SizeOf(PUTF8Char)); inc(n); inc(R,FieldCount); end; inc(j,FieldCount); end; nSet := n-1; // put unmarked IDs j := FieldCount; for i := 1 to fRowCount do begin if not GetBitPtr(@Bits,i-1) then begin if fIDColumn<>nil then fIDColumn[n] := oldIDColumn[i]; MoveFast(oldResults[j],R^,FieldCount*SizeOf(PUTF8Char)); inc(n); inc(R,FieldCount); end; inc(j,FieldCount); end; assert(n-1=fRowCount); // recalcultate Bits[] FillCharFast(Bits,(fRowCount shr 3)+1,0); for i := 0 to nSet-1 do SetBitPtr(@Bits,i); // slow but accurate end; function TSQLTable.IDColumnHide: boolean; var FID,R,F: integer; S,D1,D2: PPUTF8Char; begin // 1. check if possible result := false; if (self=nil) or Assigned(fIDColumn) or (FieldCount<=1) then exit; // already hidden or not possible FID := fFieldIndexID; if FID<0 then exit; // no 'ID' field // 2. alloc new arrays of PUTF8Char dec(fFieldCount); R := fRowCount+1; SetLength(fIDColumn,R); // will contain the ID column data SetLength(fNotIDColumn,R*FieldCount); // will be the new fResults[] // 3. copy fResults[] into new arrays S := @fResults[0]; D1 := @fNotIDColumn[0]; D2 := @fIDColumn[0]; for R := 0 to fRowCount do for F := 0 to FieldCount do begin // we have FieldCount := FieldCount-1 if F<>FID then begin D1^ := S^; // copy not ID column into fNotIDColumn[] inc(D1); end else begin D2^ := S^; // copy ID column into fIDColumn[] inc(D2); end; inc(S); end; // 4. TSQLTable data now points to new values without ID field result := true; fResults := @fNotIDColumn[0]; end; function TSQLTable.IDColumnHiddenValue(Row: integer): TID; begin if (self=nil) or (fResults=nil) or (Row<=0) or (Row>fRowCount) then result := 0 else if Assigned(fIDColumn) then // get hidden ID column UTF-8 content SetID(fIDColumn[Row],result) else if fFieldIndexID>=0 then // get ID column field index SetID(fResults[Row*FieldCount+fFieldIndexID],result) else result := 0; end; procedure TSQLTable.IDArrayFromBits(const Bits; var IDs: TIDDynArray); var n, i, FID: integer; begin if not Assigned(fIDColumn) then begin FID := fFieldIndexID; // get ID column field index if FID<0 then exit; end else FID := 0; // make compiler happy n := GetBitsCount(Bits,fRowCount); if n=fRowCount then begin IDColumnHiddenValues(IDs); // all selected -> direct get all IDs exit; end; SetLength(IDs,n); if n=0 then exit; n := 0; if Assigned(fIDColumn) then begin for i := 1 to fRowCount do if GetBitPtr(@Bits,i-1) then begin IDs[n] := GetInt64(fIDColumn[i]); // get hidden ID column UTF-8 content inc(n); end; end else begin inc(FID,FieldCount); // [i*FieldCount+FID] = [(i+1)*FieldCount+FID] below for i := 0 to fRowCount-1 do if GetBitPtr(@Bits,i) then begin IDs[n] := GetInt64(fResults[i*FieldCount+FID]); // get ID column UTF-8 content inc(n); end; end; end; procedure TSQLTable.IDColumnHiddenValues(var IDs: TIDDynArray); var n, i, FID: integer; U: PPUTF8Char; begin n := fRowCount; if not Assigned(fIDColumn) then begin FID := fFieldIndexID; // get ID column field index if FID<0 then n := 0; end else FID := 0; SetLength(IDs,n); if n=0 then exit; if Assigned(fIDColumn) then begin for i := 1 to fRowCount do IDs[i-1] := GetInt64(fIDColumn[i]); // get hidden ID column UTF-8 content end else begin U := @fResults[FID+FieldCount]; // U^ = ID column UTF-8 content for i := 0 to fRowCount-1 do begin IDs[i] := GetInt64(U^); inc(U,FieldCount); end; end; end; procedure TSQLTable.IDArrayToBits(var Bits; var IDs: TIDDynArray); var i,FID: integer; U: PPUTF8Char; ID: Pointer; IDmax: integer; // AllID: : TIDDynArray; begin if length(IDs)=fRowCount then begin // all selected -> all bits set to 1 FillCharFast(Bits,(fRowCount shr 3)+1,255); exit; end; FillCharFast(Bits,(fRowCount shr 3)+1,0); if IDs=nil then exit; // no selected -> all bits left to 0 // we sort IDs to use FastFindInt64Sorted() and its O(log(n)) binary search ID := @IDs[0]; IDmax := length(IDs)-1; QuickSortInt64(ID,0,IDmax); if not Assigned(fIDColumn) then begin FID := fFieldIndexID; // get ID column field index if FID<0 then exit; // no ID column -> unable to get bit index end else FID := 0; // make compiler happy if Assigned(fIDColumn) then begin for i := 1 to fRowCount do if FastFindInt64Sorted(ID,IDmax,GetInt64(fIDColumn[i]))>=0 then SetBitPtr(@Bits,i-1); end else begin U := @fResults[FID+FieldCount]; // U^ = ID column UTF-8 content for i := 0 to fRowCount-1 do begin if FastFindInt64Sorted(ID,IDmax,GetInt64(U^))>=0 then SetBitPtr(@Bits,i); inc(U,FieldCount); end; end; { // debugg: IDArrayFromBits(Bits,AllID); assert(length(AllID)=length(IDs)); QuickSortInteger(@AllID[0],0,high(AllID)); QuickSortInteger(@IDs[0],0,high(IDs)); assert(comparemem(@AllID[0],@IDs[0],length(AllID)*SizeOf(TID))); } end; function TSQLTable.RowFromID(aID: TID; aNotFoundMinusOne: boolean): integer; var ID: RawUTF8; FID: integer; U: PPUTF8Char; begin if self=nil then begin result := -1; exit; end; if (fResults<>nil) and (aID>0) then begin // search aID as UTF-8 in fIDColumn[] or fResults[] Int64ToUtf8(aID,ID); if Assigned(fIDColumn) then begin // get hidden ID column UTF-8 content for result := 1 to fRowCount do if StrComp(fIDColumn[result],pointer(ID))=0 then exit; end else begin FID := fFieldIndexID; // get ID column field index if FID>=0 then begin U := @fResults[FID+FieldCount]; // U^ = ID column UTF-8 content for result := 1 to fRowCount do if StrComp(U^,pointer(ID))=0 then exit else inc(U,FieldCount); end; end; end; if aNotFoundMinusOne then result := -1 else result := fRowCount; // not found -> return last row index end; function TSQLTable.DeleteRow(Row: integer): boolean; begin if (self=nil) or (Row<1) or (Row>fRowCount) then begin result := false; exit; // out of range end; if Assigned(fIDColumn) then if RowfRowCount) or (cardinal(field)>=cardinal(fFieldCount)) then exit; // out of range if fFieldType=nil then InitFieldTypes; V := fResults[row*fFieldCount+field]; with fFieldType[field] do if expandHugeIDAsUniqueIdentifier and (field=fFieldIndexID) then begin SetInt64(V,PInt64(@id)^); if id.CreateTimeUnix>JAN2015_UNIX then id.ToVariant(value) else value := id.Value; end else begin if expandEnumsAsText and (ContentType=sftEnumerate) then begin enum := GetInteger(V,err); if (err=0) and (ContentTypeInfo<>nil) then begin value := PEnumType(ContentTypeInfo)^.GetEnumNameOrd(enum)^; exit; end; end else if expandTimeLogAsText then case ContentType of sftTimeLog,sftModTime,sftCreateTime,sftUnixTime: begin SetInt64(V,t.Value); if t.Value=0 then value := 0 else begin if ContentType=sftUnixTime then t.FromUnixTime(t.Value); TDocVariantData(value).InitObject(['Time',t.Text(true),'Value',t.Value],JSON_OPTIONS_FAST); end; exit; end; sftUnixMSTime: begin // no TTimeLog use for milliseconds resolution SetInt64(V,t.Value); if t.Value=0 then value := 0 else TDocVariantData(value).InitObject(['Time',UnixMSTimeToString(t.Value),'Value',t.Value],JSON_OPTIONS_FAST); exit; end; end; ValueVarToVariant(V,StrLen(V),ContentType,TVarData(value),true,ContentTypeInfo,options); end; end; procedure TSQLTable.ToDocVariant(Row: integer; out doc: variant; options: TDocVariantOptions; expandTimeLogAsText,expandEnumsAsText, expandHugeIDAsUniqueIdentifier: boolean); var f: integer; v: TVariantDynArray; begin if (self=nil) or (Row<1) or (Row>fRowCount) then exit; // out of range SetLength(v,fFieldCount); for f := 0 to fFieldCount-1 do GetAsVariant(Row,f,v[f],expandTimeLogAsText,expandEnumsAsText, expandHugeIDAsUniqueIdentifier,options); if length(fFieldNames)<>fFieldCount then InitFieldNames; TDocVariantData(doc).InitObjectFromVariants(fFieldNames,v,JSON_OPTIONS_FAST); end; procedure TSQLTable.ToDocVariant(out docs: TVariantDynArray; readonly: boolean); var r: integer; begin if (self=nil) or (fRowCount=0) then exit; SetLength(docs,fRowCount); if readonly then begin if SQLTableRowVariantType=nil then SQLTableRowVariantType := SynRegisterCustomVariantType(TSQLTableRowVariant); for r := 0 to fRowCount-1 do with TSQLTableRowVariantData(docs[r]) do begin VType := SQLTableRowVariantType.VarType; VTable := self; VRow := r+1; end; end else for r := 0 to fRowCount-1 do ToDocVariant(r+1,docs[r]); end; procedure TSQLTable.ToDocVariant(out docarray: variant; readonly: boolean); var Values: TVariantDynArray; begin ToDocVariant(Values,readonly); TDocVariantData(docarray).InitArrayFromVariants(Values,JSON_OPTIONS_FAST); end; {$endif NOVARIANTS} function TSQLTable.DeleteColumnValues(Field: integer): boolean; var i: integer; U: PPUTF8Char; begin if cardinal(Field)>=cardinal(FieldCount) then begin result := false; exit; // out of range end; U := @fResults[Field+FieldCount]; // U^ = column UTF-8 content for this field for i := 1 to fRowCount do begin U^ := nil; // just void UTF-8 content text inc(U,FieldCount); end; result := true; end; function TSQLTable.GetQueryTableNameFromSQL: RawUTF8; begin if (fQueryTableNameFromSQL='') and (fQuerySQL<>'') then fQueryTableNameFromSQL := GetTableNameFromSQLSelect(fQuerySQL,true); result := fQueryTableNameFromSQL; end; function TSQLTable.FieldPropFromTables(const PropName: RawUTF8; out PropInfo: TSQLPropInfo; out TableIndex: integer): TSQLFieldType; procedure SearchInQueryTables(aPropName: PUTF8Char; aTableIndex: integer); begin if IsRowID(aPropName) then begin result := sftInteger; PropInfo := nil; TableIndex := aTableIndex; exit; end else if fQueryTables[aTableIndex]<>nil then begin PropInfo := fQueryTables[aTableIndex].RecordProps.Fields.ByName(aPropName); if PropInfo<>nil then begin result := PropInfo.SQLFieldTypeStored; if result<>sftUnknown then TableIndex := aTableIndex; exit; end; result := sftUnknown; end; end; var i,t: integer; begin TableIndex := -1; result := sftUnknown; if fQueryTableIndexFromSQL=-2 then begin fQueryTableIndexFromSQL := -1; if (fQueryTables<>nil) and (QueryTableNameFromSQL<>'') then for i := 0 to length(fQueryTables)-1 do if IdemPropNameU(fQueryTables[i].SQLTableName,fQueryTableNameFromSQL) then begin fQueryTableIndexFromSQL := i; break; end; end; if fQueryTableIndexFromSQL>=0 then begin SearchInQueryTables(pointer(PropName),fQueryTableIndexFromSQL); if result<>sftUnknown then exit; end; if length(fQueryTables)=1 then SearchInQueryTables(pointer(PropName),0) else begin i := PosExChar('.',PropName)-1; if i<0 then // no 'ClassName.PropertyName' format: find first exact property name for t := 0 to high(fQueryTables) do begin SearchInQueryTables(pointer(PropName),t); if result<>sftUnknown then exit; end else // handle property names as 'ClassName.PropertyName' for t := 0 to high(fQueryTables) do if fQueryTables[t]<>nil then // avoid GPF if IdemPropNameU(fQueryTables[t].RecordProps.SQLTableName,pointer(PropName),i) then begin SearchInQueryTables(@PropName[i+2],t); exit; end; end; end; procedure TSQLTable.SetFieldType(Field: integer; FieldType: TSQLFieldType; FieldTypeInfo: pointer; FieldSize,FieldTableIndex: integer); begin if (self=nil) or (cardinal(Field)>=cardinal(FieldCount)) then exit; if fFieldType=nil then InitFieldTypes else if Field>=length(fFieldType) then SetLength(fFieldType,Field+1); // e.g. from TSQLTableWritable.AddField with fFieldType[Field] do begin ContentType := FieldType; ContentSize := FieldSize; ContentTypeInfo := nil; if FieldTypeInfo<>nil then case FieldType of sftEnumerate: if (PTypeInfo(FieldTypeInfo)^.Kind=tkEnumeration) then ContentTypeInfo := PTypeInfo(FieldTypeInfo)^.EnumBaseType; sftSet: if (PTypeInfo(FieldTypeInfo)^.Kind=tkSet) then ContentTypeInfo := PTypeInfo(FieldTypeInfo)^.SetEnumType; sftBlobDynArray: ContentTypeInfo := FieldTypeInfo; {$ifndef NOVARIANTS} sftNullable: begin ContentTypeInfo := FieldTypeInfo; ContentType := NullableTypeToSQLFieldType(FieldTypeInfo); if ContentType=sftUnknown then ContentType := sftNullable; end; {$endif NOVARIANTS} end; {$ifndef NOVARIANTS} if ContentType in [sftVariant,sftNullable] then ContentDB := ftUnknown else // ftUTF8/ftNull are not precise enough {$endif NOVARIANTS} ContentDB := SQLFIELDTYPETODBFIELDTYPE[ContentType]; TableIndex := FieldTableIndex; end; end; procedure TSQLTable.SetFieldType(const FieldName: RawUTF8; FieldType: TSQLFieldType; FieldTypeInfo: pointer; FieldSize: integer); begin SetFieldType(FieldIndex(FieldName),FieldType,FieldTypeInfo,FieldSize); end; const DBTOFIELDTYPE: array[TSQLDBFieldType] of TSQLFieldType = (sftUnknown, sftUnknown,sftInteger,sftFloat,sftCurrency,sftDateTime,sftUTF8Text,sftBlob); procedure TSQLTable.SetFieldTypes(const DBTypes: TSQLDBFieldTypeDynArray); var f: integer; begin if length(DBTypes)<>FieldCount then raise ESQLTableException.CreateUTF8('%.SetFieldTypes(DBTypes?)',[self]); for f := 0 to FieldCount-1 do SetFieldType(f,DBTOFIELDTYPE[DBTypes[f]]); end; function TSQLTable.GetRowCount: integer; begin if self=nil then result := 0 else result := fRowCount; end; procedure TSQLTable.InitFieldTypes; var f,i,len: integer; sft: TSQLFieldType; info: pointer; prop: TSQLPropInfo; size,tableindex: integer; U: PPUTF8Char; guessed: boolean; tlog: TTimeLog; begin if Assigned(fQueryColumnTypes) and (FieldCount<>length(fQueryColumnTypes)) then raise ESQLTableException.CreateUTF8('%.CreateWithColumnTypes() called with % '+ 'column types, whereas the result has % columns', [self,length(fQueryColumnTypes),FieldCount]); SetLength(fFieldType,FieldCount); for f := 0 to FieldCount-1 do begin prop := nil; info := nil; size := -1; tableindex := -1; guessed := false; // init fFieldType[] from fQueryTables/fQueryColumnTypes[] if Assigned(fQueryColumnTypes) then sft := fQueryColumnTypes[f] else if Assigned(QueryTables) then begin // retrieve column info from field name sft := FieldPropFromTables(fResults[f],prop,tableindex); if prop<>nil then begin if prop.InheritsFrom(TSQLPropInfoRTTI) then info := TSQLPropInfoRTTI(prop).PropType; size := prop.FieldWidth; end; end else sft := sftUnknown; if sft=sftUnknown then // not found in fQueryTables/fQueryColumnTypes[]: guess from content if IsRowID(fResults[f]) then sft := sftInteger else begin guessed := true; if f in fFieldParsedAsString then begin // the parser identified string values -> check if was sftDateTime sft := sftUTF8Text; U := @fResults[FieldCount+f]; for i := 1 to fRowCount do if U^=nil then // search for a non void column inc(U,FieldCount) else begin len := StrLen(U^); tlog := Iso8601ToTimeLogPUTF8Char(U^,len); if tlog<>0 then if (len in [8,10]) and (cardinal(tlog shr 26)-1800<300) then sft := sftDateTime else // e.g. YYYYMMDD date (Y=1800..2100) if len>=15 then sft := sftDateTime; // e.g. YYYYMMDDThhmmss date/time value break; end; end else begin U := @fResults[FieldCount+f]; for i := 1 to fRowCount do begin sft := UTF8ContentNumberType(U^); inc(U,FieldCount); if sft=sftUnknown then continue else // null -> search for a non void column if sft=sftInteger then // may be a floating point with no decimal if FieldTypeIntegerDetectionOnAllRows then continue else // we only checked the first field -> best guess... sft := sftCurrency; break; // found a non-integer content (e.g. sftFloat/sftUtf8Text) end; end; end; SetFieldType(f,sft,info,size,tableindex); if guessed then fFieldType[f].ContentDB := ftUnknown; // may fail on some later row end; end; function TSQLTable.FieldType(Field: integer): TSQLFieldType; begin if (self<>nil) and (cardinal(Field)nil) and (cardinal(Field)cardinal(fRowCount)) or (cardinal(Field)>=cardinal(FieldCount)) then // cardinal() -> test <0 result := nil else result := fResults[Row*FieldCount+Field]; end; function TSQLTable.GetU(Row,Field: integer): RawUTF8; var P: PUTF8Char; begin if (self=nil) or (fResults=nil) or (cardinal(Row)>cardinal(fRowCount)) or (cardinal(Field)>=cardinal(FieldCount)) then // cardinal() -> test <0 result := '' else begin P := fResults[Row*FieldCount+Field]; FastSetString(Result,P,StrLen(P)); end; end; function TSQLTable.Get(Row: integer; const FieldName: RawUTF8): PUTF8Char; begin result := Get(Row,FieldIndex(FieldName)); end; function TSQLTable.GetU(Row: integer; const FieldName: RawUTF8): RawUTF8; begin result := GetU(Row,FieldIndex(FieldName)); end; function TSQLTable.GetA(Row, Field: integer): WinAnsiString; begin result := Utf8ToWinAnsi(Get(Row,Field)); end; function TSQLTable.GetAsInteger(Row, Field: integer): integer; begin result := GetInteger(Get(Row,Field)); end; function TSQLTable.GetAsInteger(Row: integer; const FieldName: RawUTF8): integer; begin result := GetInteger(Get(Row,FieldIndex(FieldName))); end; function TSQLTable.GetAsInt64(Row, Field: integer): Int64; begin SetInt64(Get(Row,Field),result); end; function TSQLTable.GetAsInt64(Row: integer; const FieldName: RawUTF8): Int64; begin SetInt64(Get(Row,FieldIndex(FieldName)),result); end; function TSQLTable.GetAsFloat(Row,Field: integer): TSynExtended; begin result := GetExtended(Get(Row,Field)); end; function TSQLTable.GetAsFloat(Row: integer; const FieldName: RawUTF8): TSynExtended; begin result := GetExtended(Get(Row,FieldIndex(FieldName))); end; function TSQLTable.GetAsCurrency(Row,Field: integer): currency; begin result := StrToCurrency(Get(Row,Field)); end; function TSQLTable.GetAsCurrency(Row: integer; const FieldName: RawUTF8): currency; begin result := StrToCurrency(Get(Row,FieldIndex(FieldName))); end; function TSQLTable.GetAsDateTime(Row,Field: integer): TDateTime; var P: PUTF8Char; begin result := 0; if Row=0 then exit; // header P := Get(Row,Field); if P=nil then exit; case FieldType(Field) of sftCurrency,sftFloat: result := GetExtended(P); sftInteger, // TSQLTable.InitFieldTypes may have recognized an integer sftTimeLog, sftModTime, sftCreateTime: result := TimeLogToDateTime(GetInt64(P)); sftUnixTime: result := UnixTimeToDateTime(GetInt64(P)); sftUnixMSTime: result := UnixMSTimeToDateTime(GetInt64(P)); else // sftDateTime and any other kind will try from ISO-8601 text result := Iso8601ToDateTimePUTF8Char(P); end; end; function TSQLTable.GetAsDateTime(Row: integer; const FieldName: RawUTF8): TDateTime; begin result := GetAsDateTime(Row,FieldIndex(FieldName)); end; function TSQLTable.GetS(Row, Field: integer): shortstring; begin UTF8ToShortString(result,Get(Row,Field)); end; function TSQLTable.GetString(Row, Field: integer): string; var U: PUTF8Char; begin U := Get(Row,Field); if U=nil then result := '' else {$ifdef UNICODE} UTF8DecodeToUnicodeString(U,StrLen(U),result); {$else} CurrentAnsiConvert.UTF8BufferToAnsi(U,StrLen(U),RawByteString(result)); {$endif} end; function TSQLTable.GetSynUnicode(Row,Field: integer): SynUnicode; var U: PUTF8Char; begin result := ''; U := Get(Row,Field); if U<>nil then UTF8ToSynUnicode(U,StrLen(U),result); end; function TSQLTable.GetCaption(Row, Field: integer): string; begin GetCaptionFromPCharLen(Get(Row,Field),result); end; function BlobToTSQLRawBlob(P: PUTF8Char): TSQLRawBlob; begin BlobToTSQLRawBlob(P,result); end; procedure BlobToTSQLRawBlob(P: PUTF8Char; var result: TSQLRawBlob); var Len, LenHex: integer; begin result := ''; Len := StrLen(P); if Len=0 then exit; if Len>=3 then if (P[0] in ['x','X']) and (P[1]='''') and (P[Len-1]='''') then begin // BLOB literals are string literals containing hexadecimal data and // preceded by a single "x" or "X" character. For example: X'53514C697465' LenHex := (Len-3) shr 1; SetLength(result,LenHex); if SynCommons.HexToBin(@P[2],pointer(result),LenHex) then exit; // valid hexa data end else if (PInteger(P)^ and $00ffffff=JSON_BASE64_MAGIC) and Base64ToBinSafe(@P[3],Len-3,RawByteString(result)) then exit; // safe decode Base-64 content ('\uFFF0base64encodedbinary') // TEXT format SetString(result,PAnsiChar(P),Len); end; function BlobToTSQLRawBlob(const Blob: RawByteString): TSQLRawBlob; var Len, LenHex: integer; P: PUTF8Char; begin result := ''; if Blob='' then exit; Len := length(Blob); P := pointer(Blob); if Len>=3 then if (P[0] in ['x','X']) and (P[1]='''') and (P[Len-1]='''') then begin // BLOB literals are string literals containing hexadecimal data and // preceded by a single "x" or "X" character. For example: X'53514C697465' LenHex := (Len-3) shr 1; SetLength(result,LenHex); if SynCommons.HexToBin(@P[2],pointer(result),LenHex) then exit; // valid hexa data end else if (PInteger(P)^ and $00ffffff=JSON_BASE64_MAGIC) and Base64ToBinSafe(@P[3],Len-3,RawByteString(result)) then exit; // safe decode Base-64 content ('\uFFF0base64encodedbinary') // TEXT format result := Blob; end; function BlobToStream(P: PUTF8Char): TStream; begin Result := TRawByteStringStream.Create(BlobToTSQLRawBlob(P)); end; function BlobToBytes(P: PUTF8Char): TBytes; var Len, LenResult: integer; begin result := nil; Len := StrLen(P); if Len=0 then exit; if Len>=3 then if (P[0] in ['x','X']) and (P[1]='''') and (P[Len-1]='''') then begin // BLOB literals format LenResult := (Len-3)shr 1; SetLength(Result,LenResult); if SynCommons.HexToBin(@P[2],pointer(Result),LenResult) then exit; // valid hexa data end else if (PInteger(P)^ and $00ffffff=JSON_BASE64_MAGIC) and IsBase64(@P[3],Len-3) then begin // Base-64 encoded content ('\uFFF0base64encodedbinary') inc(P,3); dec(Len,3); LenResult := Base64ToBinLength(pointer(P),len); SetLength(Result,LenResult); if LenResult>0 then Base64Decode(pointer(P),pointer(Result),Len shr 2); exit; end; // TEXT format SetLength(Result,Len); MoveFast(P^,pointer(Result)^,Len); end; function TSQLRawBlobToBlob(const RawBlob: TSQLRawBlob): RawUTF8; // BLOB literals are string literals containing hexadecimal data and // preceded by a single "x" or "X" character. For example: X'53514C697465' begin result := TSQLRawBlobToBlob(pointer(RawBlob),length(RawBlob)); end; function TSQLRawBlobToBlob(RawBlob: pointer; RawBlobLength: integer): RawUTF8; // BLOB literals are string literals containing hexadecimal data and // preceded by a single "x" or "X" character. For example: X'53514C697465' var P: PAnsiChar; begin result := ''; if RawBlobLength<>0 then begin SetLength(result,RawBlobLength*2+3); P := pointer(result); P[0] := 'X'; P[1] := ''''; BinToHex(RawBlob,P+2,RawBlobLength); P[RawBlobLength*2+2] := ''''; end; end; function isBlobHex(P: PUTF8Char): boolean; // BLOB literals are string literals containing hexadecimal data and // preceded by a single "x" or "X" character. For example: X'53514C697465' var Len: integer; begin if P=nil then begin result := false; exit; end; while (P^<=' ') and (P^<>#0) do inc(P); if (P[0] in ['x','X']) and (P[1]='''') then begin Len := (StrLen(P)-3) shr 1; result := (P[Len-1]='''') and SynCommons.HexToBin(@P[2],nil,Len); exit; end else begin result := false; exit; end; end; function TSQLTable.GetBlob(Row, Field: integer): TSQLRawBlob; begin result := BlobToTSQLRawBlob(Get(Row,Field)); end; function TSQLTable.GetBytes(Row,Field: integer): TBytes; begin result := BlobToBytes(Get(Row,Field)); end; function TSQLTable.GetStream(Row,Field: integer): TStream; begin result := BlobToStream(Get(Row,Field)); end; function TSQLTable.GetDateTime(Row, Field: integer): TDateTime; begin result := Iso8601ToDateTimePUTF8Char(Get(Row,Field),0) end; function TSQLTable.GetRowValues(Field: integer; out Values: TRawUTF8DynArray): integer; var i: integer; U: PPUTF8Char; begin result := 0; if (self=nil) or (cardinal(Field)>cardinal(FieldCount)) or (fRowCount=0) then exit; SetLength(Values,fRowCount); U := @fResults[FieldCount+Field]; // start reading after first Row (= Field Names) for i := 0 to fRowCount-1 do begin FastSetString(Values[i],U^,StrLen(U^)); inc(U,FieldCount); // go to next row end; result := fRowCount; end; function TSQLTable.GetRowValues(Field: integer; out Values: TInt64DynArray): integer; var i: integer; U: PPUTF8Char; begin result := 0; if (self=nil) or (cardinal(Field)>cardinal(FieldCount)) or (fRowCount=0) then exit; SetLength(Values,fRowCount); U := @fResults[FieldCount+Field]; // start reading after first Row (= Field Names) for i := 0 to fRowCount-1 do begin SetInt64(U^,Values[i]); inc(U,FieldCount); // go to next row end; result := fRowCount; end; function TSQLTable.GetRowLengths(Field: integer; var LenStore: TSynTempBuffer): integer; var len: PInteger; i: integer; U: PPUTF8Char; begin result := 0; if (self=nil) or (cardinal(Field)>cardinal(FieldCount)) or (fRowCount=0) then begin LenStore.buf := nil; // avoid GPF in LenStore.Done exit; end; U := @fResults[FieldCount+Field]; // start reading after first Row (= Field Names) len := LenStore.Init(fRowCount*SizeOf(len^)); for i := 1 to fRowCount do begin len^ := StrLen(U^); inc(result,len^); inc(len); inc(U,FieldCount); // go to next row end; end; function TSQLTable.GetRowValues(Field: integer; const Sep,Head,Trail: RawUTF8): RawUTF8; var i, L, SepLen: integer; U: PPUTF8Char; P: PUTF8Char; len: PInteger; tmp: TSynTempBuffer; begin L := GetRowLengths(Field,tmp); if L=0 then begin result := Head+Trail; exit; end; SepLen := length(Sep); inc(L,length(Head)+SepLen*(fRowCount-1)+length(Trail)); FastSetString(result,nil,L); P := AppendRawUTF8ToBuffer(pointer(result),Head); U := @fResults[FieldCount+Field]; // start reading after first Row (= Field Names) len := tmp.buf; for i := 2 to fRowCount do begin MoveFast(U^^,P^,len^); inc(P,len^); if SepLen>0 then begin MoveSmall(pointer(Sep),P,SepLen); inc(P,SepLen); end; inc(len); inc(U,FieldCount); // go to next row end; MoveFast(U^^,P^,len^); // last row without Sep if Trail<>'' then MoveFast(pointer(Trail)^,P[len^],length(Trail)); tmp.Done; end; procedure TSQLTable.GetJSONValues(W: TJSONWriter; RowFirst, RowLast, IDBinarySize: integer); var U: PPUTF8Char; f,r: PtrInt; i64: Int64; label nostr,str; begin if (self=nil) or (FieldCount<=0) or (fRowCount<=0) then begin W.Add('[',']'); exit; end; // check range if RowLast=0 then RowLast := fRowCount else if RowLast>fRowCount then RowLast := fRowCount; if RowFirst<=0 then RowFirst := 1; // start reading after first Row (Row 0 = Field Names) // get col names and types if fFieldType=nil then InitFieldTypes; SetLength(W.ColNames,FieldCount); for f := 0 to FieldCount-1 do begin W.ColNames[f] := fResults[f]; // first Row is field Names if not Assigned(OnExportValue) then if (f=fFieldIndexID) and (IDBinarySize>0) then W.ColNames[f] := 'id'; // ajax-friendly end; W.AddColumns(RowLast-RowFirst+1); // write or init field names (see JSON Expand) if W.Expand then W.Add('['); // write rows data U := @fResults[FieldCount*RowFirst]; for r := RowFirst to RowLast do begin if W.Expand then W.Add('{'); for f := 0 to FieldCount-1 do begin if W.Expand then W.AddString(W.ColNames[f]); // '"'+ColNames[]+'":' if Assigned(OnExportValue) then W.AddString(OnExportValue(self,r,f,false)) else if (IDBinarySize>0) and (f=fFieldIndexID) then begin SetInt64(U^,i64); W.AddBinToHexDisplayQuoted(@i64,IDBinarySize); end else if U^=nil then W.AddShort('null') else case fFieldType[f].ContentDB of ftInt64,ftDouble,ftCurrency: nostr: W.AddNoJSONEscape(U^,StrLen(U^)); ftDate,ftUTF8,ftBlob: begin str: W.Add('"'); W.AddJSONEscape(U^); W.Add('"'); end; else if IsStringJSON(U^) then // fast and safe enough goto str else goto nostr; end; W.Add(','); inc(U); // points to next value end; W.CancelLastComma; if W.Expand then begin W.Add('}',','); if r<>RowLast then W.AddCR; // make expanded json more human readable end else W.Add(','); end; W.EndJSONObject(1,0,false); // "RowCount": set by W.AddColumns() above end; procedure TSQLTable.GetJSONValues(JSON: TStream; Expand: boolean; RowFirst, RowLast, IDBinarySize: integer); var W: TJSONWriter; tmp: TTextWriterStackBuffer; begin W := TJSONWriter.Create(JSON,Expand,false,nil,0,@tmp); try GetJSONValues(W,RowFirst,RowLast,IDBinarySize); W.FlushFinal; finally W.Free; end; end; function TSQLTable.GetJSONValues(Expand: boolean; IDBinarySize, BufferSize: integer): RawUTF8; var W: TJSONWriter; tmp: TTextWriterStackBuffer; begin if BufferSizefRowCount) then RowLast := fRowCount; if RowFirst<0 then RowFirst := 0; W := TTextWriter.Create(Dest,@temp,SizeOf(temp)); try if AddBOM then W.AddShort(#$ef#$bb#$bf); // add UTF-8 Byte Order Mark if Tab then CommaSep := #9; FMax := FieldCount-1; U := @fResults[RowFirst*FieldCount]; for R := RowFirst to RowLast do for F := 0 to FMax do begin if Assigned(OnExportValue) then W.AddString(OnExportValue(self,R,F,false)) else if Tab or not IsStringJSON(U^) then W.AddNoJSONEscape(U^,StrLen(U^)) else begin W.Add('"'); W.AddNoJSONEscape(U^,StrLen(U^)); W.Add('"'); end; if F=FMax then W.AddCR else W.Add(CommaSep); inc(U); // points to next value end; W.FlushFinal; finally W.Free; end; end; function TSQLTable.GetCSVValues(Tab: boolean; CommaSep: AnsiChar; AddBOM: boolean; RowFirst,RowLast: integer): RawUTF8; var MS: TRawByteStringStream; begin MS := TRawByteStringStream.Create; try GetCSVValues(MS,Tab,CommaSep,AddBOM,RowFirst,RowLast); result := MS.DataString; finally MS.Free; end; end; procedure TSQLTable.GetMSRowSetValues(Dest: TStream; RowFirst,RowLast: integer); const FIELDTYPE_TOXML: array[TSQLDBFieldType] of RawUTF8 = ( // ftUnknown, ftNull, ftInt64, ftDouble, ftCurrency, '','',' dt:type="i8"',' dt:type="float"',' dt:type="number" rs:dbtype="currency"', // ftDate, ftUTF8, ftBlob ' dt:type="dateTime"',' dt:type="string"',' dt:type="bin.hex"'); var W: TTextWriter; f,r: integer; U: PPUTF8Char; begin W := TTextWriter.Create(Dest,32768); try W.AddShort(''); if (self<>nil) and ((FieldCount>0) or (fRowCount>0)) then begin // retrieve normalized field names and types if length(fFieldNames)<>fFieldCount then InitFieldNames; if fFieldType=nil then InitFieldTypes; // check range if RowLast=0 then RowLast := fRowCount else if RowLast>fRowCount then RowLast := fRowCount; if RowFirst<=0 then RowFirst := 1; // start reading after first Row (Row 0 = Field Names) // write schema from col names and types W.AddShort(''); for f := 0 to FieldCount-1 do begin W.AddShort(''); end; W.AddShort(''); // write rows data U := @fResults[FieldCount*RowFirst]; W.AddShort(''); for r := RowFirst to RowLast do begin W.AddShort('nil then begin W.Add('f'); W.Add(f); W.Add('=','"'); W.AddXmlEscape(U^); W.Add('"',' '); end; inc(U); // points to next value end; W.Add('/','>'); end; W.AddShort(''); end; W.AddShort(''); W.FlushFinal; finally W.Free; end; end; function TSQLTable.GetMSRowSetValues: RawUTF8; var MS: TRawByteStringStream; begin MS := TRawByteStringStream.Create; try GetMSRowSetValues(MS,1,RowCount); result := MS.DataString; finally MS.Free; end; end; function TSQLTable.GetODSDocument(withColumnTypes: boolean): RawByteString; const ODSmimetype: RawUTF8 = 'application/vnd.oasis.opendocument.spreadsheet'; ODSContentHeader: RawUTF8 = ''+ ''; ODSmeta: RawUTF8 = XMLUTF8_HEADER+''; ODSsettings: RawUTF8 = XMLUTF8_HEADER+''; ODSmanifest: RawUTF8 = XMLUTF8_HEADER+''+ ''+ ''; var Zip: TZipWriteToStream; Dest: TRawByteStringStream; content: RawUTF8; W: TTextWriter; U: PPUTF8Char; r,f: integer; begin Dest := TRawByteStringStream.Create; try Zip := TZipWriteToStream.Create(Dest); try Zip.AddStored('mimetype',pointer(ODSmimetype),length(ODSmimetype)); Zip.AddDeflated('styles.xml',pointer(ODSstyles),length(ODSstyles)); Zip.AddDeflated('meta.xml',pointer(ODSmeta),length(ODSmeta)); Zip.AddDeflated('settings.xml',pointer(ODSsettings),length(ODSsettings)); Zip.AddDeflated('META-INF/manifest.xml',pointer(ODSmanifest),length(ODSmanifest)); W := TTextWriter.CreateOwnedStream(65536); try W.AddShort(XMLUTF8_HEADER); W.AddString(ODSContentHeader); W.Add(FieldCount); W.AddShort('" />'); if (self<>nil) and ((FieldCount>0) or (fRowCount>0)) then begin if withColumnTypes and (fFieldType=nil) then InitFieldTypes; U := pointer(fResults); for r := 0 to fRowCount do begin W.AddShort(''); if withColumnTypes and (r>0) then begin for f := 0 to FieldCount-1 do begin W.AddShort(''); end; ftDate: begin W.AddShort('date" office:date-value="'); W.AddXmlEscape(U^); W.AddShort('" />'); end; else begin //ftUnknown,ftNull,ftUTF8,ftBlob: W.AddShort('string">'); W.AddXmlEscape(U^); W.AddShort(''); end; end; inc(U); // points to next value end; end else for f := 0 to FieldCount-1 do begin W.AddShort(''); W.AddXmlEscape(U^); W.AddShort(''); inc(U); end; W.AddShort(''); end; end; W.AddShort(ODSContentFooter); W.SetText(content); finally W.Free; end; Zip.AddDeflated('content.xml',pointer(content),length(content)); finally Zip.Free; end; result := Dest.DataString; finally Dest.Free; end; end; procedure TSQLTable.GetHtmlTable(Dest: TTextWriter); var R,F: integer; U: PPUTF8Char; begin Dest.AddShort(''#10); U := pointer(fResults); for R := 0 to fRowCount do begin Dest.AddShort(''); for F := 0 to FieldCount-1 do begin if R=0 then Dest.AddShort(''); inc(U); // points to next value end; Dest.AddShort(''#10); end; Dest.AddShort('
') else Dest.AddShort(''); if Assigned(OnExportValue) and (R>0) then Dest.AddHtmlEscapeUTF8(OnExportValue(self,R,F,true),hfOutsideAttributes) else Dest.AddHtmlEscape(U^,hfOutsideAttributes); if R=0 then Dest.AddShort('') else Dest.AddShort('
'); end; function TSQLTable.GetHtmlTable(const Header: RawUTF8): RawUTF8; var W: TTextWriter; temp: TTextWriterStackBuffer; begin W := TTextWriter.CreateOwnedStream(temp); try W.AddShort(''); W.AddString(Header); W.AddShort(''#10); GetHtmlTable(W); W.AddShort(#10''); W.SetText(result); finally W.Free; end; end; function TSQLTable.GetW(Row, Field: integer): RawUnicode; begin result := UTF8DecodeToRawUnicode(Get(Row,Field),0); end; function TSQLTable.GetWP(Row, Field: integer; Dest: PWideChar; MaxDestChars: cardinal): integer; var P: PUTF8Char; begin P := Get(Row,Field); result := UTF8ToWideChar(Dest,P,MaxDestChars,0) shr 1; // bytes div 2 end; function TSQLTable.LengthW(Row, Field: integer): integer; begin // nil -> fast calculate unicode length, without any memory allocation result := Utf8ToUnicodeLength(Get(Row,Field)); end; function UTF8CompareCurr64(P1,P2: PUTF8Char): PtrInt; var V1,V2: Int64; begin // faster than UTF8CompareDouble() for pure decimal (no exponent) values V1 := StrToCurr64(P1); V2 := StrToCurr64(P2); if V1#0) and (PWord(P2)^<>ord('0')) then goto Z // P1=true P2=true else begin P: result := 1; // P1=true P2=false exit; end; end; function UTF8CompareInt32(P1,P2: PUTF8Char): PtrInt; var V1,V2: PtrInt; begin if P1=P2 then begin result := 0; exit; end; V1 := GetInteger(P1); V2 := GetInteger(P2); if V1T2 then result := +1 else // we have T1=T2 -> same Table -> sort by ID if V10 then begin er: result := UTF8IComp(P1,P2); exit; end; V2 := GetExtended(P2,Err); if Err<>0 then goto er; if V1 compare as strings result := StrComp(P1,P2) else if SameValue(V1,V2,1/MSecsPerDay) then result := 0 else if V1 sort by ID if Assigned(IDColumn) then SetInt64(IDColumn[I],i64) else SetInt64(PPUTF8Char(PtrUInt(CI)-FieldIDPtr)^,i64); if i64PID then result := +1; end; end; function TUTF8QuickSort.CompJ: integer; var i64: Int64; begin result := Params.Comp(CJ^,PP^); if result=0 then begin // same value -> sort by ID if Assigned(IDColumn) then SetInt64(IDColumn[J],i64) else SetInt64(PPUTF8Char(PtrUInt(CJ)-FieldIDPtr)^,i64); if i64PID then result := +1; end; end; procedure ExchgFields(P1,P2: PPointer; FieldCount: PtrUInt); {$ifdef CPUX86} {$ifdef FPC} nostackframe; assembler; {$endif} asm // eax=P1 edx=P2 ecx=FieldCount push esi push edi @1: mov esi, [eax] mov edi, [edx] mov [edx], esi mov [eax], edi add eax, 4 add edx, 4 dec ecx jnz @1 pop edi pop esi end; {$else} inline; var p: pointer; begin repeat p := P1^; P1^ := P2^; P2^ := p; inc(P1); inc(P2); dec(FieldCount); until FieldCount=0; end; {$endif CPUX86} procedure ExchgPointer(p1,p2: PPointer); {$ifdef HASINLINE}inline;{$endif} var p: pointer; begin p := p2^; p2^ := p1^; p1^ := p; end; procedure TUTF8QuickSort.Sort(L, R: Integer); var P: PtrInt; begin if @Params.Comp<>nil then repeat I := L; CI := @Results[I*Params.FieldCount+Params.FieldIndex]; J := R; CJ := @Results[J*Params.FieldCount+Params.FieldIndex]; P := ((I+J) shr 1); SetPP(@Results[P*Params.FieldCount+Params.FieldIndex],P); repeat // this loop has no multiplication -> most of the time is spent in compIJ if Params.Asc then begin // ascending order comparison while compI<0 do begin inc(I); inc(PByte(CI),FieldCountNextPtr); // next row end; while compJ>0 do begin dec(J); dec(PByte(CJ),FieldCountNextPtr); // previous row end; end else begin // descending order comparison while compI>0 do begin inc(I); inc(PByte(CI),FieldCountNextPtr); // next row end; while compJ<0 do begin dec(J); dec(PByte(CJ),FieldCountNextPtr); // previous row end; end; if I<=J then begin if I<>J then begin // swap elements if CurrentRow=J then // update current row number CurrentRow := I else if CurrentRow=I then CurrentRow := J; // full row exchange ExchgFields(pointer(PtrUInt(CI)-FieldFirstPtr),pointer(PtrUInt(CJ)-FieldFirstPtr), Params.FieldCount); // exchange PUTF8Char for whole I,J rows if Assigned(IDColumn) then // exchange hidden ID column also ExchgPointer(@IDColumn[I],@IDColumn[J]); end; if PP=CI then SetPP(CJ,J) else if PP=CJ then SetPP(CI,I); inc(I); dec(J); inc(PByte(CI),FieldCountNextPtr); dec(PByte(CJ),FieldCountNextPtr); end else break; until I>J; if J - L < R - I then begin // use recursion only for smaller range P := I; // I,J will be overriden in QuickSort() call if L < J then Sort(L, J); L := P; end else begin P := J; if I < R then Sort(I, R); R := P end; until L >= R; end; procedure TSQLTable.SortFields(const FieldName: RawUTF8; Asc: boolean; PCurrentRow: PInteger; FieldType: TSQLFieldType; CustomCompare: TUTF8Compare); begin SortFields(FieldIndex(FieldName),Asc,PCurrentRow,FieldType,CustomCompare); end; procedure TSQLTable.SortFields(Field: integer; Asc: boolean; PCurrentRow: PInteger; FieldType: TSQLFieldType; CustomCompare: TUTF8Compare); var quicksort: TUTF8QuickSort; // fast static object for sorting begin if (FieldCount=0) or (Cardinal(Field)>=cardinal(FieldCount)) then exit; if FieldType=sftUnknown then // guess the field type from first row FieldType := self.FieldType(Field); // store sorting parameters for re-sort in TSQLTableJSON.FillFrom() if Assigned(CustomCompare) then fSortParams.Comp := CustomCompare else begin fSortParams.Comp := SQLFieldTypeComp[FieldType]; if @fSortParams.Comp=nil then exit; end; fSortParams.FieldType := FieldType; fSortParams.FieldCount := FieldCount; fSortParams.FieldIndex := Field; fSortParams.Asc := Asc; // this sort routine is very fast, thanks to the dedicated static object quicksort.Params := fSortParams; quicksort.Results := fResults; quicksort.IDColumn := @fIDColumn[0]; quicksort.FieldCountNextPtr := FieldCount*SizeOf(PtrInt); quicksort.FieldFirstPtr := Field*SizeOf(PtrInt); if fFieldIndexID<0 then // if no ID colum, assume first quicksort.FieldIDPtr := quicksort.FieldFirstPtr else quicksort.FieldIDPtr := (Field-fFieldIndexID)*SizeOf(PtrInt); if PCurrentRow=nil then quicksort.CurrentRow := -1 else quicksort.CurrentRow := PCurrentRow^; if fRowCount>1 then // ignore first row = field names -> (1,RowCount) quicksort.Sort(1,fRowCount); if PCurrentRow<>nil then PCurrentRow^ := quicksort.CurrentRow; end; function TSQLTable.SearchFieldSorted(const Value: RawUTF8; FieldIndex: integer; CustomCompare: TUTF8Compare): integer; begin result := SearchFieldSorted(pointer(Value),FieldIndex,CustomCompare); end; function TSQLTable.SearchFieldSorted(Value: PUTF8Char; FieldIndex: integer; CustomCompare: TUTF8Compare): integer; var L,R,cmp: integer; begin if (self<>nil) and (Value<>nil) and (fRowCount>0) and (cardinal(FieldIndex)R; result := 0; end else result := SearchFieldEquals(Value,FieldIndex); end else result := 0; end; type {$ifdef USERECORDWITHMETHODS}TUTF8QuickSortMulti = record {$else}TUTF8QuickSortMulti = object{$endif} public Results: PPUtf8CharArray; IDColumn: PPUtf8CharArray; FieldCount: integer; IndexMax: integer; Index: array of record ndx: integer; Comp: TUTF8Compare; Desc: boolean; end; // used for row content comparison function Compare(A,B: integer): integer; /// recursively perform the sort procedure Sort(L, R: Integer); end; function TUTF8QuickSortMulti.Compare(A,B: integer): integer; var i: PtrInt; begin result := 0; for i := 0 to IndexMax do with Index[i] do begin if ndx>=0 then result := Comp(Results[A*FieldCount+ndx],Results[B*FieldCount+ndx]) else // Fields[].ndx=-1 for hidden ID column result := GetInt64(IDColumn[A])-GetInt64(IDColumn[B]); if result<>0 then begin if Desc then result := -result; // descending order -> inverse comparison exit; end; end; end; procedure TUTF8QuickSortMulti.Sort(L, R: Integer); var I,J,P: integer; begin if L0 do dec(J); if I<=J then begin if I<>J then begin // swap elements ExchgFields(pointer(PtrUInt(@Results[I*FieldCount])), pointer(PtrUInt(@Results[J*FieldCount])),FieldCount); if Assigned(IDColumn) then // update hidden ID column also ExchgPointer(@IDColumn[I],@IDColumn[J]); end; if P=I then P := J else if P=J then P := I; inc(I); dec(J); end; until I>J; if J - L < R - I then begin // use recursion only for smaller range if L < J then Sort(L, J); L := I; end else begin if I < R then Sort(I, R); R := J; end; until L >= R; end; procedure TSQLTable.SortFields(const Fields: array of integer; const Asc: array of boolean; const CustomCompare: array of TUTF8Compare); var quicksort: TUTF8QuickSortMulti; i: integer; begin if (self=nil) or (fRowCount<=1) or (FieldCount<=0) or (length(Fields)=0) then exit; quicksort.FieldCount := FieldCount; quicksort.IndexMax := high(Fields); SetLength(quicksort.Index,quicksort.IndexMax+1); for i := 0 to quicksort.IndexMax do with quicksort.Index[i] do begin if i<=high(CustomCompare) then Comp := CustomCompare[i]; ndx := Fields[i]; if ndx<0 then begin // Fields[]=-1 for ID column if not Assigned(fIDColumn) then begin // leave ndx<0 for hidden ID ndx := fFieldIndexID; // use the ID column if ndx<0 then exit; // no ID column available if @Comp=nil then Comp := @UTF8CompareInt64; end; continue; end; if @Comp=nil then Comp := SortCompare(ndx); if @Comp=nil then exit; // impossible to sort this kind of field (or invalid field index) end; for i := 0 to high(Asc) do if (i<=quicksort.IndexMax) and not Asc[i] then quicksort.Index[i].Desc := true; quicksort.Results := fResults; quicksort.IDColumn := @fIDColumn[0]; quicksort.Sort(1,fRowCount); // ignore first row = field names -> (1,RowCount) end; function TSQLTable.SortCompare(Field: integer): TUTF8Compare; begin result := SQLFieldTypeComp[FieldType(Field)]; end; procedure TSQLTable.Assign(source: TSQLTable); begin fResults := source.fResults; fRowCount := source.fRowCount; fFieldCount := source.fFieldCount; end; constructor TSQLTable.Create(const aSQL: RawUTF8); begin fQuerySQL := aSQL; fFieldIndexID := -1; fQueryTableIndexFromSQL := -2; // indicates not searched end; constructor TSQLTable.CreateFromTables(const Tables: array of TSQLRecordClass; const aSQL: RawUTF8); var n: integer; begin Create(aSQL); n := length(Tables); if n>0 then begin SetLength(fQueryTables,n); MoveFast(Tables[0],fQueryTables[0],n*SizeOf(TClass)); end; end; constructor TSQLTable.CreateWithColumnTypes(const ColumnTypes: array of TSQLFieldType; const aSQL: RawUTF8); begin Create(aSQL); SetLength(fQueryColumnTypes,length(ColumnTypes)); MoveFast(ColumnTypes[0],fQueryColumnTypes[0],length(ColumnTypes)*SizeOf(TSQLFieldType)); end; destructor TSQLTable.Destroy; begin fOwnedRecords.Free; inherited Destroy; end; function TSQLTable.QueryRecordType: TSQLRecordClass; begin if (self<>nil) and (pointer(fQueryTables)<>nil) then result := fQueryTables[0] else result := nil; end; function TSQLTable.NewRecord(RecordType: TSQLRecordClass): TSQLRecord; begin result := nil; if self=nil then exit; if RecordType=nil then begin RecordType := QueryRecordType; if RecordType=nil then exit; end; result := RecordType.Create; if fOwnedRecords=nil then fOwnedRecords := TSynObjectList.Create; fOwnedRecords.Add(result); end; {$ifdef ISDELPHI2010} // Delphi 2009/2010 generics are buggy function TSQLTable.ToObjectList: TObjectList; var R,Item: TSQLRecord; row: PPUtf8Char; i: integer; {$ifdef ISDELPHIXE3}rec: PSQLRecordArray;{$endif} begin result := TObjectList.Create; // TObjectList will free each T instance if (self=nil) or (fRowCount=0) then exit; R := TSQLRecordClass(T).Create; try R.FillPrepare(self); row := @fResults[FieldCount]; // row^ points to first row of data {$ifdef ISDELPHIXE3} result.Count := fRowCount; // faster than manual Add() rec := pointer(result.List); for i := 0 to fRowCount-1 do begin Item := TSQLRecordClass(T).Create; rec[i] := Item; {$else} for i := 1 to fRowCount do begin Item := TSQLRecordClass(T).Create; Result.Add(Item); {$endif} R.fFill.Fill(pointer(row),Item); Item.fInternalState := fInternalState; // set InternalState property Inc(row,FieldCount); // next data row end; finally R.Free; end; end; {$endif} procedure TSQLTable.ToObjectList(DestList: TObjectList; RecordType: TSQLRecordClass); var R: TSQLRecord; row: PPUtf8Char; rec: PSQLRecord; i: integer; begin if DestList=nil then exit; DestList.Clear; if (self=nil) or (fRowCount=0) then exit; if RecordType=nil then begin RecordType := QueryRecordType; if RecordType=nil then exit; end; R := RecordType.Create; try R.FillPrepare(self); DestList.Count := fRowCount; // faster than manual Add() rec := pointer(DestList.List); row := @fResults[FieldCount]; // row^ points to first row of data for i := 1 to fRowCount do begin rec^ := RecordType.Create; // TObjectList will own and free each instance R.fFill.Fill(pointer(row),rec^); rec^.fInternalState := fInternalState; // set InternalState property inc(rec); inc(row,FieldCount); // next data row end; finally R.Free; end; end; function TSQLTable.ToObjArray(var ObjArray; RecordType: TSQLRecordClass): boolean; var R: TSQLRecord; Row: PPUtf8Char; i: integer; arr: TSQLRecordObjArray absolute ObjArray; begin result := false; ObjArrayClear(arr); if self=nil then exit; if RecordType=nil then begin RecordType := QueryRecordType; if RecordType=nil then exit; end; result := true; if fRowCount=0 then exit; R := RecordType.Create; try R.FillPrepare(self); SetLength(arr,fRowCount); // faster than manual ObjArrayAdd() Row := @fResults[FieldCount]; // Row^ points to first row of data for i := 0 to fRowCount-1 do begin arr[i] := RecordType.Create; R.fFill.Fill(pointer(Row),arr[i]); Inc(Row,FieldCount); // next data row end; finally R.Free; end; end; function TSQLTable.ToObjectList(RecordType: TSQLRecordClass): TObjectList; begin result := TObjectList.Create; ToObjectList(result,RecordType); end; function TSQLTable.Step(SeekFirst: boolean; RowVariant: PVariant): boolean; begin result := false; if (self=nil) or (fRowCount<=0) then exit; // nothing to iterate over if SeekFirst then fStepRow := 1 else if fStepRow>=fRowCount then exit else inc(fStepRow); result := true; {$ifndef NOVARIANTS} if RowVariant=nil then exit; if SQLTableRowVariantType=nil then SQLTableRowVariantType := SynRegisterCustomVariantType(TSQLTableRowVariant); if (PVarData(RowVariant)^.VType=SQLTableRowVariantType.VarType) and (PSQLTableRowVariantData(RowVariant)^.VTable=self) and (PSQLTableRowVariantData(RowVariant)^.VRow<0) then exit; // already initialized -> quick exit VarClear(RowVariant^); PSQLTableRowVariantData(RowVariant)^.VType := SQLTableRowVariantType.VarType; PSQLTableRowVariantData(RowVariant)^.VTable := self; PSQLTableRowVariantData(RowVariant)^.VRow := -1; // follow fStepRow {$endif NOVARIANTS} end; function TSQLTable.FieldBuffer(FieldIndex: Integer): PUTF8Char; begin if (self=nil) or (cardinal(FieldIndex)>=cardinal(fFieldCount)) then raise ESQLTableException.CreateUTF8('%.FieldBuffer(%): invalid index', [self,FieldIndex]); if (fStepRow=0) or (fStepRow>fRowCount) then raise ESQLTableException.CreateUTF8('%.FieldBuffer(%): no previous Step', [self,FieldIndex]); result := fResults[fStepRow*FieldCount+FieldIndex]; end; function TSQLTable.FieldBuffer(const FieldName: RawUTF8): PUTF8Char; var i: integer; begin i := FieldIndex(FieldName); if i<0 then raise ESQLTableException.CreateUTF8('%.FieldBuffer(%): unknown field', [self,FieldName]); if (fStepRow=0) or (fStepRow>fRowCount) then raise ESQLTableException.CreateUTF8('%.FieldBuffer(%): no previous Step', [self,FieldName]); result := fResults[fStepRow*FieldCount+i]; end; function TSQLTable.FieldAsInteger(FieldIndex: Integer): Int64; begin SetInt64(FieldBuffer(FieldIndex),result); end; function TSQLTable.FieldAsInteger(const FieldName: RawUTF8): Int64; begin SetInt64(FieldBuffer(FieldName),result); end; function TSQLTable.FieldAsFloat(FieldIndex: Integer): TSynExtended; begin result := GetExtended(FieldBuffer(FieldIndex)); end; function TSQLTable.FieldAsFloat(const FieldName: RawUTF8): TSynExtended; begin result := GetExtended(FieldBuffer(FieldName)); end; function TSQLTable.FieldAsRawUTF8(FieldIndex: Integer): RawUTF8; var buf: PUTF8Char; begin buf := FieldBuffer(FieldIndex); FastSetString(result,buf,StrLen(buf)); end; function TSQLTable.FieldAsRawUTF8(const FieldName: RawUTF8): RawUTF8; var buf: PUTF8Char; begin buf := FieldBuffer(FieldName); FastSetString(result,buf,StrLen(buf)); end; function TSQLTable.FieldAsString(FieldIndex: Integer): String; var buf: PUTF8Char; begin buf := FieldBuffer(FieldIndex); UTF8DecodeToString(buf,StrLen(buf),result); end; function TSQLTable.FieldAsString(const FieldName: RawUTF8): String; var buf: PUTF8Char; begin buf := FieldBuffer(FieldName); UTF8DecodeToString(buf,StrLen(buf),result); end; {$ifndef NOVARIANTS} function TSQLTable.Field(FieldIndex: integer): variant; begin if (self=nil) or (cardinal(FieldIndex)>=cardinal(fFieldCount)) then raise ESQLTableException.CreateUTF8('%.Field(%): invalid index', [self,FieldIndex]); if (fStepRow=0) or (fStepRow>fRowCount) then raise ESQLTableException.CreateUTF8('%.Field(%): no previous Step', [self,FieldIndex]); GetVariant(fStepRow,FieldIndex,result); end; function TSQLTable.Field(const FieldName: RawUTF8): variant; var i: integer; begin i := FieldIndex(FieldName); if i<0 then raise ESQLTableException.CreateUTF8('%.Field(%): unknown field', [self,FieldName]); result := Field(i); end; {$endif} function TSQLTable.CalculateFieldLengthMean(var aResult: TIntegerDynArray; FromDisplay: boolean=false): integer; procedure CalculateEnumerates(F: integer; P: PEnumType); var R, i, n: integer; EnumCounts: array of integer; // slow GetCaption() will be called once U: PPUTF8Char; begin if P=nil then exit; // no a true enumerate field // 1. count of every possible enumerated value into EnumCounts[] SetLength(EnumCounts,P^.MaxValue+1); U := @fResults[FieldCount+F]; // start reading after first Row (= Field Names) for R := 1 to fRowCount do begin n := GetInteger(U^); if n<=P^.MaxValue then // update count of every enumerated value inc(EnumCounts[n]) else // GetCaption(invalid index) displays first one inc(EnumCounts[0]); inc(U,FieldCount); // points to next row end; // 2. update aResult[F] with displayed caption text length n := 0; for i := 0 to P^.MaxValue do if EnumCounts[i]<>0 then inc(n,length(P^.GetCaption(i))*EnumCounts[i]); aResult[F] := n; // store displayed total length end; var R,F,n: integer; U: PPUTF8Char; Tot: cardinal; begin SetLength(aResult,FieldCount); if FromDisplay and (length(fFieldLengthMean)=FieldCount) then begin MoveFast(fFieldLengthMean[0],aResult[0],FieldCount*SizeOf(integer)); result := fFieldLengthMeanSum; exit; end; if fRowCount=0 then begin // no data: calculate field length from first row (i.e. Field Names) U := @fResults[0]; for F := 0 to FieldCount-1 do begin inc(aResult[F],Utf8FirstLineToUnicodeLength(U^)); // count inc(U); // points to next value end; Tot := 1; end else begin if fFieldType=nil then InitFieldTypes; U := @fResults[FieldCount]; // start reading after first Row for R := 1 to fRowCount do // sum all lengths by field for F := 0 to FieldCount-1 do begin case fFieldType[F].ContentType of sftInteger, sftBlob, sftBlobCustom, sftUTF8Custom, sftRecord, sftRecordVersion, sftID, sftTID, sftSet, sftCurrency: inc(aResult[F],8); else inc(aResult[F],Utf8FirstLineToUnicodeLength(U^)); end; inc(U); // points to next value end; if Assigned(fQueryTables) then begin // aResult[] must be recalculated from captions, if exists for F := 0 to FieldCount-1 do with fFieldType[F] do case ContentType of sftEnumerate: CalculateEnumerates(F,ContentTypeInfo); end; end; Tot := fRowCount; end; result := 0; for F := 0 to FieldCount-1 do begin n := cardinal(aResult[F]) div Tot; // Mean = total/count if n=0 then n := 1; // none should be 0 aResult[F] := n; inc(result,n); // fast calculate mean sum end; end; function TSQLTable.FieldLengthMean(Field: integer): cardinal; begin if (self=nil) or (cardinal(Field)>=cardinal(FieldCount)) or (fResults=nil) then result := 0 else begin if fFieldLengthMean=nil then // if not already calculated, do it now fFieldLengthMeanSum := CalculateFieldLengthMean(fFieldLengthMean); result := fFieldLengthMean[Field]; end; end; function TSQLTable.FieldLengthMeanSum: cardinal; begin if self=nil then result := 0 else begin if fFieldLengthMean=nil then FieldLengthMean(0); // initialize fFieldLengthMean[] and fFieldLengthMeanSum result := fFieldLengthMeanSum; end; end; function TSQLTable.FieldLengthMax(Field: integer; NeverReturnsZero: boolean): cardinal; var i: integer; len: cardinal; U: PPUTF8Char; begin result := 0; if (self<>nil) and (cardinal(Field)=0 then // return already computed value result := ContentSize else begin if (ContentTypeInfo<>nil) and (ContentType=sftEnumerate) then begin // compute maximum size from available captions for i := 0 to PEnumType(ContentTypeInfo)^.MaxValue do begin len := length(PEnumType(ContentTypeInfo)^.GetCaption(i)); if len>result then result := len; end; end else begin // compute by reading all data rows U := @fResults[FieldCount+Field]; for i := 1 to fRowCount do begin len := StrLen(U^); if len>result then result := len; inc(U,FieldCount); end; end; ContentSize := result; end; end; if (result=0) and NeverReturnsZero then result := 1; // minimal not null length end; function TSQLTable.FieldTable(Field: integer): TSQLRecordClass; begin if (self=nil) or (cardinal(Field)>=cardinal(FieldCount)) or (fQueryTables=nil) then result := nil else begin if fFieldType=nil then InitFieldTypes; Field := fFieldType[Field].TableIndex; if Field<0 then result := nil else result := fQueryTables[Field]; end; end; procedure TSQLTable.SetFieldLengthMean(const Lengths: array of cardinal); var F: integer; n: cardinal; begin if (self=nil) or (length(Lengths)<>FieldCount) then exit; if fFieldLengthMean=nil then // if not already calculated, allocate array SetLength(fFieldLengthMean,FieldCount); fFieldLengthMeanSum := 0; for F := 0 to FieldCount-1 do begin n := Lengths[F]; if n=0 then n := 1; // none should be 0 fFieldLengthMean[F] := n; inc(fFieldLengthMeanSum,n); // fast calculate mean sum end; end; procedure TSQLTable.FieldLengthMeanIncrease(aField, aIncrease: integer); begin if (self=nil) or (cardinal(aField)>=cardinal(FieldCount)) then exit; // avoid GPF if fFieldLengthMean=nil then FieldLengthMean(0); // initialize fFieldLengthMean[] and fFieldLengthMeanSum inc(fFieldLengthMean[aField],aIncrease); inc(fFieldLengthMeanSum,aIncrease); end; function TSQLTable.SearchValue(const UpperValue: RawUTF8; StartRow, FieldIndex: integer; Client: TObject; Lang: TSynSoundExPronunciation; UnicodeComparison: boolean): integer; var U: PPUTF8Char; Kind: TSQLFieldType; Search: PAnsiChar; UpperUnicode: RawUnicode; UpperUnicodeLen: integer; info: PSQLTableFieldType; Val64: Int64; ValTimeLog: TTimelogBits absolute Val64; i,err: integer; EnumValue: RawUTF8; s: string; P: PShortString; EnumValues: set of 0..63; Soundex: TSynSoundEx; CL: TSQLRest absolute Client; tmp: array[0..23] of AnsiChar; begin result := 0; if (self=nil) or (StartRow<=0) or (StartRow>fRowCount) or (UpperValue='') or (cardinal(FieldIndex)>=cardinal(FieldCount)) then exit; Search := pointer(UpperValue); if Search^='%' then begin inc(Search); if Search^='%' then begin inc(Search); if Search^='%' then begin inc(Search); Lang := sndxSpanish; end else Lang := sndxFrench; end else Lang := sndxEnglish; end; if ((Lang<>sndxNone) and not Soundex.Prepare(Search,Lang)) then exit; result := StartRow; Kind := FieldType(FieldIndex,info); U := @fResults[FieldCount*StartRow+FieldIndex]; // search in one specified field value if (Kind=sftEnumerate) and (info.ContentTypeInfo<>nil) then begin // for enumerates: first search in all available values Int64(EnumValues) := 0; P := @PEnumType(info.ContentTypeInfo)^.NameList; for i := 0 to PEnumType(info.ContentTypeInfo)^.MaxValue do begin EnumValue := TrimLeftLowerCaseShort(P); GetCaptionFromPCharLen(pointer(EnumValue),s); StringToUTF8(s,EnumValue); if ((Lang<>sndxNone) and SoundEx.UTF8(pointer(EnumValue))) or ((Lang=sndxNone) and FindUTF8(pointer(EnumValue),Search)) then include(EnumValues,i); inc(PByte(P),ord(P^[0])+1); end; // then search directly from the INTEGER value if Int64(EnumValues)<>0 then while cardinal(result)<=cardinal(fRowCount) do begin i := GetInteger(U^,err); if (err=0) and (i in EnumValues) then exit; // we found a matching field inc(U,FieldCount); // ignore all other fields -> jump to next row data inc(Result); end; result := 0; // not found exit; end; // special cases: conversion from INTEGER to text before search if Kind in [sftTimeLog,sftModTime,sftCreateTime,sftUnixTime,sftUnixMSTime] then while cardinal(result)<=cardinal(fRowCount) do begin SetInt64(U^,Val64); if Val64<>0 then begin case Kind of sftUnixTime: ValTimeLog.FromUnixTime(Val64); sftUnixMSTime: // seconds resolution is enough for value search ValTimeLog.FromUnixMSTime(Val64); end; tmp[ValTimeLog.Text(tmp,true,' ')] := #0; if FindAnsi(tmp,Search) then exit; end; inc(U,FieldCount); // ignore all other fields -> jump to next row data inc(Result); end else if ((Kind in [sftRecord,sftID,sftTID,sftSessionUserID]) and (Client<>nil) and Client.InheritsFrom(TSQLRest) and (CL.Model<>nil)) then while cardinal(result)<=cardinal(fRowCount) do begin SetInt64(U^,Val64); if Val64<>0 then begin if Kind=sftRecord then EnumValue := RecordRef(Val64).Text(CL.Model) else EnumValue := U^; // sftID/sftTID -> display ID number -> no sounded if Lang=sndxNone then begin if FindUTF8(pointer(EnumValue),Search) then exit; end else if SoundEx.UTF8(pointer(EnumValue)) then exit; end; inc(U,FieldCount); // ignore all other fields -> jump to next row data inc(Result); end else // by default, search as UTF-8 encoded text if Lang<>sndxNone then begin while cardinal(result)<=cardinal(fRowCount) do if SoundEx.UTF8(U^) then exit else begin inc(U,FieldCount); // ignore all other fields -> jump to next row data inc(Result); end; end else if UnicodeComparison then begin // slowest but always accurate Unicode comparison UpperUnicode := UTF8DecodeToRawUnicodeUI(RawUTF8(Search),@UpperUnicodeLen); while cardinal(result)<=cardinal(fRowCount) do if FindUnicode(pointer(Utf8DecodeToRawUnicode(U^,0)), pointer(UpperUnicode),UpperUnicodeLen) then exit else begin inc(U,FieldCount); // ignore all other fields -> jump to next row data inc(Result); end end else // default fast Win1252 search while cardinal(result)<=cardinal(fRowCount) do if FindUTF8(U^,Search) then exit else begin inc(U,FieldCount); // ignore all other fields -> jump to next row data inc(Result); end; result := 0; // not found end; function TSQLTable.SearchValue(const UpperValue: RawUTF8; StartRow: integer; FieldIndex: PInteger; Client: TObject; Lang: TSynSoundExPronunciation; UnicodeComparison: boolean): integer; var F, Row: integer; begin result := 0; if (self=nil) or (StartRow<=0) or (StartRow>fRowCount) or (UpperValue='') then exit; // search in all fields values for F := 0 to FieldCount-1 do begin Row := SearchValue(UpperValue,StartRow,F,Client,Lang,UnicodeComparison); if (Row<>0) and ((result=0) or (Rownil then FieldIndex^ := F; result := Row; end; end; end; function TSQLTable.SearchFieldEquals(const Value: RawUTF8; FieldIndex, StartRow: integer; CaseSensitive: boolean): integer; begin result := SearchFieldEquals(pointer(Value),FieldIndex,StartRow,CaseSensitive); end; function TSQLTable.SearchFieldEquals(Value: PUTF8Char; FieldIndex, StartRow: integer; CaseSensitive: boolean): integer; var U: PPUTF8Char; begin if (self<>nil) and (Value<>nil) and (cardinal(FieldIndex)nil) and (Value<>'') and (cardinal(FieldIndex)0 then GetVariant(r,v,Result); end; {$endif NOVARIANTS} function TSQLTable.ExpandAsString(Row, Field: integer; Client: TObject; out Text: string; const CustomFormat: string): TSQLFieldType; var info: PSQLTableFieldType; err: integer; Value: Int64; ValueTimeLog: TTimeLogBits absolute Value; ValueDateTime: TDateTime; Ref: RecordRef absolute Value; label IsDateTime; begin // Text was already forced to '' because was defined as "out" parameter if Row=0 then begin // Field Name result := sftUnknown; Text := GetCaption(0,Field); exit; end; result := FieldType(Field,info); case result of sftDateTime, sftDateTimeMS: begin Value := Iso8601ToTimeLogPUTF8Char(Get(Row,Field),0); IsDateTime: if Value<>0 then begin {$ifndef LVCL} if CustomFormat<>'' then begin Text := FormatDateTime(CustomFormat,ValueTimeLog.ToDateTime); if Text<>CustomFormat then exit; // valid conversion end; {$endif LVCL} Text := ValueTimeLog.i18nText; exit; end; end; sftBlob: Text := '???'; sftFloat: if CustomFormat<>'' then try if pos('%',CustomFormat)>0 then Text := Format(CustomFormat,[GetExtended(Get(Row,Field))]) {$ifndef LVCL} else Text := FormatFloat(CustomFormat,GetExtended(Get(Row,Field))) {$endif LVCL}; exit; except on {$ifdef LVCL}Exception{$else}EConvertError{$endif} do Text := ''; end; sftCurrency: if CustomFormat<>'' then try if pos('%',CustomFormat)>0 then Text := Format(CustomFormat,[StrToCurrency(Get(Row,Field))]) {$ifndef LVCL} else Text := FormatCurr(CustomFormat,StrToCurrency(Get(Row,Field))) {$endif}; exit; except on {$ifdef LVCL}Exception{$else}EConvertError{$endif} do Text := ''; end; sftEnumerate, sftSet, sftRecord, sftID, sftTID, sftRecordVersion, sftSessionUserID, sftTimeLog, sftModTime, sftCreateTime, sftUnixTime, sftUnixMSTime: begin Value := GetInt64(Get(Row,Field),err); if err<>0 then // not an integer -> to be displayed as sftUTF8Text result := sftUTF8Text else case result of sftEnumerate: if info.ContentTypeInfo<>nil then begin Text := PEnumType(info.ContentTypeInfo)^.GetCaption(Value); exit; end; sftTimeLog, sftModTime, sftCreateTime: goto IsDateTime; sftUnixTime: begin ValueTimeLog.FromUnixTime(Value); goto IsDateTime; end; sftUnixMSTime: if Value<>0 then begin ValueDateTime := UnixMSTimeToDateTime(Value); {$ifndef LVCL} if CustomFormat<>'' then begin Text := FormatDateTime(CustomFormat,ValueDateTime); if Text<>CustomFormat then exit; // valid conversion end; {$endif LVCL} Text := DateTimeToi18n(ValueDateTime); exit; end; { sftID, sftTID, sftSet, sftRecordVersion: result := sftUTF8Text; // will display INTEGER field as number } sftRecord: if (Value<>0) and (Client<>nil) and Client.InheritsFrom(TSQLRest) then // 'TableName ID' Text := {$ifdef UNICODE}Ansi7ToString{$endif}(Ref.Text(TSQLRest(Client).Model)) else result := sftUTF8Text; // display ID number if no table model end; end; end; if Text='' then // returns the value as text by default Text := GetString(Row,Field); end; function TSQLTable.ExpandAsSynUnicode(Row,Field: integer; Client: TObject; out Text: SynUnicode): TSQLFieldType; var s: string; begin result := ExpandAsString(Row,Field,Client,s); StringToSynUnicode(s,Text); end; function TSQLTable.GetTimeLog(Row, Field: integer; Expanded: boolean; FirstTimeChar: AnsiChar): RawUTF8; var Value: TTimeLogBits; begin SetInt64(Get(Row,Field),Value.Value); result := Value.Text(Expanded,FirstTimeChar); end; {$ifndef NOVARIANTS} { TSQLTableRowVariant } function TSQLTableRowVariant.IntGet(var Dest: TVarData; const Instance: TVarData; Name: PAnsiChar; NameLen: PtrInt): boolean; var r,f: integer; rv: TSQLTableRowVariantData absolute Instance; begin if rv.VTable=nil then raise ESQLTableException.CreateUTF8('Invalid %.% call',[self,Name]); r := rv.VRow; if r<0 then begin r := rv.VTable.fStepRow; if (r=0) or (r>rv.VTable.fRowCount) then raise ESQLTableException.CreateUTF8('%.%: no previous Step',[self,Name]); end; f := rv.VTable.FieldIndex(PUTF8Char(Name)); result := f>=0; if f>=0 then rv.VTable.GetVariant(r,f,Variant(Dest)); end; procedure TSQLTableRowVariant.Cast(var Dest: TVarData; const Source: TVarData); begin CastTo(Dest,Source,VarType); end; procedure TSQLTableRowVariant.CastTo(var Dest: TVarData; const Source: TVarData; const AVarType: TVarType); var r: integer; tmp: variant; // use a temporary TDocVariant for the conversion begin if AVarType=VarType then begin RaiseCastError; end else begin if Source.VType<>VarType then RaiseCastError; r := TSQLTableRowVariantData(Source).VRow; if r<0 then r := TSQLTableRowVariantData(Source).VTable.fStepRow; TSQLTableRowVariantData(Source).VTable.ToDocVariant(r,tmp); if AVarType=DocVariantVType then begin VarClear(variant(Dest)); TDocVariantData(Dest) := TDocVariantData(tmp); end else RawUTF8ToVariant(VariantSaveJSON(tmp),Dest,AVarType); end; end; procedure TSQLTableRowVariant.ToJSON(W: TTextWriter; const Value: variant; Escape: TTextWriterKind); var r: integer; tmp: variant; // write row via a TDocVariant begin r := TSQLTableRowVariantData(Value).VRow; if r<0 then r := TSQLTableRowVariantData(Value).VTable.fStepRow; TSQLTableRowVariantData(Value).VTable.ToDocVariant(r,tmp); W.AddVariant(tmp,Escape); end; { TObjectVariant } var ObjectVariantType: TCustomVariantType; class procedure TObjectVariant.New(var V: Variant; Obj: TObject); begin VarClear(V); if ObjectVariantType=nil then ObjectVariantType := SynRegisterCustomVariantType(TObjectVariant); TVarData(V).VType := ObjectVariantType.VarType; TVarData(V).VPointer := Obj; end; procedure TObjectVariant.ToJSON(W: TTextWriter; const Value: variant; Escape: TTextWriterKind); begin W.WriteObject(TVarData(Value).VPointer); end; const _INTGETOBJECTPROPINFO_ID = pointer(1); function IntGetObjectPropInfo(o: TObject; Name: pointer; NameLen: PtrInt): PPropInfo; begin if (o=nil) or (Name=nil) then raise EObjectVariant.CreateUTF8('Invalid TObjectVariant.% call',[Name]); result := ClassFieldPropWithParentsFromUTF8(PPointer(o)^,Name,NameLen); if (result=nil) and IsRowID(Name,NameLen) and o.InheritsFrom(TSQLRecord) then result := _INTGETOBJECTPROPINFO_ID; end; function TObjectVariant.IntGet(var Dest: TVarData; const Instance: TVarData; Name: PAnsiChar; NameLen: PtrInt): boolean; var info: PPropInfo; o: TObject; begin o := Instance.VPointer; info := IntGetObjectPropInfo(o,Name,NameLen); result := info<>nil; if info<>nil then if info=_INTGETOBJECTPROPINFO_ID then variant(Dest) := TSQLRecord(o).IDValue else if info^.PropType^.Kind=tkClass then New(Variant(Dest),info^.GetObjProp(o)) else info^.GetVariant(o,Variant(Dest)); end; function TObjectVariant.IntSet(const Instance, Value: TVarData; Name: PAnsiChar; NameLen: PtrInt): boolean; var info: PPropInfo; o: TObject; begin o := Instance.VPointer; info := IntGetObjectPropInfo(o,Name,NameLen); if info=nil then result := false else begin if info=_INTGETOBJECTPROPINFO_ID then VariantToInt64(Variant(Value),PInt64(@TSQLRecord(o).fID)^) else info^.SetFromVariant(o,Variant(Value)); result := true; end; end; {$endif NOVARIANTS} procedure Base64MagicToBlob(Base64: PUTF8Char; var result: RawUTF8); begin // do not escape the result: returns e.g. X'53514C697465' result := TSQLRawBlobToBlob(Base64ToBin(PAnsiChar(Base64),StrLen(Base64))); end; { TJSONObjectDecoder } const EndOfJSONField = [',',']','}',':']; procedure GetJSONArrayOrObject(P: PUTF8Char; out PDest: PUTF8Char; EndOfObject: PUTF8Char; var result: RawUTF8); var Beg: PUTF8Char; begin PDest := nil; Beg := P; P := GotoNextJSONObjectOrArray(P); // quick go to end of array of object if P=nil then begin result := ''; exit; end; if EndOfObject<>nil then EndOfObject^ := P^; PDest := P+1; FastSetString(result,Beg,P-Beg); end; procedure GetJSONArrayOrObjectAsQuotedStr(P: PUTF8Char; out PDest: PUTF8Char; EndOfObject: PUTF8Char; var result: RawUTF8); var Beg: PUTF8Char; begin result := ''; PDest := nil; Beg := P; P := GotoNextJSONObjectOrArray(P); // quick go to end of array of object if P=nil then exit; if EndOfObject<>nil then EndOfObject^ := P^; P^ := #0; // so Beg will be a valid ASCIIZ string PDest := P+1; QuotedStr(Beg,'''',result); end; procedure TJSONObjectDecoder.Decode(var P: PUTF8Char; const Fields: TRawUTF8DynArray; Params: TJSONObjectDecoderParams; const RowID: TID; ReplaceRowIDWithID: boolean); var EndOfObject: AnsiChar; procedure GetSQLValue(ndx: PtrInt); var wasString: boolean; res: PUTF8Char; resLen, c: integer; begin res := P; if res=nil then begin FieldTypeApproximation[ndx] := ftaNull; FieldValues[ndx] := NULL_STR_VAR; exit; end; while res^ in [#1..' '] do inc(res); if (PInteger(res)^=NULL_LOW) and (res[4] in [#0,#9,#10,#13,' ',',','}',']']) then begin /// GetJSONField('null') returns '' -> check here to make a diff with '""' FieldTypeApproximation[ndx] := ftaNull; FieldValues[ndx] := NULL_STR_VAR; inc(res,4); while res^ in [#1..' '] do inc(res); if res^=#0 then P := nil else begin EndOfObject := res^; res^ := #0; P := res+1; end; end else begin // first check if nested object or array case res^ of // handle JSON {object} or [array] in P '{': begin // will work e.g. for custom variant types FieldTypeApproximation[ndx] := ftaObject; if params=pNonQuoted then GetJSONArrayOrObject(res,P,@EndOfObject,FieldValues[ndx]) else GetJSONArrayOrObjectAsQuotedStr(res,P,@EndOfObject,FieldValues[ndx]); end; '[': begin // will work e.g. for custom variant types FieldTypeApproximation[ndx] := ftaArray; if params=pNonQuoted then GetJSONArrayOrObject(res,P,@EndOfObject,FieldValues[ndx]) else GetJSONArrayOrObjectAsQuotedStr(res,P,@EndOfObject,FieldValues[ndx]); end; else begin // handle JSON string, number or false/true in P res := GetJSONField(res,P,@wasString,@EndOfObject,@resLen); if wasString then begin c := PInteger(res)^ and $00ffffff; if c=JSON_BASE64_MAGIC then begin FieldTypeApproximation[ndx] := ftaBlob; case Params of pInlined: // untouched -> recognized as BLOB in SQLParamContent() QuotedStr(res,'''',FieldValues[ndx]); { pQuoted: // \uFFF0base64encodedbinary -> 'X''hexaencodedbinary''' // if not inlined, it can be used directly in INSERT/UPDATE statements Base64MagicToBlob(res+3,FieldValues[ndx]); pNonQuoted:} else // returned directly as RawByteString Base64ToBin(PAnsiChar(res)+3,resLen-3,RawByteString(FieldValues[ndx])); end; end else begin if c=JSON_SQLDATE_MAGIC then begin FieldTypeApproximation[ndx] := ftaDate; inc(res,3); // ignore \uFFF1 magic marker end else FieldTypeApproximation[ndx] := ftaString; // regular string content if Params=pNonQuoted then // returned directly as RawUTF8 FastSetString(FieldValues[ndx],res,resLen) else { escape SQL strings, cf. the official SQLite3 documentation: "A string is formed by enclosing the string in single quotes ('). A single quote within the string can be encoded by putting two single quotes in a row - as in Pascal." } QuotedStr(res,'''',FieldValues[ndx]); end; end else if res=nil then begin FieldTypeApproximation[ndx] := ftaNull; FieldValues[ndx] := NULL_STR_VAR; end else // avoid GPF, but will return invalid SQL // non string params (numeric or false/true) are passed untouched if PInteger(res)^=FALSE_LOW then begin FieldValues[ndx] := SmallUInt32UTF8[0]; FieldTypeApproximation[ndx] := ftaBoolean; end else if PInteger(res)^=TRUE_LOW then begin FieldValues[ndx] := SmallUInt32UTF8[1]; FieldTypeApproximation[ndx] := ftaBoolean; end else begin FastSetString(FieldValues[ndx],res,resLen); FieldTypeApproximation[ndx] := ftaNumber; end; end; end; end; end; var FN: PUTF8Char; F, FNlen: integer; FieldIsRowID: Boolean; begin FieldCount := 0; DecodedRowID := 0; DecodedFieldTypesToUnnest := nil; FillCharFast(FieldTypeApproximation,SizeOf(FieldTypeApproximation),ord(ftaNumber{TID})); InlinedParams := Params; if pointer(Fields)=nil then begin // get "COL1"="VAL1" pairs, stopping at '}' or ']' DecodedFieldNames := @FieldNames; if RowID>0 then begin // insert explicit RowID if ReplaceRowIDWithID then FieldNames[0] := 'ID' else FieldNames[0] := 'RowID'; Int64ToUtf8(RowID,FieldValues[0]); FieldCount := 1; DecodedRowID := RowID; end; repeat if P=nil then break; FN := GetJSONPropName(P,@FNlen); if (FN=nil) or (P=nil) then break; // invalid JSON field name FieldIsRowID := IsRowId(FN); if FieldIsRowID then if RowID>0 then begin GetJSONField(P,P,nil,@EndOfObject); // ignore this if explicit RowID if EndOfObject in [#0,'}',']'] then break else continue; end else if ReplaceRowIDWithID then begin FN := 'ID'; FNlen := 2; end; FastSetString(FieldNames[FieldCount],FN,FNlen); GetSQLValue(FieldCount); // update EndOfObject if FieldIsRowID then SetID(FieldValues[FieldCount],DecodedRowID); inc(FieldCount); if FieldCount=MAX_SQLFIELDS then raise EParsingException.Create('Too many inlines in TJSONObjectDecoder'); until EndOfObject in [#0,'}',']']; end else begin // get "VAL1","VAL2"... if P=nil then exit; if RowID>0 then raise EParsingException.Create('TJSONObjectDecoder(expanded) won''t handle RowID'); if length(Fields)>MAX_SQLFIELDS then raise EParsingException.Create('Too many inlines in TJSONObjectDecoder'); DecodedFieldNames := pointer(Fields); FieldCount := length(Fields); for F := 0 to FieldCount-1 do GetSQLValue(F); // update EndOfObject end; end; procedure TJSONObjectDecoder.Decode(const JSON: RawUTF8; const Fields: TRawUTF8DynArray; Params: TJSONObjectDecoderParams; const RowID: TID; ReplaceRowIDWithID: Boolean); var tmp: TSynTempBuffer; P: PUTF8Char; begin tmp.Init(JSON); try P := tmp.buf; if P<>nil then while P^ in [#1..' ','{','['] do inc(P); Decode(P,Fields,Params,RowID,ReplaceRowIDWithID); finally tmp.Done; end; end; function TJSONObjectDecoder.SameFieldNames(const Fields: TRawUTF8DynArray): boolean; var i: integer; begin result := false; if length(Fields)<>FieldCount then exit; for i := 0 to FieldCount-1 do if not IdemPropNameU(Fields[i],FieldNames[i]) then exit; result := true; end; procedure TJSONObjectDecoder.AssignFieldNamesTo(var Fields: TRawUTF8DynArray); var i: integer; begin SetLength(Fields,FieldCount); for i := 0 to FieldCount-1 do Fields[i] := FieldNames[i]; end; {$ifdef ISDELPHI20062007} {$WARNINGS OFF} // circument Delphi 2007 false positive warning {$endif} const PG_FT: array[TSQLDBFieldType] of string[9] = ( 'int4', 'text', 'int8', 'float8', 'numeric', 'timestamp', 'text', 'bytea'); function TJSONObjectDecoder.EncodeAsSQLPrepared(const TableName: RawUTF8; Occasion: TSQLOccasion; const UpdateIDFieldName: RawUTF8; BatchOptions: TSQLRestBatchOptions): RawUTF8; var F: integer; W: TTextWriter; temp: TTextWriterStackBuffer; begin W := TTextWriter.CreateOwnedStream(temp); try case Occasion of soUpdate: begin if FieldCount=0 then raise EORMException.Create('Invalid EncodeAsSQLPrepared(0)'); W.AddShort('update '); W.AddString(TableName); if DecodedFieldTypesToUnnest<>nil then begin // PostgreSQL bulk update via nested array binding W.AddShort(' as t set '); for F := 0 to FieldCount-1 do begin W.AddString(DecodedFieldNames^[F]); W.AddShort('=v.'); W.AddString(DecodedFieldNames^[F]); W.Add(','); end; W.CancelLastComma; W.AddShort(' from ( select'); for F := 0 to FieldCount-1 do begin W.AddShort(' unnest(?::'); W.AddShort(PG_FT[DecodedFieldTypesToUnnest^[F]]); W.AddShort('[]),'); end; W.AddShort(' unnest(?::int8[]) ) as v('); // last param is ID for F := 0 to FieldCount-1 do begin W.AddString(DecodedFieldNames^[F]); W.Add(','); end; W.AddString(UpdateIDFieldName); W.AddShort(') where t.'); W.AddString(UpdateIDFieldName); W.AddShort('=v.'); W.AddString(UpdateIDFieldName); end else begin // regular UPDATE statement W.AddShort(' set '); for F := 0 to FieldCount-1 do begin // append 'COL1=?,COL2=?' W.AddString(DecodedFieldNames^[F]); W.AddShort('=?,'); end; W.CancelLastComma; W.AddShort(' where '); W.AddString(UpdateIDFieldName); W.Add('=','?'); // last param is ID end; end; soInsert: begin if boInsertOrIgnore in BatchOptions then W.AddShort('insert or ignore into ') else if boInsertOrReplace in BatchOptions then W.AddShort('insert or replace into ') else W.AddShort('insert into '); W.AddString(TableName); if FieldCount=0 then W.AddShort(' default values') else begin W.Add(' ','('); for F := 0 to FieldCount-1 do begin // append 'COL1,COL2' W.AddString(DecodedFieldNames^[F]); W.Add(','); end; W.CancelLastComma; W.AddShort(') values ('); if DecodedFieldTypesToUnnest<>nil then // PostgreSQL bulk insert via nested array binding for F := 0 to FieldCount-1 do begin W.AddShort('unnest(?::'); W.AddShort(PG_FT[DecodedFieldTypesToUnnest^[F]]); W.AddShort('[]),'); end else // regular INSERT statement W.AddStrings('?,',FieldCount); W.CancelLastComma; W.Add(')'); end; end; else raise EORMException.CreateUTF8('Unexpected EncodeAsSQLPrepared(%)',[ord(Occasion)]); end; W.SetText(result); finally W.Free; end; end; {$ifdef ISDELPHI20062007} {$WARNINGS ON} {$endif} function TJSONObjectDecoder.EncodeAsSQL(Update: boolean): RawUTF8; var F: integer; W: TTextWriter; temp: TTextWriterStackBuffer; procedure AddValue; begin if InlinedParams=pInlined then W.AddShort(':('); W.AddString(FieldValues[F]); if InlinedParams=pInlined then W.AddShort('):,') else W.Add(','); end; begin result := ''; if FieldCount=0 then exit; W := TTextWriter.CreateOwnedStream(temp); try if Update then begin for F := 0 to FieldCount-1 do // append 'COL1=...,COL2=...' if not IsRowID(pointer(DecodedFieldNames^[F])) then begin W.AddString(DecodedFieldNames^[F]); W.Add('='); AddValue; end; W.CancelLastComma; end else begin // returns ' (COL1,COL2) VALUES ('VAL1',VAL2)' W.Add(' ','('); for F := 0 to FieldCount-1 do begin // append 'COL1,COL2' W.AddString(DecodedFieldNames^[F]); W.Add(','); end; W.CancelLastComma; W.AddShort(') VALUES ('); for F := 0 to FieldCount-1 do AddValue; W.CancelLastComma; W.Add(')'); end; W.SetText(result); finally W.Free; end; end; procedure TJSONObjectDecoder.EncodeAsJSON(out result: RawUTF8); var F: integer; W: TTextWriter; temp: TTextWriterStackBuffer; begin if FieldCount=0 then exit; W := TTextWriter.CreateOwnedStream(temp); try W.Add('{'); for F := 0 to FieldCount-1 do begin W.AddFieldName(DecodedFieldNames^[F]); if FieldTypeApproximation[F] in [ftaBlob,ftaDate,ftaString] then if InlinedParams=pNonQuoted then W.AddJSONString(FieldValues[F]) else W.AddQuotedStringAsJSON(FieldValues[F]) else W.AddString(FieldValues[F]); W.Add(','); end; W.CancelLastComma; W.Add('}'); W.SetText(result); finally W.Free; end; end; function TJSONObjectDecoder.FindFieldName(const FieldName: RawUTF8): integer; begin for result := 0 to FieldCount-1 do if IdemPropNameU(FieldNames[result],FieldName) then exit; result := -1; end; procedure TJSONObjectDecoder.AddFieldValue(const FieldName,FieldValue: RawUTF8; FieldType: TJSONObjectDecoderFieldType); begin if FieldCount=MAX_SQLFIELDS then raise EParsingException.CreateUTF8('Too many fields for TJSONObjectDecoder.AddField(%) max=%', [FieldName,MAX_SQLFIELDS]); FieldNames[FieldCount] := FieldName; FieldValues[FieldCount] := FieldValue; FieldTypeApproximation[FieldCount] := FieldType; inc(FieldCount); end; const FROMINLINED: array[boolean] of TJSONObjectDecoderParams = ( pQuoted, pInlined); function GetJSONObjectAsSQL(var P: PUTF8Char; const Fields: TRawUTF8DynArray; Update, InlinedParams: boolean; RowID: TID; ReplaceRowIDWithID: Boolean): RawUTF8; var Decoder: TJSONObjectDecoder; begin Decoder.Decode(P,Fields,FROMINLINED[InlinedParams],RowID,ReplaceRowIDWithID); result := Decoder.EncodeAsSQL(Update); end; function GetJSONObjectAsSQL(const JSON: RawUTF8; Update, InlinedParams: boolean; RowID: TID; ReplaceRowIDWithID: Boolean): RawUTF8; var Decoder: TJSONObjectDecoder; begin Decoder.Decode(JSON,nil,FROMINLINED[InlinedParams],RowID,ReplaceRowIDWithID); result := Decoder.EncodeAsSQL(Update); end; function Expect(var P: PUTF8Char; Value: PUTF8Char; ValueLen: PtrInt): boolean; {$ifdef HASINLINE}inline;{$endif} var i: PtrInt; begin // ValueLen is at least 8 bytes long result := false; if P=nil then exit; while (P^<=' ') and (P^<>#0) do inc(P); if PPtrInt(P)^=PPtrInt(Value)^ then begin for i := SizeOf(PtrInt) to ValueLen - 1 do if P[i]<>Value[i] then exit; inc(P,ValueLen); result := true; end; end; function GetJSONIntegerVar(var P: PUTF8Char): PtrInt; var c: PtrUInt; begin while (P^<=' ') and (P^<>#0) do inc(P); c := byte(P^)-48; if c>9 then result := 0 else begin result := c; inc(P); repeat c := byte(P^)-48; if c>9 then break; result := result*10+PtrInt(c); inc(P); until false; end; end; function GetJSONInt64Var(var P: PUTF8Char): Int64; var c: PtrUInt; begin while (P^<=' ') and (P^<>#0) do inc(P); c := byte(P^)-48; if c>9 then result := 0 else begin result := c; inc(P); repeat c := byte(P^)-48; if c>9 then break; result := result*10+Int64(c); inc(P); until false; end; end; const FIELDCOUNT_PATTERN: PUTF8Char = '{"fieldCount":'; // 14 chars ROWCOUNT_PATTERN: PUTF8Char = ',"rowCount":'; // 12 chars VALUES_PATTERN: PUTF8Char = ',"values":['; // 11 chars function UnJSONFirstField(var P: PUTF8Char): RawUTF8; // expand=true: [ {"col1":val11} ] -> val11 // expand=false: { "fieldCount":1,"values":["col1",val11] } -> vall11 begin result := ''; if P=nil then exit; if Expect(P,FIELDCOUNT_PATTERN,14) then begin // not expanded format if GetJSONIntegerVar(P)<>1 then exit; // wrong field count while P^<>'[' do if P^=#0 then exit else inc(P); // go to ["col1" inc(P); // go to "col1" end else begin // expanded format while P^<>'[' do if P^=#0 then exit else inc(P); // need an array of objects repeat inc(P); if P^=#0 then exit; until P^='{'; // go to object begining end; if GetJSONPropName(P)<>nil then // ignore field name result := GetJSONField(P,P); // get field value end; function IsNotAjaxJSON(P: PUTF8Char): Boolean; begin result := Expect(P,FIELDCOUNT_PATTERN,14); end; function NotExpandedBufferRowCountPos(P,PEnd: PUTF8Char): PUTF8Char; var i: integer; begin result := nil; if (PEnd<>nil) and (PEnd-P>24) then for i := 1 to 24 do // search for "rowCount": at the end of the JSON buffer case PEnd[-i] of ']',',': exit; ':': begin if CompareMemFixed(PEnd-i-11,pointer(ROWCOUNT_PATTERN),11) then result := PEnd-i+1; exit; end; end; end; function IsNotExpandedBuffer(var P: PUTF8Char; PEnd: PUTF8Char; var FieldCount,RowCount: integer): boolean; procedure GetRowCountNotExpanded(P: PUTF8Char); begin RowCount := 0; repeat // get a row P := GotoNextJSONItem(P,FieldCount); if P=nil then exit; // unexpected end inc(RowCount); until P[-1]=']'; // end of array if P^ in ['}',','] then begin // expected formated JSON stream if RowCount>0 then dec(RowCount); // first Row = field names -> data in rows 1..RowCount end else RowCount := -1; // bad format -> no data end; var RowCountPos: PUTF8Char; begin if not Expect(P,FIELDCOUNT_PATTERN,14) then begin result := false; exit; end; FieldCount := GetJSONIntegerVar(P); if Expect(P,ROWCOUNT_PATTERN,12) then RowCount := GetJSONIntegerVar(P) else begin RowCountPos := NotExpandedBufferRowCountPos(P,PEnd); if RowCountPos=nil then RowCount := -1 else // mark "rowCount":.. not available RowCount := GetCardinal(RowCountPos); end; result := (FieldCount<>0) and Expect(P,VALUES_PATTERN,11); if result and (RowCount<0) then GetRowCountNotExpanded(P); // returns RowCount=-1 if P^ is invalid end; function StartWithQuotedID(P: PUTF8Char; out ID: TID): boolean; begin if PCardinal(P)^ and $ffffdfdf= ord('I')+ord('D')shl 8+ord('"')shl 16+ord(':')shl 24 then begin SetID(P+4,ID); result := ID>0; exit; end else if (PCardinalArray(P)^[0] and $dfdfdfdf= ord('R')+ord('O')shl 8+ord('W')shl 16+ord('I')shl 24) and (PCardinalArray(P)^[1] and $ffffdf= ord('D')+ord('"')shl 8+ord(':')shl 16) then begin SetID(P+7,ID); result := ID>0; exit; end; ID := 0; result := false; end; function StartWithID(P: PUTF8Char; out ID: TID): boolean; begin if PCardinal(P)^ and $ffdfdf= ord('I')+ord('D')shl 8+ord(':')shl 16 then begin SetID(P+3,ID); result := ID>0; exit; end else if (PCardinalArray(P)^[0] and $dfdfdfdf= ord('R')+ord('O')shl 8+ord('W')shl 16+ord('I')shl 24) and (PCardinalArray(P)^[1] and $ffdf=ord('D')+ord(':')shl 8) then begin SetID(P+6,ID); result := ID>0; exit; end; ID := 0; result := false; end; function JSONGetID(P: PUTF8Char; out ID: TID): Boolean; begin if (P<>nil) and NextNotSpaceCharIs(P,'{') then if NextNotSpaceCharIs(P,'"') then result := StartWithQuotedID(P,ID) else result := StartWithID(P,ID) else begin ID := 0; result := false; end; end; function JSONGetObject(var P: PUTF8Char; ExtractID: PID; var EndOfObject: AnsiChar; KeepIDField: boolean): RawUTF8; var Beg, PC: PUTF8Char; begin result := ''; if P=nil then exit; while (P^<=' ') and (P^<>#0) do inc(P); if P^<>'{' then exit; Beg := P; P := GotoNextJSONObjectOrArray(Beg); if (P<>nil) and not (P^ in EndOfJSONField) then P := nil; if P<>nil then begin EndOfObject := P^; inc(P); // ignore end of object, i.e. ',' or ']' if ExtractID<>nil then if JSONGetID(Beg,ExtractID^) and not KeepIDField then begin PC := PosChar(Beg,','); // ignore the '"ID":203,' pair if PC=nil then exit; PC^ := '{'; FastSetString(result,PC,P-PC-1); exit; end; FastSetString(result,Beg,P-Beg-1); end; end; { TSQLTableJSON } function TSQLTableJSON.PrivateCopyChanged(aJSON: PUTF8Char; aLen: integer): boolean; var Hash: cardinal; begin Hash := crc32c(0,pointer(aJSON),aLen); result := (fPrivateCopyHash=0) or (Hash=0) or (Hash<>fPrivateCopyHash); if not result then exit; FastSetString(fPrivateCopy,aJSON,aLen+16); // +16 for SSE4.2 read-ahead fPrivateCopyHash := Hash; end; function TSQLTableJSON.ParseAndConvert(Buffer: PUTF8Char; BufferLen: integer): boolean; function GetFieldCountExpanded(P: PUTF8Char): integer; var EndOfObject: AnsiChar; begin result := 0; repeat P := GotoNextJSONItem(P,2,@EndOfObject); // ignore Name+Value items if P=nil then begin // unexpected end result := 0; exit; end; inc(result); if EndOfObject='}' then break; // end of object until false; end; var i, max, nfield, nrow, resmax, f: integer; EndOfObject: AnsiChar; P: PUTF8Char; wasString: Boolean; begin result := false; // error on parsing fFieldIndexID := -1; if (self=nil) or (Buffer=nil) then exit; // go to start of object P := GotoNextNotSpace(Buffer); if IsNotExpandedBuffer(P,Buffer+BufferLen,fFieldCount,fRowCount) then begin // A. Not Expanded (more optimized) format as array of values (* {"fieldCount":9,"values":["ID","Int","Test","Unicode","Ansi","ValFloat","ValWord", "ValDate","Next",0,0,"abcde+?ef+?+?","abcde+?ef+?+?","abcde+?ef+?+?", 3.14159265300000E+0000,1203,"2009-03-10T21:19:36",0,..],"rowCount":20} *) // 1. check RowCount and DataLen if fRowCount<0 then begin // IsNotExpanded() notified wrong input fRowCount := 0; // may occur if P^ content was invalid exit; end; // 2. initialize and fill fResults[] PPUTF8CharArray memory max := (fRowCount+1)*FieldCount; SetLength(fJSONResults,max); fResults := @fJSONResults[0]; // unescape+zeroify JSONData + fill fResults[] to proper place dec(max); f := 0; for i := 0 to max do begin // get a field fJSONResults[i] := GetJSONFieldOrObjectOrArray(P,@wasString,nil,true); if (P=nil) and (i<>max) then exit; // failure (GetRowCountNotExpanded should have detected it) if i>=FieldCount then begin if wasString then Include(fFieldParsedAsString,f); // mark column was "string" inc(f); if f=FieldCount then f := 0; // check all rows end; end; end else begin // B. Expanded format as array of objects (each with field names) (* [{"ID":0,"Int":0,"Test":"abcde+?ef+?+?","Unicode":"abcde+?ef+?+?","Ansi": "abcde+?ef+?+?","ValFloat": 3.14159265300000E+0000,"ValWord":1203, "ValDate":"2009-03-10T21:19:36","Next":0},{..}] *) // 1. get fields count from first row while P^<>'[' do if P^=#0 then exit else inc(P); // need an array of objects repeat inc(P); if P^=#0 then exit; until P^ in ['{',']']; // go to object beginning if P^=']' then begin // [] -> void data result := true; exit; end; inc(P); nfield := GetFieldCountExpanded(P); if nField=0 then exit; // invalid data for first row // 2. get values (assume fields are always the same as in the first object) max := nfield; // index to start storing values in fResults[] resmax := nfield*2; SetLength(fJSONResults,resmax); // space for field names + 1 data row nrow := 0; repeat // let fJSONResults[] point to unescaped+zeroified JSON values for f := 0 to nfield-1 do begin if nrow=0 then // get field name from 1st Row fJSONResults[f] := GetJSONPropName(P) else P := GotoNextJSONItem(P); // ignore field name for later rows // warning: field order if not checked, and should be as expected if max>=resmax then begin // check space inside loop for GPF security resmax := NextGrow(resmax); SetLength(fJSONResults,resmax); // enough space for more rows end; if P=nil then break; // normal end: no more field name fJSONResults[max] := GetJSONFieldOrObjectOrArray(P,@wasString,@EndOfObject,true); if P=nil then begin nfield := 0; break; // unexpected end end; if wasString then // mark column was "string" Include(fFieldParsedAsString,f); if (EndOfObject='}') and (f'}' then break; // data field layout is not consistent: should never happen inc(nrow); while (P^<>'{') and (P^<>']') do // go to next object beginning if P^=#0 then exit else inc(P); if P^=']' then break else inc(P); // jmp '{' until false; if max<>(nrow+1)*nfield then begin // field count must be the same for all objects fFieldCount := 0; fRowCount := 0; exit; // data field layout is not consistent: should never happen end; // 3. save field pointers to fResults[] SetLength(fJSONResults,max); // resize to exact size fResults := @fJSONResults[0]; fFieldCount := nfield; fRowCount := nrow; end; for i := 0 to fFieldCount-1 do if IsRowID(fResults[i]) then begin fFieldIndexID := i; break; end; result := true; // if we reached here, means successfull conversion from P^ end; function TSQLTableJSON.UpdateFrom(const aJSON: RawUTF8; var Refreshed: boolean; PCurrentRow: PInteger): boolean; var len: Integer; begin len := length(aJSON); if PrivateCopyChanged(pointer(aJSON),len) then if ParseAndConvert(pointer(fPrivateCopy),len) then begin // parse success from new aJSON data -> need some other update? if Assigned(fIDColumn) then begin // ID column was hidden -> do it again Finalize(fIDColumn); IDColumnHide; end; with fSortParams do if FieldCount<>0 then // TSQLTable.SortFields() was called -> do it again SortFields(FieldIndex,Asc,PCurrentRow,FieldType); Refreshed := true; result := true; end else // parse error result := false else // data didn't change (fPrivateCopyHash checked) result := true; end; constructor TSQLTableJSON.Create(const aSQL: RawUTF8; JSONBuffer: PUTF8Char; JSONBufferLen: integer); begin // don't raise exception on error parsing inherited Create(aSQL); ParseAndConvert(JSONBuffer,JSONBufferLen); end; constructor TSQLTableJSON.Create(const aSQL, aJSON: RawUTF8); var len: integer; begin len := length(aJSON); FastSetString(fPrivateCopy,pointer(aJSON),len+16); // +16 for SSE4.2 read-ahead Create(aSQL,pointer(fPrivateCopy),len); end; constructor TSQLTableJSON.CreateFromTables(const Tables: array of TSQLRecordClass; const aSQL: RawUTF8; JSONBuffer: PUTF8Char; JSONBufferLen: integer); begin // don't raise exception on error parsing inherited CreateFromTables(Tables,aSQL); ParseAndConvert(JSONBuffer,JSONBufferLen); end; constructor TSQLTableJSON.CreateFromTables(const Tables: array of TSQLRecordClass; const aSQL, aJSON: RawUTF8); var len: integer; begin len := length(aJSON); FastSetString(fPrivateCopy,pointer(aJSON),len+16); CreateFromTables(Tables,aSQL,pointer(fPrivateCopy),len); end; constructor TSQLTableJSON.CreateWithColumnTypes(const ColumnTypes: array of TSQLFieldType; const aSQL: RawUTF8; JSONBuffer: PUTF8Char; JSONBufferLen: integer); begin // don't raise exception on error parsing inherited CreateWithColumnTypes(ColumnTypes,aSQL); ParseAndConvert(JSONBuffer,JSONBufferLen); end; constructor TSQLTableJSON.CreateWithColumnTypes(const ColumnTypes: array of TSQLFieldType; const aSQL, aJSON: RawUTF8); var len: integer; begin len := length(aJSON); FastSetString(fPrivateCopy,pointer(aJSON),len+16); CreateWithColumnTypes(ColumnTypes,aSQL,pointer(fPrivateCopy),len); end; { TSQLTableWritable } function TSQLTableWritable.AddField(const FieldName: RawUTF8): integer; var prev: TPUTF8CharDynArray; rowlen,i: integer; S,D: PByte; begin if (FieldName='') or (FieldIndex(FieldName)>=0) then raise ESQLTableException.CreateUTF8('%.AddField(%) invalid fieldname',[self,FieldName]); result := fFieldCount; inc(fFieldCount); SetLength(fFieldNames,fFieldCount); fFieldNames[result] := FieldName; fFieldNameOrder := nil; prev := fJSONResults; fJSONResults := nil; SetLength(fJSONResults,(fRowCount+1)*fFieldCount); fResults := pointer(fJSONResults); S := pointer(prev); D := pointer(fJSONResults); rowlen := result*SizeOf(pointer); MoveFast(S^,D^,rowlen); inc(S,rowlen); inc(D,rowlen); PPUTF8Char(D)^ := pointer(FieldName); inc(D,SizeOf(pointer)); for i := 1 to fRowCount do begin MoveFast(S^,D^,rowlen); inc(S,rowlen); inc(D,rowlen+SizeOf(pointer)); // leave new field value as D^=nil end; end; procedure TSQLTableWritable.Update(Row: integer; const FieldName, Value: RawUTF8); begin Update(Row,FieldIndexExisting(FieldName),Value); end; procedure TSQLTableWritable.Update(Row, Field: integer; const Value: RawUTF8); var U: PPUTF8Char; begin if (self=nil) or (fResults=nil) or (Row<=0) or (Row>fRowCount) or (cardinal(Field)>=cardinal(FieldCount)) then exit; U := @fResults[Row*FieldCount+Field]; if (U^=nil) or (StrComp(U^,pointer(Value))<>0) then if fNewValuesInterning<>nil then U^ := pointer(fNewValuesInterning.Unique(Value)) else begin AddRawUTF8(fNewValues,fNewValuesCount,Value); U^ := pointer(Value); end; end; function TSQLTableWritable.AddField(const FieldName: RawUTF8; FieldType: TSQLFieldType; FieldTypeInfo: pointer; FieldSize: integer): integer; begin result := AddField(FieldName); SetFieldType(result,FieldType,FieldTypeInfo,FieldSize); end; function TSQLTableWritable.AddField(const FieldName: RawUTF8; FieldTable: TSQLRecordClass; const FieldTableName: RawUTF8=''): integer; var prop: TSQLPropInfo; nfo: pointer; begin result := AddField(FieldName); if FieldTable=nil then exit; with FieldTable.RecordProps.Fields do if FieldTableName<>'' then prop := ByRawUTF8Name(FieldTableName) else prop := ByRawUTF8Name(FieldName); if prop = nil then exit; if prop.InheritsFrom(TSQLPropInfoRTTI) then nfo := TSQLPropInfoRTTI(prop).PropType else nfo := nil; SetFieldType(result,prop.SQLFieldTypeStored,nfo,prop.FieldWidth, PtrArrayAddOnce(fQueryTables,FieldTable)); end; {$ifndef NOVARIANTS} procedure TSQLTableWritable.Update(Row: integer; const FieldName: RawUTF8; const Value: variant); begin Update(Row,FieldIndexExisting(FieldName),Value); end; procedure TSQLTableWritable.Update(Row, Field: integer; const Value: variant); var U: RawUTF8; wasString: boolean; begin VariantToUTF8(Value,U,wasString); Update(Row,Field,U); end; {$endif NOVARIANTS} procedure TSQLTableWritable.Join(From: TSQLTable; const FromKeyField, KeyField: RawUTF8); var fk,dk,f,i,k,ndx: integer; n,fn: RawUTF8; info: PSQLTableFieldType; new: TIntegerDynArray; begin dk := FieldIndexExisting(KeyField); SetLength(new,FieldCount); fk := From.FieldIndexExisting(FromKeyField); From.SortFields(fk); // faster merge with O(log(n)) binary search for f := 0 to From.FieldCount-1 do // add From fields (excluding FromKeyField) if f<>fk then begin n := From.FieldNames[f]; fn := n; if FieldIndex(fn)>=0 then // ensure unique name for i := 2 to 100 do begin fn := n+SmallUInt32UTF8[i]; if FieldIndex(fn)<0 then break; end; if From.FieldType(f,info)=sftUnknown then // set TSQLTableFieldType i := AddField(fn) else if info.TableIndex>=0 then i := AddField(fn,From.QueryTables[info.TableIndex],n) else begin i := AddField(fn); if i>=length(fFieldType) then SetLength(fFieldType,i+1); fFieldType[i] := info^; end; new[f] := i; end; ndx := FieldCount; for i := 1 to fRowCount do begin // merge data k := From.SearchFieldSorted(fResults[ndx+dk],fk); if k>0 then begin k := k*From.FieldCount; for f := 0 to From.FieldCount-1 do if f<>fk then fResults[ndx+new[f]] := From.fResults[k+f]; // fast PUTF8Char copy end; inc(ndx,FieldCount); end; end; { TINIWriter } procedure TINIWriter.WriteObject(Value: TObject; const SubCompName: RawUTF8; WithSection, RawUTF8DynArrayAsCSV: boolean); var CT: TClass; P: PPropInfo; Obj: TObject; i: integer; begin if Value<>nil then begin if WithSection then // new TObject.ClassName is UnicodeString (Delphi 20009) -> inline code with // vmtClassName = UTF-8 encoded text stored in a shortstring = -44 Add(#13#10'[%]'#13#10,[ClassNameShort(Value)^]); CT := Value.ClassType; repeat for i := 1 to InternalClassPropInfo(CT,P) do begin if P^.PropType^.Kind=tkClass then begin // recursive serialization Obj := P^.GetObjProp(Value); if (Obj<>nil) and ClassHasPublishedFields(PPointer(Obj)^) then WriteObject(Obj,SubCompName+ToUTF8(P^.Name)+'.',false); end else begin // regular properties Add('%%=%'#13#10,[SubCompName,P^.Name]); P^.GetToText(Value,self,RawUTF8DynArrayAsCSV,twNone); AddCR; end; P := P^.Next; end; CT := GetClassParent(CT); until CT=TObject; end; end; function UTF8ContentNumberType(P: PUTF8Char): TSQLFieldType; begin if (P=nil) or ((PInteger(P)^=ord('n')+ord('u')shl 8+ord('l')shl 16+ ord('l')shl 24) and (P[4]=#0)) then result := sftUnknown else case TextToVariantNumberType(P) of varInt64: result := sftInteger; varDouble: result := sftFloat; varCurrency: result := sftCurrency; else result := sftUTF8Text; end; end; function UTF8ContentType(P: PUTF8Char): TSQLFieldType; var c,len: integer; begin if P<>nil then begin while (P^<=' ') and (P^<>#0) do inc(P); if (PInteger(P)^=NULL_LOW) and (P[4]=#0) then result := sftUnknown else // don't check for 'false' or 'true' here, since their UTF-8 value is 0/1 if P^ in ['-','0'..'9'] then case TextToVariantNumberType(P) of varInt64: result := sftInteger; varDouble: result := sftFloat; varCurrency: result := sftCurrency; else begin len := StrLen(P); if (len>15) and (Iso8601ToTimeLogPUTF8Char(P,len)<>0) then result := sftDateTime else result := sftUTF8Text; end; end else begin c := PInteger(P)^ and $00ffffff; if (c=JSON_BASE64_MAGIC) or ((P^='''') and isBlobHex(P)) then result := sftBlob else if c=JSON_SQLDATE_MAGIC then result := sftDateTime else result := sftUTF8Text; end; end else result := sftUnknown; end; { TPropInfo } function TPropInfo.ClassFromJSON(Instance: TObject; From: PUTF8Char; var Valid: boolean; Options: TJSONToObjectOptions): PUTF8Char; var Field: ^TObject; {$ifndef FPC}tmp: TObject;{$endif} begin valid := false; result := nil; if (@self=nil) or (PropType^.Kind<>tkClass) or (Instance=nil) then exit; if SetterIsField then // setter to field -> direct in-memory access Field := SetterAddr(Instance) else {$ifndef FPC} if WriteIsDefined and not (j2oSetterNoCreate in Options) then begin // it is a setter method -> create a temporary object tmp := PropType^.ClassCreate; try result := JSONToObject(tmp,From,Valid,nil,Options); if not Valid then FreeAndNil(tmp) else begin SetOrdProp(Instance,PtrInt(tmp)); // PtrInt(tmp) is OK for CPU64 if j2oSetterExpectsToFreeTempInstance in Options then FreeAndNil(tmp); end; except on Exception do tmp.Free; end; exit; end else {$endif FPC} if GetterIsField then // no setter -> use direct in-memory access from getter (if available) Field := GetterAddr(Instance) else // no setter, nor direct field offset -> impossible to set the instance exit; result := JSONToObject(Field^,From,Valid,nil,Options); end; function TPropInfo.GetOrdValue(Instance: TObject): PtrInt; begin if (Instance<>nil) and (@self<>nil) and (PropType^.Kind in [tkInteger,tkEnumeration,tkSet,{$ifdef FPC}tkBool,{$endif}tkClass]) then result := GetOrdProp(Instance) else result := -1; end; function TPropInfo.GetInt64Value(Instance: TObject): Int64; begin if (Instance<>nil) and (@self<>nil) then case PropType^.Kind of tkInteger,tkEnumeration,tkSet,tkChar,tkWChar,tkClass{$ifdef FPC},tkBool{$endif}: result := GetOrdProp(Instance); tkInt64{$ifdef FPC},tkQWord{$endif}: result := GetInt64Prop(Instance); else result := 0; end else result := 0; end; function TPropInfo.GetCurrencyValue(Instance: TObject): Currency; begin if (Instance<>nil) and (@self<>nil) and (PropType^.Kind=tkFloat) then if {$ifdef HASINLINE}PropType^.FloatType{$else} PFloatType(@PropType^.Name[ord(PropType^.Name[0])+1])^{$endif}=ftCurr then result := GetCurrencyProp(Instance) else result := GetFloatProp(Instance) else result := 0; end; function TPropInfo.GetDoubleValue(Instance: TObject): double; begin if (Instance<>nil) and (@self<>nil) and (PropType^.Kind=tkFloat) then result := GetFloatProp(Instance) else result := 0; end; procedure TPropInfo.SetDoubleValue(Instance: TObject; const Value: double); begin if (Instance<>nil) and (@self<>nil) and (PropType^.Kind=tkFloat) then SetFloatProp(Instance,Value); end; function TPropInfo.GetDynArray(Instance: TObject): TDynArray; begin result.Init(TypeInfo,GetFieldAddr(Instance)^); end; procedure TPropInfo.GetDynArray(Instance: TObject; var result: TDynArray); begin result.Init(TypeInfo,GetFieldAddr(Instance)^); end; function TPropInfo.DynArrayIsObjArray: boolean; begin if PropType^.Kind=tkDynArray then result := ObjArraySerializers.Find(TypeInfo)<>nil else result := false; end; function TPropInfo.DynArrayIsObjArrayInstance: PClassInstance; begin if PropType^.Kind<>tkDynArray then result := nil else result := TJSONSerializer.RegisterObjArrayFindType(TypeInfo); end; procedure TPropInfo.GetLongStrValue(Instance: TObject; var result: RawUTF8); var tmp: RawByteString; WS: WideString; {$ifdef HASVARUSTRING}US: UnicodeString;{$endif} cp: integer; begin if (Instance<>nil) and (@self<>nil) then case PropType^.Kind of {$ifdef FPC}tkLStringOld,{$endif} tkLString: begin GetLongStrProp(Instance,tmp); if tmp='' then result := '' else begin cp := PropType^.AnsiStringCodePage; case cp of CP_UTF8: result := tmp; CP_SQLRAWBLOB: result := TSQLRawBlobToBlob(TSQLRawBlob(tmp)); else result := TSynAnsiConvert.Engine(cp).AnsiToUTF8(tmp); end; end; end; {$ifdef HASVARUSTRING} tkUString: begin GetUnicodeStrProp(Instance,US); RawUnicodeToUtf8(pointer(US),length(US),result); end; {$endif HASVARUSTRING} tkWString: begin GetWideStrProp(Instance,WS); RawUnicodeToUtf8(pointer(WS),length(WS),result); end; else result := ''; end else result := ''; end; procedure TPropInfo.GetRawByteStringValue(Instance: TObject; var Value: RawByteString); begin if (Instance<>nil) and (@self<>nil) and (PropType^.Kind in [{$ifdef FPC}tkLStringOld,{$endif}tkLString]) then GetLongStrProp(Instance,Value) else Value := ''; end; procedure TPropInfo.SetLongStrValue(Instance: TObject; const Value: RawUTF8); procedure HandleAnsiString(Instance: TObject; const Value: RawUTF8; cp: integer); var tmp: RawByteString; begin if cp=CP_SQLRAWBLOB then tmp := BlobToTSQLRawBlob(Value) else tmp := TSynAnsiConvert.Engine(cp).UTF8ToAnsi(Value); {$ifdef FPC} // an FPC quirck ... and Alf work-around ... ;-) if cp=CP_UTF16 then begin Setlength(tmp,length(tmp)+1); tmp[length(tmp)] := #0; end; {$endif} SetLongStrProp(Instance,tmp); end; {$ifdef HASVARUSTRING} procedure HandleUnicode(Instance: TObject; const Value: RawUTF8); begin SetUnicodeStrProp(Instance,UTF8DecodeToUnicodeString(Value)); end; {$endif} procedure HandleWideString(Instance: TObject; const Value: RawUTF8); begin SetWideStrProp(Instance,UTF8ToWideString(Value)); end; var cp: integer; begin if (Instance<>nil) and (@self<>nil) then case PropType^.Kind of {$ifdef FPC}tkLStringOld,{$endif}tkLString: begin if Value<>'' then begin cp := PropType^.AnsiStringCodePage; if cp=CP_UTF8 then SetLongStrProp(Instance,Value) else HandleAnsiString(Instance,Value,cp); end else SetLongStrProp(Instance,''); end; {$ifdef HASVARUSTRING} tkUString: HandleUnicode(Instance,Value); {$endif} tkWString: HandleWideString(Instance,Value); end; end; procedure TPropInfo.CopyLongStrProp(Source,Dest: TObject); var tmp: RawByteString; begin GetLongStrProp(Source,tmp); SetLongStrProp(Dest,tmp); end; {$ifndef NOVARIANTS} procedure TPropInfo.SetFromVariant(Instance: TObject; const Value: variant); var i: integer; i64: Int64; u: RawUTF8; d: double; c: PClassInstance; begin if (Instance<>nil) and (@self<>nil) then case PropType^.Kind of tkInteger,tkEnumeration,tkSet,tkChar,tkWChar{$ifdef FPC},tkBool{$endif}: if VariantToInteger(Value,i) then SetOrdProp(Instance,i) else if VariantToUTF8(Value,u) then begin if PropType^.Kind=tkEnumeration then i := PropType^.EnumBaseType^.GetEnumNameValue(pointer(u),length(u)) else i := UTF8ToInteger(u,-1); if i>=0 then SetOrdProp(Instance,i) end; tkInt64{$ifdef FPC},tkQWord{$endif}: if VariantToInt64(Value,i64) or (VariantToUTF8(Value,u) and ToInt64(u,i64)) then SetInt64Prop(Instance,i64); {$ifdef HASVARUSTRING}tkUString,{$endif} tkLString, tkWString {$ifdef FPC},tkLStringOld{$endif}: if VariantToUTF8(Value,u) then SetLongStrValue(Instance,u); tkFloat: if VariantToDouble(Value,d) or (VariantToUTF8(Value,u) and ToDouble(u,d)) then SetFloatProp(Instance,d); tkVariant: SetVariantProp(Instance,Value); tkClass: DocVariantToObject(_Safe(Value)^,GetObjProp(Instance)); tkDynArray: begin c := TJSONSerializer.RegisterObjArrayFindType(TypeInfo); if c<>nil then DocVariantToObjArray(_Safe(Value)^,GetFieldAddr(Instance)^,c) else begin U := _Safe(Value)^.ToJSON; GetDynArray(Instance).LoadFromJSON(pointer(U)); end; end; {$ifdef PUBLISHRECORD} tkRecord{$ifdef FPC},tkObject{$endif}: begin VariantSaveJSON(Value,twJSONEscape,u); RecordLoadJSON(GetFieldAddr(Instance)^,pointer(u),TypeInfo); end; {$endif} end; end; procedure TPropInfo.GetVariant(Instance: TObject; var Dest: variant); var i: PtrInt; U: RawUTF8; begin if (Instance<>nil) and (@self<>nil) then case PropType^.Kind of tkInteger,tkEnumeration,tkSet,tkChar,tkWChar{$ifdef FPC},tkBool{$endif}: begin i := GetOrdProp(Instance); case PropType^.Kind of tkInteger,tkSet: Dest := integer(i); tkChar: Dest := AnsiChar(i); tkWChar: Dest := WideChar(i); {$ifdef FPC} tkBool: Dest := boolean(i); tkEnumeration: Dest := integer(i); {$else} tkEnumeration: if TypeInfo=system.TypeInfo(boolean) then Dest := boolean(i) else Dest := integer(i); {$endif FPC} end; end; tkInt64{$ifdef FPC},tkQWord{$endif}: Dest := GetInt64Prop(Instance); tkLString,tkWString{$ifdef HASVARUSTRING},tkUString{$endif}{$ifdef FPC},tkLStringOld{$endif}: begin GetLongStrValue(Instance,U); RawUTF8ToVariant(U,Dest); end; tkFloat: Dest := GetFloatProp(Instance); tkVariant: GetVariantProp(Instance,Dest); tkClass: ObjectToVariant(GetObjProp(Instance),Dest); tkDynArray: begin GetDynArray(Instance).SaveToJSON(U); // works for T*ObjArray and records TDocVariantData(Dest).InitJSONInPlace(pointer(U),JSON_OPTIONS_FAST); end; else VarClear(Dest); end; end; {$endif NOVARIANTS} procedure TPropInfo.SetFromText(Instance: TObject; const Text: RawUTF8; TryCustomVariants: PDocVariantOptions; AllowDouble: boolean); {$ifndef NOVARIANTS}var tmp: variant;{$endif} begin if (Instance<>nil) and (@self<>nil) then case PropType^.Kind of tkChar,tkWChar: if Text<>'' then SetOrdProp(Instance,ord(Text[1])); tkInteger,tkEnumeration,tkSet{$ifdef FPC},tkBool{$endif}: SetOrdProp(Instance,GetIntegerDef(pointer(Text),DefaultOr0)); tkInt64{$ifdef FPC},tkQWord{$endif}: SetInt64Prop(Instance,GetInt64(pointer(Text))); tkLString,{$ifdef FPC}tkLStringOld,{$endif}{$ifdef HASVARUSTRING}tkUString,{$endif}tkWString: SetLongStrValue(Instance,Text); tkFloat: if PropType^.FloatType=ftCurr then SetCurrencyProp(Instance,StrToCurrency(pointer(Text))) else SetFloatProp(Instance,GetExtended(pointer(Text))); {$ifndef NOVARIANTS} tkVariant: begin if TryCustomVariants<>nil then GetVariantFromJSON(pointer(Text),TextToVariantNumberType(pointer(Text))=varString, tmp,TryCustomVariants,{allowdouble=}true) else RawUTF8ToVariant(Text,tmp); SetVariantProp(Instance,tmp); end; {$endif NOVARIANTS} tkDynArray: if Text<>'' then if IsRawUTF8DynArray(TypeInfo) and (Text[1]<>'[') then CSVToRawUTF8DynArray(pointer(Text),PRawUTF8DynArray(GetFieldAddr(Instance))^) else GetDynArray(Instance).LoadFromJSON(pointer(Text)); // T*ObjArray and records end; end; procedure TPropInfo.GetToText(Instance: TObject; WR: TTextWriter; RawUTF8DynArrayAsCSV: boolean; Escape: TTextWriterKind); var i: integer; tmp: RawUTF8; a: PRawUTF8DynArray; da: TDynArray; {$ifndef NOVARIANTS}v: variant;{$endif} begin if (Instance<>nil) and (@self<>nil) and (WR<>nil) then case PropType^.Kind of tkInteger,tkEnumeration,tkSet,tkChar,tkWChar{$ifdef FPC},tkBool{$endif}: begin i := GetOrdProp(Instance); case PropType^.Kind of tkChar: WR.Add(AnsiChar(i)); tkWChar: WR.AddNoJSONEscapeW(@i,1); else WR.Add(i); end; end; tkInt64{$ifdef FPC},tkQWord{$endif}: WR.Add(GetInt64Prop(Instance)); tkLString,{$ifdef FPC}tkLStringOld,{$endif}{$ifdef HASVARUSTRING}tkUString,{$endif}tkWString: begin GetLongStrValue(Instance,tmp); WR.Add(pointer(tmp),length(tmp),Escape); end; tkFloat: if PropType^.FloatType=ftCurr then WR.AddCurr64(GetCurrencyProp(Instance)) else WR.AddDouble(GetFloatProp(Instance)); {$ifndef NOVARIANTS} tkVariant: begin GetVariantProp(Instance,v); WR.AddVariant(v,Escape); end; {$endif NOVARIANTS} tkDynArray: begin if RawUTF8DynArrayAsCSV and IsRawUTF8DynArray(TypeInfo) then begin a := GetFieldAddr(Instance); if (a<>nil) and (a^<>nil) then begin for i := 0 to length(a^)-1 do begin WR.AddString(a^[i]); WR.Add(','); end; WR.CancelLastComma; end; end else begin GetDynArray(Instance,da); WR.AddDynArrayJSON(da); // works for T*ObjArray and records end; end; end; end; procedure TPropInfo.SetDefaultValue(Instance: TObject; FreeAndNilNestedObjects: boolean); var obj: TObject; da: TDynArray; {$ifdef PUBLISHRECORD} addr: pointer; {$endif} begin if (Instance<>nil) and (@self<>nil) then case PropType^.Kind of tkInteger,tkEnumeration,tkSet,tkChar,tkWChar{$ifdef FPC},tkBool{$endif}: SetOrdProp(Instance,DefaultOr0); tkInt64{$ifdef FPC},tkQWord{$endif}: SetInt64Prop(Instance,0); tkLString{$ifdef FPC},tkLStringOld{$endif}: SetLongStrProp(Instance,''); {$ifdef HASVARUSTRING} tkUString: SetUnicodeStrProp(Instance,''); {$endif HASVARUSTRING} tkWString: SetWideStrProp(Instance,''); tkFloat: SetFloatProp(Instance,0); {$ifndef NOVARIANTS} tkVariant: SetVariantProp(Instance,SynCommons.Null); {$endif} tkClass: begin obj := GetObjProp(Instance); if obj<>nil then if FreeAndNilNestedObjects then begin SetOrdProp(Instance,0); // mimic FreeAndNil() obj.Free; end else ClearObject(obj,false); end; tkDynArray: begin GetDynArray(Instance,da); da.Count := 0; // will handle also any T*ObjArray end; {$ifdef PUBLISHRECORD} tkRecord{$ifdef FPC},tkObject{$endif}: begin addr := GetFieldAddr(Instance); RecordClear(addr^,TypeInfo); FillcharFast(addr^,TypeInfo^.RecordType^.Size,0); end; {$endif} end; end; function TPropInfo.GetGenericStringValue(Instance: TObject): string; var tmp: RawUTF8; begin if (Instance=nil) or (@self=nil) or not(PropType^.Kind in [{$ifdef FPC}tkLStringOld,{$endif} {$ifdef HASVARUSTRING}tkUString,{$endif} tkLString, tkWString]) then result := '' else begin GetLongStrValue(Instance,tmp); result := UTF8ToString(tmp); end; end; procedure TPropInfo.SetGenericStringValue(Instance: TObject; const Value: string); begin if (Instance<>nil) and (@self<>nil) then case PropType^.Kind of {$ifdef FPC}tkLStringOld,{$endif}tkLString, tkWString: SetLongStrValue(Instance,StringToUtf8(Value)); {$ifdef HASVARUSTRING} tkUString: SetUnicodeStrProp(Instance,UnicodeString(Value)); {$endif} end; end; {$ifdef HASVARUSTRING} function TPropInfo.GetUnicodeStrValue(Instance: TObject): UnicodeString; begin if (Instance<>nil) and (@self<>nil) and (PropType^.Kind=tkUString) then GetUnicodeStrProp(Instance,result) else result := ''; end; procedure TPropInfo.SetUnicodeStrValue(Instance: TObject; const Value: UnicodeString); begin if (Instance<>nil) and (@self<>nil) and (PropType^.Kind=tkUString) then SetUnicodeStrProp(Instance,Value); end; {$endif HASVARUSTRING} procedure TPropInfo.SetOrdValue(Instance: TObject; Value: PtrInt); begin if (Instance<>nil) and (@self<>nil) and (PropType^.Kind in [ tkInteger,tkEnumeration,tkSet,{$ifdef FPC}tkBool,{$endif}tkClass]) then SetOrdProp(Instance,Value); end; procedure TPropInfo.SetInt64Value(Instance: TObject; Value: Int64); begin if (Instance<>nil) and (@self<>nil) then case PropType^.Kind of tkInteger,tkEnumeration,tkSet,tkChar,tkWChar,tkClass{$ifdef FPC},tkBool{$endif}: SetOrdProp(Instance,Value); tkInt64{$ifdef FPC}, tkQWord{$endif}: SetInt64Prop(Instance,Value); end; end; function TPropInfo.SameValue(Source: TObject; DestInfo: PPropInfo; Dest: TObject): boolean; {$ifndef NOVARIANTS} function CompareVariants: boolean; var VS,VD: Variant; begin GetVariantProp(Source,VS); DestInfo^.GetVariantProp(Dest,VD); result := VS=VD; // rely on Variants.pas comparison end; {$endif} function CompareStrings: Boolean; var US,UD: RawUTF8; begin GetLongStrValue(Source,US); DestInfo^.GetLongStrValue(Dest,UD); result := US=UD; end; var kS,kD: TTypeKind; daS,daD: TDynArray; i: integer; begin if Source=Dest then begin result := true; exit; end; result := false; if (Source=nil) or (Dest=nil) or (@self=nil) or (DestInfo=nil) then exit; kS := PropType^.Kind; kD := DestInfo^.PropType^.Kind; if kS in tkStringTypes then if kD in tkStringTypes then result := CompareStrings else exit else if kS in tkOrdinalTypes then if kD in tkOrdinalTypes then result := GetInt64Value(Source)=DestInfo^.GetInt64Value(Dest) else exit else if kS=kD then case KS of tkClass: result := ObjectEquals(GetObjProp(Source),DestInfo^.GetObjProp(Dest)); tkFloat: begin if DestInfo^.PropType^.FloatType=PropType^.FloatType then case PropType^.FloatType of ftCurr: begin if GetterIsField and ((@self=DestInfo) or DestInfo^.GetterIsField) then result := PInt64(GetterAddr(Source))^=PInt64(DestInfo.GetterAddr(Dest))^ else result := GetCurrencyProp(Source)=DestInfo^.GetCurrencyProp(Dest); exit; end; ftDoub: begin if GetterIsField and ((@self=DestInfo) or DestInfo^.GetterIsField) then result := PInt64(GetterAddr(Source))^=PInt64(DestInfo.GetterAddr(Dest))^ else result := SynCommons.SameValue(GetDoubleProp(Source),DestInfo^.GetDoubleProp(Dest)); exit; end; end; result := SynCommons.SameValueFloat(GetFloatProp(Source),DestInfo^.GetFloatProp(Dest)); end; tkDynArray: begin GetDynArray(Source,daS); DestInfo^.GetDynArray(Dest,daD); if daS.Count=daD.Count then if DynArrayIsObjArray and ((@self=DestInfo) or DestInfo^.DynArrayIsObjArray) then begin for i := 0 to daS.Count-1 do if not ObjectEquals(PObjectArray(daS.Value^)[i],PObjectArray(daD.Value^)[i]) then exit; result := true; end else result := daD.Equals(daS); end; {$ifndef NOVARIANTS} tkVariant: result := CompareVariants; {$endif} end; end; function ClassFieldPropInstanceMatchingClass( aSearchedInstance: TObject; aSearchedClassType: TClass): TObject; var P: PPropInfo; begin result := aSearchedInstance; if (aSearchedInstance=nil) or aSearchedInstance.InheritsFrom(aSearchedClassType) then exit; P := ClassFieldPropWithParentsFromClassType(PPointer(aSearchedInstance)^,aSearchedClassType); if P<>nil then begin result := P^.GetObjProp(aSearchedInstance); if result=nil then result := aSearchedInstance; end; end; function TPropInfo.CopyToNewObject(aFrom: TObject): TObject; var aClass: TClass; aInstance: TClassInstance; begin if aFrom=nil then begin result := nil; exit; end; aClass := PropType^.ClassType^.ClassType; aInstance.Init(aClass); result := aInstance.CreateNew; try CopyObject(ClassFieldPropInstanceMatchingClass(aFrom,aClass),result); except FreeAndNil(result); // avoid memory leak if error during new instance copy end; end; procedure TPropInfo.CopyValue(Source, Dest: TObject; DestInfo: PPropInfo); var Value: RawByteString; WS: WideString; {$ifdef HASVARUSTRING} US: UnicodeString; {$endif} {$ifndef NOVARIANTS} V: variant; {$endif} S,D: TObject; kS,kD: TTypeKind; ft: TSQLFieldType; label i64, int, dst, obj, str; begin if DestInfo=nil then DestInfo := @self; if (@self=nil) or (Source=nil) or (Dest=Source) or (Dest=nil) then exit; kS := PropType^.Kind; kD := DestInfo^.PropType^.Kind; case kS of {$ifdef FPC}tkBool,{$endif} tkEnumeration, tkInteger, tkSet, tkChar, tkWChar: int: if DestInfo=@Self then SetOrdProp(Dest,GetOrdProp(Source)) else dst: if kD in tkOrdinalTypes then // use Int64 to handle e.g. cardinal DestInfo^.SetInt64Value(Dest,GetInt64Value(Source)); tkClass: begin ft := PropType^.ClassSQLFieldType; case ft of sftID: // TSQLRecord published properties (sftID) if TSQLRecord(Source).fFill.JoinedFields then // -> pre-allocated fields by Create*Joined() goto obj else // -> these are not class instances, but INTEGER reference to records goto int; sftMany, sftObject: begin // generic case: copy also class content (create instances) obj: S := GetObjProp(Source); if (DestInfo=@self) or ((kD=tkClass) and (DestInfo^.PropType^.ClassSQLFieldType=ft)) then begin D := DestInfo.GetObjProp(Dest); {$ifndef LVCL} if S.InheritsFrom(TCollection) then CopyCollection(TCollection(S),TCollection(D)) else {$endif} begin D.Free; // release previous D instance then set a new copy of S DestInfo.SetOrdProp(Dest,PtrInt(DestInfo^.CopyToNewObject(S))); end; end; end; end; end; tkInt64{$ifdef FPC}, tkQWord{$endif}: if DestInfo=@self then // works also with QWord, TID, TTimeLog, Double and Currency i64: SetInt64Prop(Dest,GetInt64Prop(Source)) else goto dst; tkFloat: if DestInfo=@self then if (PropType^.FloatType in [ftDoub,ftCurr]) and GetterIsField and SetterIsField then goto I64 else SetFloatProp(Dest,GetFloatProp(Source)) else if kD=tkFloat then DestInfo.SetFloatProp(Dest,GetFloatProp(Source)); tkLString{$ifdef FPC},tkLStringOld{$endif}: if kD=kS then begin GetLongStrProp(Source,Value); DestInfo.SetLongStrProp(Dest,Value); end else str: if kD in tkStringTypes then begin GetLongStrValue(Source,RawUTF8(Value)); DestInfo.SetLongStrValue(Dest,RawUTF8(Value)); end; {$ifdef HASVARUSTRING} tkUString: if kD=tkUString then begin GetUnicodeStrProp(Source,US); DestInfo.SetUnicodeStrProp(Dest,US); end else goto str; {$endif} tkWString: if kD=tkWString then begin GetWideStrProp(Source,WS); DestInfo.SetWideStrProp(Dest,WS); end else goto str; tkDynArray: if (DestInfo=@self) or (TypeInfo=DestInfo.TypeInfo) then DestInfo.GetDynArray(Dest).Copy(GetDynArray(Source)); tkRecord{$ifdef FPC},tkObject{$endif}: if (DestInfo=@self) or (TypeInfo=DestInfo.TypeInfo) then RecordCopy(DestInfo.GetFieldAddr(Dest)^,GetFieldAddr(Source)^,TypeInfo); {$ifndef NOVARIANTS} tkVariant: if kD=tkVariant then begin GetVariantProp(Source,V); DestInfo.SetVariantProp(Dest,V); end; {$endif} end; // note: tkString (shortstring) and tkInterface not handled end; function TPropInfo.IsBlob: boolean; begin result := (@self<>nil) and (TypeInfo=system.TypeInfo(TSQLRawBlob)); end; function TPropInfo.DefaultOr0: integer; begin if Default=NO_DEFAULT then result := 0 else result := Default; end; function TPropInfo.IsDefaultOrVoid(Instance: TObject): boolean; var p: PPointer; begin case PropType^.Kind of tkInteger,tkEnumeration,tkSet,tkChar,tkWChar{$ifdef FPC},tkBool{$endif}: result := GetOrdProp(Instance)=DefaultOr0; tkFloat: result := GetFloatProp(Instance)=0; tkInt64{$ifdef FPC},tkQWord{$endif}: result := GetInt64Prop(Instance)=0; tkLString,{$ifdef HASVARUSTRING}tkUString,{$endif}{$ifdef FPC}tkLStringOld,{$endif} tkWString,tkDynArray,tkInterface: begin p := GetFieldAddr(Instance); result := (p<>nil) and (p^=nil); end; tkVariant: begin p := GetFieldAddr(Instance); result := (p<>nil) and VarDataIsEmptyOrNull(p); end; tkClass: result := IsObjectDefaultOrVoid(GetObjProp(Instance)); else result := false; end; end; function TPropInfo.RetrieveFieldSize: integer; begin case PropType^.Kind of tkInteger,tkEnumeration,tkSet,tkChar,tkWChar{$ifdef FPC},tkBool{$endif}: result := ORDTYPE_SIZE[PropType^.OrdType]; tkFloat: case PropType^.FloatType of ftSingle: result := 4; ftExtended: result := 10; else result := 8; end; tkLString,{$ifdef HASVARUSTRING}tkUString,{$endif}{$ifdef FPC}tkLStringOld,{$endif} tkWString,tkClass,tkInterface,tkDynArray: result := SizeOf(pointer); tkInt64{$ifdef FPC},tkQWord{$endif}: result := 8; tkVariant: result := SizeOf(variant); else result := 0; end; end; function FromOrdType(o: TOrdType; P: pointer): PtrInt; {$ifdef HASINLINE}inline;{$endif} // shared by GetOrdProp/GetEnumName begin case o of otSByte: result := PShortInt(P)^; otSWord: result := PSmallInt(P)^; otSLong: result := PInteger(P)^; otUByte: result := PByte(P)^; otUWord: result := PWord(P)^; otULong: result := PCardinal(P)^; {$ifdef FPC_NEWRTTI} otSQWord, otUQWord: result := PInt64(P)^; {$endif} else result := 0; // should not happen end; end; procedure ToOrdType(o: TOrdType; P: pointer; Value: PtrInt); {$ifdef HASINLINE}inline;{$endif} begin case o of otUByte,otSByte: PByte(P)^ := Value; otUWord,otSWord: PWord(P)^ := Value; otULong,otSLong: PCardinal(P)^ := Value; {$ifdef FPC_NEWRTTI}otSQWord, otUQWord: PInt64(P)^ := Value;{$endif} end; end; function TPropInfo.IsStored(Instance: TObject): boolean; type TGetProc = function: boolean of object; TGetIndexed = function(Index: integer): boolean of object; var pt: byte; call: TMethod; begin pt := {$ifdef FPC}(PropProcs shr 4)and 3{$else}PropWrap(StoredProc).Kind{$endif}; if {$ifdef FPC}pt=ptConst{$else}(StoredProc and (not PtrInt($ff)))=0{$endif} then result := boolean(StoredProc) else begin case pt of ptField: begin result := PBoolean(PtrUInt(Instance)+StoredProc{$ifndef FPC}and $00ffffff{$endif})^; exit; end; ptVirtual: call.Code := PPointer(PPtrUInt(Instance)^+{$ifndef FPC}word{$endif}(StoredProc))^; else call.Code := pointer(StoredProc); end; call.Data := Instance; if {$ifdef FPC}(PropProcs shr 6)and 1{$else}Index{$endif}<>NO_INDEX then result := TGetIndexed(call)(Index) else result := TGetProc(call); end; end; function TPropInfo.GetObjProp(Instance: TObject): TObject; type TGetProc = function: TObject of object; TGetIndexed = function(Index: Integer): TObject of object; var call: TMethod; begin case Getter(Instance,@call) of picField: result := PObject(call.Data)^; picMethod: result := TGetProc(call); picIndexed: result := TGetIndexed(call)(Index); else result := nil; end; end; function TPropInfo.GetOrdProp(Instance: TObject): PtrInt; type TGetProc = function: Pointer of object; // pointer result is a PtrInt register TGetIndexed = function(Index: Integer): Pointer of object; var call: TMethod; begin case Getter(Instance,@call) of picField: call.Code := PPointer(call.Data)^; picMethod: call.Code := TGetProc(call); picIndexed: call.Code := TGetIndexed(call)(Index); else call.Code := nil; // call.Code is used to store the raw value end; if PropType^.Kind in [tkClass,tkDynArray,tkInterface] then result := PtrInt(call.Code) else result := FromOrdType({$ifdef HASINLINE}PropType^.OrdType{$else} POrdType(@PropType^.Name[ord(PropType^.Name[0])+1])^{$endif},@call.Code); end; procedure TPropInfo.SetOrdProp(Instance: TObject; Value: PtrInt); type TSetProc = procedure(Value: PtrInt) of object; TSetIndexed = procedure(Index: integer; Value: PtrInt) of object; var call: TMethod; begin case Setter(Instance,@call) of picField: if PropType^.Kind=tkClass then PPTrInt(call.Data)^ := Value else ToOrdType(PropType^.OrdType,call.Data,Value); picMethod: TSetProc(call)(Value); picIndexed: TSetIndexed(call)(Index,Value); end; end; function TPropInfo.GetInt64Prop(Instance: TObject): Int64; type TGetProc = function: Int64 of object; TGetIndexed = function(Index: Integer): Int64 of object; var call: TMethod; begin case Getter(Instance,@call) of picField: result := PInt64(call.Data)^; picMethod: result := TGetProc(call); picIndexed: result := TGetIndexed(call)(Index); else result := 0; end; end; procedure TPropInfo.SetInt64Prop(Instance: TObject; const Value: Int64); type TSetProc = procedure(Value: Int64) of object; TSetIndexed = procedure(Index: integer; Value: Int64) of object; var call: TMethod; begin case Setter(Instance,@call) of picField: PInt64(call.Data)^ := Value; picMethod: TSetProc(call)(Value); picIndexed: TSetIndexed(call)(Index,Value); end; end; procedure TPropInfo.GetShortStrProp(Instance: TObject; var Value: RawByteString); type TGetProc = function: ShortString of object; TGetIndexed = function(Index: Integer): ShortString of object; var call: TMethod; tmp: ShortString; begin case Getter(Instance,@call) of picField: tmp := PShortString(call.Data)^; picMethod: tmp := TGetProc(call); picIndexed: tmp := TGetIndexed(call)(Index); else tmp := ''; end; ShortStringToAnsi7String(tmp,RawUTF8(Value)); end; // no SetShortStrProp() by now procedure TPropInfo.GetLongStrProp(Instance: TObject; var Value: RawByteString); procedure SubProc(pic: TPropInfoCall; const call: TMethod); type TGetProc = function: RawByteString of object; TGetIndexed = function(Index: Integer): RawByteString of object; begin case pic of picMethod: value := TGetProc(call); picIndexed: value := TGetIndexed(call)(Index); else value := ''; end; end; var pic: TPropInfoCall; call: TMethod; begin pic := Getter(Instance,@call); if pic=picField then value := PRawByteString(call.Data)^ else SubProc(pic,call); // avoid try..finally end; procedure TPropInfo.SetLongStrProp(Instance: TObject; const Value: RawByteString); type TSetProc = procedure(const Value: RawByteString) of object; TSetIndexed = procedure(Index: integer; const Value: RawByteString) of object; var call: TMethod; begin case Setter(Instance,@call) of picField: PRawByteString(call.Data)^ := Value; picMethod: TSetProc(call)(Value); picIndexed: TSetIndexed(call)(Index,Value); end; end; procedure TPropInfo.GetWideStrProp(Instance: TObject; var Value: WideString); type TGetProc = function: WideString of object; TGetIndexed = function(Index: Integer): WideString of object; var call: TMethod; begin case Getter(Instance,@call) of picField: value := PWideString(call.Data)^; picMethod: value := TGetProc(call); picIndexed: value := TGetIndexed(call)(Index); else value := ''; end; end; procedure TPropInfo.SetWideStrProp(Instance: TObject; const Value: WideString); type TSetProc = procedure(const Value: WideString) of object; TSetIndexed = procedure(Index: integer; const Value: WideString) of object; var call: TMethod; begin case Setter(Instance,@call) of picField: PWideString(call.Data)^ := Value; picMethod: TSetProc(call)(Value); picIndexed: TSetIndexed(call)(Index,Value); end; end; {$ifdef HASVARUSTRING} procedure TPropInfo.GetUnicodeStrProp(Instance: TObject; var Value: UnicodeString); procedure SubProc(pic: TPropInfoCall; const call: TMethod); // avoid try..finally type TGetProc = function: UnicodeString of object; TGetIndexed = function(Index: Integer): UnicodeString of object; begin case pic of picMethod: Value := TGetProc(call); picIndexed: Value := TGetIndexed(call)(Index); else Value := ''; end; end; var pic: TPropInfoCall; call: TMethod; begin pic := Getter(Instance,@call); if pic=picField then Value := PUnicodeString(call.Data)^ else SubProc(pic,call); end; procedure TPropInfo.SetUnicodeStrProp(Instance: TObject; const Value: UnicodeString); type TSetProc = procedure(const Value: UnicodeString) of object; TSetIndexed = procedure(Index: integer; const Value: UnicodeString) of object; var call: TMethod; begin case Setter(Instance,@call) of picField: PUnicodeString(call.Data)^ := Value; picMethod: TSetProc(call)(Value); picIndexed: TSetIndexed(call)(Index,Value); end; end; {$endif HASVARUSTRING} function TPropInfo.GetCurrencyProp(Instance: TObject): currency; type TGetProc = function: currency of object; TGetIndexed = function(Index: Integer): currency of object; var call: TMethod; begin case Getter(Instance,@call) of picField: result := PCurrency(call.Data)^; picMethod: result := TGetProc(call); picIndexed: result := TGetIndexed(call)(Index); else result := 0; end; end; procedure TPropInfo.SetCurrencyProp(Instance: TObject; const Value: Currency); type TSetProc = procedure(const Value: currency) of object; TSetIndexed = procedure(Index: integer; const Value: currency) of object; var call: TMethod; begin case Setter(Instance,@call) of picField: PCurrency(call.Data)^ := Value; picMethod: TSetProc(call)(Value); picIndexed: TSetIndexed(call)(Index,Value); end; end; function TPropInfo.GetDoubleProp(Instance: TObject): double; type TGetProc = function: double of object; TGetIndexed = function(Index: Integer): double of object; var call: TMethod; begin case Getter(Instance,@call) of picField: result := unaligned(PDouble(call.Data)^); picMethod: result := TGetProc(call); picIndexed: result := TGetIndexed(call)(Index); else result := 0; end; end; procedure TPropInfo.SetDoubleProp(Instance: TObject; Value: Double); type TSetProc = procedure(const Value: double) of object; TSetIndexed = procedure(Index: integer; const Value: double) of object; var call: TMethod; begin case Setter(Instance,@call) of picField: unaligned(PDouble(call.Data)^) := Value; picMethod: TSetProc(call)(Value); picIndexed: TSetIndexed(call)(Index,Value); end; end; function TPropInfo.GetFloatProp(Instance: TObject): double; type TSingleProc = function: Single of object; TSingleIndexed = function(Index: Integer): Single of object; TDoubleProc = function: Double of object; TDoubleIndexed = function(Index: Integer): Double of object; TExtendedProc = function: Extended of object; TExtendedIndexed = function(Index: Integer): Extended of object; TCurrencyProc = function: Currency of object; TCurrencyIndexed = function(Index: Integer): Currency of object; var call: TMethod; ft: TFloatType; begin result := 0; ft := {$ifdef HASINLINE}PropType^.FloatType{$else} PFloatType(@PropType^.Name[ord(PropType^.Name[0])+1])^{$endif}; case Getter(Instance,@call) of picField: case ft of ftSingle: result := PSingle(call.Data)^; ftDoub: result := unaligned(PDouble(call.Data)^); ftExtended: result := PExtended(call.Data)^; ftCurr: result := PCurrency(call.Data)^; end; picMethod: case ft of ftSingle: result := TSingleProc(call); ftDoub: result := TDoubleProc(call); ftExtended: result := TExtendedProc(call); ftCurr: result := TCurrencyProc(call); end; picIndexed: case ft of ftSingle: result := TSingleIndexed(call)(Index); ftDoub: result := TDoubleIndexed(call)(Index); ftExtended: result := TExtendedIndexed(call)(Index); ftCurr: result := TCurrencyIndexed(call)(Index); end; end; end; procedure TPropInfo.SetFloatProp(Instance: TObject; Value: TSynExtended); type TSingleProc = procedure(const Value: Single) of object; TSingleIndexed = procedure(Index: integer; const Value: Single) of object; TDoubleProc = procedure(const Value: double) of object; TDoubleIndexed = procedure(Index: integer; const Value: double) of object; TExtendedProc = procedure(const Value: Extended) of object; TExtendedIndexed = procedure(Index: integer; const Value: Extended) of object; TCurrencyProc = procedure(const Value: Currency) of object; TCurrencyIndexed = procedure(Index: integer; const Value: Currency) of object; var call: TMethod; ft: TFloatType; begin ft := {$ifdef HASINLINE}PropType^.FloatType{$else} PFloatType(@PropType^.Name[ord(PropType^.Name[0])+1])^{$endif}; case Setter(Instance,@call) of picField: case ft of ftSingle: PSingle(call.Data)^ := Value; ftDoub: unaligned(PDouble(call.Data)^) := Value; ftExtended: PExtended(call.Data)^ := Value; ftCurr: PCurrency(call.Data)^ := Value; end; picMethod: case ft of ftSingle: TSingleProc(call)(Value); ftDoub: TDoubleProc(call)(Value); ftExtended: TExtendedProc(call)(Value); ftCurr: TCurrencyProc(call)(Value); end; picIndexed: case ft of ftSingle: TSingleIndexed(call)(Index,Value); ftDoub: TDoubleIndexed(call)(Index,Value); ftExtended: TExtendedIndexed(call)(Index,Value); ftCurr: TCurrencyIndexed(call)(Index,Value); end; end; end; {$ifndef NOVARIANTS} procedure TPropInfo.GetVariantProp(Instance: TObject; var result: Variant); procedure SubProc(pic: TPropInfoCall; const call: TMethod); // avoid try..finally type TGetProc = function: variant of object; TGetIndexed = function(Index: Integer): variant of object; begin case pic of picMethod: result := TGetProc(call); picIndexed: result := TGetIndexed(call)(Index); else result := ''; end; end; var pic: TPropInfoCall; call: TMethod; begin pic := Getter(Instance,@call); if pic=picField then SetVariantByValue(PVariant(call.Data)^,result) else SubProc(pic,call); end; procedure TPropInfo.SetVariantProp(Instance: TObject; const Value: Variant); type TSetProc = procedure(const Value: variant) of object; TSetIndexed = procedure(Index: integer; const Value: variant) of object; var call: TMethod; begin case Setter(Instance,@call) of picField: PVariant(call.Data)^ := Value; picMethod: TSetProc(call)(Value); picIndexed: TSetIndexed(call)(Index,Value); end; end; {$endif NOVARIANTS} { TEnumType } {$ifdef FPC_ENUMHASINNER} function TEnumType.MinValue: Longint; begin result := inner.iMinValue; end; function TEnumType.MaxValue: Longint; begin result := inner.iMaxValue; end; function TEnumType.BaseType: PPTypeInfo; begin result := inner.iBaseType; end; {$endif FPC_ENUMHASINNER} function TEnumType.GetEnumName(const Value): PShortString; begin result := GetEnumNameOrd(FromOrdType(OrdType,@Value)); end; function TEnumType.GetEnumNameOrd(Value: Integer): PShortString; // note: FPC doesn't align NameList (cf. GetEnumName() function in typinfo.pp) {$ifdef PUREPASCAL} begin result := @NameList; if cardinal(Value)<=cardinal(MaxValue) then while Value>0 do begin inc(PByte(result),ord(result^[0])+1); dec(Value); end else result := @NULL_SHORTSTRING; end; {$else} {$ifdef FPC} nostackframe; assembler; {$endif} asm // eax=PEnumType edx=Value xor ecx, ecx {$ifdef FPC_ENUMHASINNER} cmp edx, [eax].TEnumType.inner.iMaxValue {$else} cmp edx, [eax].TEnumType.MaxValue {$endif} lea eax, [eax].TEnumType.NameList ja @0 test edx, edx jz @z push edx shr edx, 2 // fast pipelined by-four scanning jz @1 @4: dec edx movzx ecx, byte ptr[eax] lea eax, [eax + ecx + 1] movzx ecx, byte ptr[eax] lea eax, [eax + ecx + 1] movzx ecx, byte ptr[eax] lea eax, [eax + ecx + 1] movzx ecx, byte ptr[eax] lea eax, [eax + ecx + 1] jnz @4 pop edx and edx, 3 jnz @s @z: ret @1: pop edx @s: movzx ecx, byte ptr[eax] dec edx lea eax, [eax + ecx + 1] // next short string jnz @s ret @0: lea eax, NULL_SHORTSTRING end; {$endif} function TEnumType.GetSetNameCSV(Value: integer; SepChar: AnsiChar; FullSetsAsStar: boolean): RawUTF8; var W: TTextWriter; temp: TTextWriterStackBuffer; begin W := TTextWriter.CreateOwnedStream(temp); try GetSetNameCSV(W,Value,SepChar,FullSetsAsStar); W.SetText(result); finally W.Free; end; end; procedure TEnumType.GetSetNameCSV(W: TTextWriter; Value: integer; SepChar: AnsiChar; FullSetsAsStar: boolean); var j: integer; PS: PShortString; begin W.Add('['); if FullSetsAsStar and GetAllBits(Value,MaxValue+1) then W.AddShort('"*"') else begin PS := @NameList; for j := MinValue to MaxValue do begin if GetBitPtr(@Value,j) then begin W.Add('"'); if twoTrimLeftEnumSets in W.CustomOptions then W.AddTrimLeftLowerCase(PS) else W.AddShort(PS^); W.Add('"',SepChar); end; inc(PByte(PS),ord(PS^[0])+1); // next item end; end; W.CancelLastComma; W.Add(']'); end; function TEnumType.GetSetNameAsDocVariant(Value: integer; FullSetsAsStar: boolean): variant; var j: integer; PS: PShortString; arr: TDocVariantData; begin arr.InitFast; if FullSetsAsStar and GetAllBits(Value,MaxValue+1) then arr.AddItem('*') else begin PS := @NameList; for j := MinValue to MaxValue do begin if GetBitPtr(@Value,j) then arr.AddItem(PS^); inc(PByte(PS),ord(PS^[0])+1); // next item end; end; result := variant(arr); end; function TEnumType.GetEnumNameValue(Value: PUTF8Char; ValueLen: integer; AlsoTrimLowerCase: boolean): Integer; begin if (Value<>nil) and (ValueLen>0) then begin result := FindShortStringListExact(@NameList, MaxValue,Value,ValueLen); if (result<0) and AlsoTrimLowerCase then result := FindShortStringListTrimLowerCase(@NameList, MaxValue,Value,ValueLen); end else result := -1; end; function TEnumType.GetEnumNameValue(const EnumName: ShortString): Integer; begin result := GetEnumNameValue(@EnumName[1],ord(EnumName[0])); end; function TEnumType.GetEnumNameValue(Value: PUTF8Char): Integer; begin result := GetEnumNameValue(Value,StrLen(Value)); end; {$ifdef HASINLINE} function TEnumType.GetEnumNameTrimed(const Value): RawUTF8; begin result := TrimLeftLowerCaseShort(GetEnumName(Value)); end; {$else} {$ifdef PUREPASCAL} function TEnumType.GetEnumNameTrimed(const Value): RawUTF8; begin result := TrimLeftLowerCaseShort(GetEnumName(Value)); end; {$else} function TEnumType.GetEnumNameTrimed(const Value): RawUTF8; asm push ecx call TEnumType.GetEnumName pop edx jmp TrimLeftLowerCaseShort end; {$endif} {$endif} function TEnumType.GetCaption(const Value): string; // GetCaptionFromPCharLen() expect ASCIIz -> use temp RawUTF8 begin GetCaptionFromPCharLen(pointer(GetEnumNameTrimed(Value)),result); end; procedure TEnumType.GetEnumNameTrimedAll(var result: RawUTF8; const Prefix: RawUTF8; quotedValues: boolean; const Suffix: RawUTF8); begin GetEnumNameAll(result,Prefix,quotedValues,Suffix,{trimed=}true); end; function TEnumType.GetEnumNameAllAsJSONArray(TrimLeftLowerCase, UnCamelCased: boolean): RawUTF8; begin GetEnumNameAll(result,'[',{quoted=}true,']',TrimLeftLowerCase,UnCamelCased); end; procedure TEnumType.GetEnumNameAll(var result: RawUTF8; const Prefix: RawUTF8; quotedValues: boolean; const Suffix: RawUTF8; trimedValues, unCamelCased: boolean); var i: integer; V: PShortString; uncamel: string; temp: TTextWriterStackBuffer; begin with TTextWriter.CreateOwnedStream(temp) do try AddString(Prefix); V := @NameList; for i := MinValue to MaxValue do begin if quotedValues then Add('"'); if unCamelCased then begin GetCaptionFromTrimmed(V,uncamel); AddNoJSONEscapeString(uncamel); end else if trimedValues then AddTrimLeftLowerCase(V) else AddShort(V^); if quotedValues then Add('"'); Add(','); inc(PByte(V),length(V^)+1); end; CancelLastComma; AddString(Suffix); SetText(result); finally Free; end; end; procedure TEnumType.GetEnumNameAll(var result: TRawUTF8DynArray; TrimLeftLowerCase: boolean); var max,i: integer; V: PShortString; begin Finalize(result); max := MaxValue-MinValue; SetLength(result,max+1); V := @NameList; for i := 0 to max do begin if TrimLeftLowerCase then result[i] := TrimLeftLowerCaseShort(V) else result[i] := RawUTF8(V^); inc(PByte(V),length(V^)+1); end; end; procedure TEnumType.AddCaptionStrings(Strings: TStrings; UsedValuesBits: Pointer); var i, L: PtrInt; Line: array[byte] of AnsiChar; P: PAnsiChar; V: PShortString; s: string; begin if @self=nil then exit; {$ifndef LVCL} Strings.BeginUpdate; try {$endif} V := @NameList; for i := MinValue to MaxValue do begin if (UsedValuesBits=nil) or GetBitPtr(UsedValuesBits,i) then begin L := ord(V^[0]); P := @V^[1]; while (L>0) and (P^ in ['a'..'z']) do begin // ignore left lowercase chars inc(P); dec(L); end; if L=0 then begin L := ord(V^[0]); P := @V^[1]; end; Line[L] := #0; // GetCaptionFromPCharLen() expect it as ASCIIZ MoveFast(P^,Line,L); GetCaptionFromPCharLen(Line,s); Strings.AddObject(s,pointer(i)); end; inc(PByte(V),length(V^)+1); end; {$ifndef LVCL} finally Strings.EndUpdate; end; {$endif} end; function TEnumType.GetCaptionStrings(UsedValuesBits: Pointer=nil): string; var List: TStringList; begin List := TStringList.Create; try AddCaptionStrings(List,UsedValuesBits); result := List.Text; finally List.Free; end; end; function TEnumType.GetEnumNameTrimedValue(const EnumName: ShortString): Integer; begin result := GetEnumNameTrimedValue(@EnumName[1],ord(EnumName[0])); end; function TEnumType.GetEnumNameTrimedValue(Value: PUTF8Char; ValueLen: integer): Integer; begin if Value=nil then result := -1 else begin if ValueLen=0 then ValueLen := StrLen(Value); result := FindShortStringListTrimLowerCase(@NameList,MaxValue,Value,ValueLen); if result<0 then result := FindShortStringListExact(@NameList,MaxValue,Value,ValueLen); end; end; function TEnumType.SizeInStorageAsEnum: Integer; begin result := ORDTYPE_SIZE[OrdType]; // MaxValue does not work e.g. with WordBool end; procedure TEnumType.SetEnumFromOrdinal(out Value; Ordinal: Integer); begin ToOrdType(OrdType,@Value,Ordinal); // MaxValue does not work e.g. with WordBool end; function TEnumType.SizeInStorageAsSet: Integer; begin case MaxValue of 0..7: result := 1; 8..15: result := 2; 16..31: result := 4; else result := 0; end; end; { TTypeInfo - not inlined methods } function TTypeInfo.ClassCreate: TObject; var instance: TClassInstance; begin instance.Init(ClassType^.ClassType); result := instance.CreateNew; end; function TTypeInfo.ClassFieldCount(onlyWithoutGetter: boolean): integer; begin result := ClassFieldCountWithParents(ClassType^.ClassType,onlyWithoutGetter); end; function TTypeInfo.ClassSQLFieldType: TSQLFieldType; const T_: array[0..5{$ifndef LVCL}+1{$endif}] of TClass = ( TSQLRecordMany,TSQLRecord,TRawUTF8List,TStrings,TObjectList,TObject {$ifndef LVCL},TCollection{$endif}); var CT: PClassType; C: TClass; T: PClassArray; begin CT := AlignTypeData(PAnsiChar(@Name[1])+ord(Name[0])); // inlined ClassType C := CT^.ClassType; T := @T_; result := sftUnknown; while true do // unrolled several InheritsFrom() calls if C<>T[0] then if C<>T[1] then if (C<>T[2]) and (C<>T[3]) and (C<>T[4]){$ifndef LVCL}and (C<>T[6]){$endif} then if CT^.ParentInfo<>nil then begin if CT^.PropCount>0 then result := sftObject; // identify any class with published properties {$ifdef HASDIRECTTYPEINFO} with PTypeInfo(CT^.ParentInfo)^ do {$else} // circumvent weird Delphi inlining compiler random bug with CT^.ParentInfo^^ do {$endif} // get parent ClassType CT := AlignTypeData(PAnsiChar(@Name[1])+ord(Name[0])); C := CT^.ClassType; if C<>T[5] then continue else break; end else break else begin result := sftObject; // TStrings,TObjectList,TRawUTF8List or TCollection break; end else begin result := sftID; // TSQLRecord field is pointer(RecordID), not an Instance break; end else begin result := sftMany; // no data is stored here, but in a pivot table break; end; end; function TTypeInfo.InheritsFrom(AClass: TClass): boolean; {$ifdef FPC_OR_PUREPASCAL} var CT: PClassType; begin CT := ClassType; repeat if CT^.ClassType={$ifndef FPC}pointer{$endif}(AClass) then begin result := true; exit; end; if CT^.ParentInfo = nil then break else CT := CT^.ParentInfo^.ClassType; until CT = nil; result := false; end; {$else} asm // eax=PClassType edx=AClass @1: movzx ecx, byte ptr[eax].TTypeInfo.Name lea eax, [eax + ecx].TTypeInfo.Name[1] cmp edx, [eax].TClassType.ClassType jz @2 mov eax, [eax].TClassType.ParentInfo test eax, eax jz @3 // no parent mov eax, [eax] // get parent type info jmp @1 @3: rep ret @2: mov eax, 1 end; {$endif} function TTypeInfo.GetSQLFieldType: TSQLFieldType; begin // very fast, thanks to the TypeInfo() compiler-generated function case Kind of tkInteger: begin result := sftInteger; // works also for otSQWord,otUQWord exit; // direct exit is faster in generated asm code end; tkInt64: if (@self=TypeInfo(TRecordReference)) or (@self=TypeInfo(TRecordReferenceToBeDeleted)) then begin result := sftRecord; exit; end else if @self=TypeInfo(TCreateTime) then begin result := sftCreateTime; exit; end else if @self=TypeInfo(TModTime) then begin result := sftModTime; exit; end else if @self=TypeInfo(TTimeLog) then begin result := sftTimeLog; exit; end else if @self=TypeInfo(TUnixTime) then begin result := sftUnixTime; exit; end else if @self=TypeInfo(TUnixMSTime) then begin result := sftUnixMSTime; exit; end else if @self=TypeInfo(TID) then begin result := sftTID; exit; end else if @self=TypeInfo(TSessionUserID) then begin result := sftSessionUserID; exit; end else if @self=TypeInfo(TRecordVersion) then begin result := sftRecordVersion; exit; end else if (ord(Name[1]) and $df=ord('T')) and // T...ID pattern in type name -> TID (PWord(@Name[ord(Name[0])-1])^ and $dfdf=ord('I')+ord('D') shl 8) then begin result := sftTID; exit; end else begin result := sftInteger; exit; end; {$ifdef FPC} tkBool: begin result := sftBoolean; exit; end; tkQWord: begin result := sftInteger; exit; end; {$endif FPC} tkSet: begin result := sftSet; exit; end; tkEnumeration: {$ifndef FPC} if @self=TypeInfo(Boolean) then begin result := sftBoolean; exit; end else {$endif FPC} if @self=TypeInfo(WordBool) then begin // circumvent a Delphi RTTI bug result := sftBoolean; exit; end else begin result := sftEnumerate; exit; end; tkFloat: if @self=TypeInfo(Currency) then begin result := sftCurrency; exit; end else if @self=TypeInfo(TDateTime) then begin result := sftDateTime; exit; end else if @self=TypeInfo(TDateTimeMS) then begin result := sftDateTimeMS; exit; end else begin result := sftFloat; exit; end; {$ifdef FPC}tkLStringOld,{$endif} tkLString: // do not use AnsiStringCodePage since AnsiString = GetAcp may change if (@self=TypeInfo(TSQLRawBlob)) or (@self=TypeInfo(RawByteString)) then begin result := sftBlob; exit; end else if @self=TypeInfo(WinAnsiString) then begin result := sftAnsiText; exit; end else begin result := sftUTF8Text; // CP_UTF8,CP_UTF16 and any other to UTF-8 text exit; end; {$ifdef HASVARUSTRING}tkUString,{$endif} tkChar, tkWChar, tkWString: begin result := sftUTF8Text; exit; end; tkDynArray: begin result := sftBlobDynArray; exit; end; {$ifdef PUBLISHRECORD} tkRecord{$ifdef FPC},tkObject{$endif}: begin result := sftUTF8Custom; exit; end; {$endif} {$ifndef NOVARIANTS} tkVariant: begin // this function does not need to handle sftNullable result := sftVariant; exit; end; {$endif NOVARIANTS} tkClass: begin result := ClassSQLFieldType; exit; end; // note: tkString (shortstring) and tkInterface not handled else begin result := sftUnknown; exit; end; end; end; function TTypeInfo.EnumBaseType: PEnumType; {$ifdef FPC} var base: PTypeInfo; begin result := GetTypeData(self); if Kind=tkBool then exit; // circumvent diverse RTTI encoding base := DeRef(result^.BaseType); if (base<>nil) and (base<>@self) then // no redirection if already the base type result := GetTypeData(base^); end; {$else} {$ifdef HASINLINENOTX86} begin with PEnumType(@Name[ord(Name[0])+1])^.BaseType^^ do result := @Name[ord(Name[0])+1]; end; {$else} asm // very fast code movzx edx, byte ptr[eax].TTypeInfo.Name mov eax, [eax + edx].TTypeInfo.Name[1].TEnumType.BaseType mov eax, [eax] movzx edx, byte ptr[eax].TTypeInfo.Name lea eax, [eax + edx].TTypeInfo.Name[1] end; {$endif} {$endif FPC} function TTypeInfo.SetEnumType: PEnumType; begin if (@self=nil) or (Kind<>tkSet) then result := nil else {$ifdef FPC} result := PTypeInfo(GetSetBaseEnum(@self))^.EnumBaseType; {$else} result := PPTypeInfo(PPointer(PtrUInt(@Name[ord(Name[0])+1])+SizeOf(TOrdType))^)^.EnumBaseType; {$endif FPC} end; function TTypeInfo.DynArrayItemType(aDataSize: PInteger): PTypeInfo; begin if @self=nil then result := nil else result := DynArrayTypeInfoToRecordInfo(@self,aDataSize); end; function TTypeInfo.DynArrayItemSize: integer; begin if @self=nil then result := 0 else DynArrayTypeInfoToRecordInfo(@self,@result); end; function TTypeInfo.DynArraySQLFieldType: TSQLFieldType; var item: mORMot.PTypeInfo; begin if @self=nil then result := sftUnknown else begin item := DynArrayTypeInfoToRecordInfo(@self); if item=nil then result := sftUnknown else result := item^.GetSQLFieldType; end; end; function TTypeInfo.AnsiStringCodePage: integer; begin {$ifdef HASCODEPAGE} if @self=TypeInfo(TSQLRawBlob) then result := CP_SQLRAWBLOB else if Kind=tkLString then // has tkLStringOld any codepage? -> UTF-8 result := PWord(GetTypeData(self))^ else {$else} if @self=TypeInfo(RawUTF8) then result := CP_UTF8 else if @self=TypeInfo(WinAnsiString) then result := CODEPAGE_US else if @self=TypeInfo(RawUnicode) then result := CP_UTF16 else if @self=TypeInfo(TSQLRawBlob) then result := CP_SQLRAWBLOB else if @self=TypeInfo(RawByteString) then result := CP_RAWBYTESTRING else if @self=TypeInfo(AnsiString) then result := CP_ACP else {$endif HASCODEPAGE} result := CP_UTF8; // default is UTF-8 end; function TTypeInfo.InterfaceGUID: PGUID; begin if (@self=nil) or (Kind<>tkInterface) then result := nil else result := @InterfaceType^.IntfGuid; end; function TTypeInfo.InterfaceUnitName: PShortString; begin if (@self=nil) or (Kind<>tkInterface) then result := @NULL_SHORTSTRING else result := @InterfaceType^.IntfUnit; end; function TTypeInfo.InterfaceAncestor: PTypeInfo; begin if (@self=nil) or (Kind<>tkInterface) then result := nil else result := Deref(InterfaceType^.IntfParent); end; procedure TTypeInfo.InterfaceAncestors(out Ancestors: PTypeInfoDynArray; OnlyImplementedBy: TInterfacedObjectClass; out AncestorsImplementedEntry: TPointerDynArray); var n: integer; nfo: PTypeInfo; typ: PInterfaceTypeData; entry: pointer; begin if (@self=nil) or (Kind<>tkInterface) then exit; n := 0; typ := InterfaceType; repeat if typ^.IntfParent=nil then exit; nfo := Deref(typ^.IntfParent); if nfo=TypeInfo(IInterface) then exit; typ := nfo^.InterfaceType; if ifHasGuid in typ^.IntfFlags then begin if OnlyImplementedBy<>nil then begin entry := OnlyImplementedBy.GetInterfaceEntry(typ^.IntfGuid); if entry=nil then continue; SetLength(AncestorsImplementedEntry,n+1); AncestorsImplementedEntry[n] := entry; end; SetLength(Ancestors,n+1); Ancestors[n] := nfo; inc(n); end; until false; end; {$ifdef FPC_PROVIDE_ATTR_TABLE} function TTypeInfo.AttributeTable: PFPCAttributeTable; begin result := GetTypeDataClean(self); end; {$endif FPC_PROVIDE_ATTR_TABLE} { TClassProp } function TClassProp.FieldProp(const PropName: shortstring): PPropInfo; var i: integer; begin if @self<>nil then begin result := @PropList; for i := 1 to PropCount do if IdemPropName(result^.Name,PropName) then exit else result := result^.Next; end; result := nil; end; { TClassType } function TClassType.ClassProp: PClassProp; begin if pointer(@self)<>nil then result := AlignToPtr(@UnitName[ord(UnitName[0])+1]) else result := nil; // avoid GPF end; function TClassType.RTTISize: integer; var C: PClassProp; P: PPropInfo; i: Integer; begin result := 0; C := ClassProp; if C=nil then exit; P := @C^.PropList; for i := 1 to C^.PropCount do P := P^.Next; result := PtrUInt(P)-PtrUInt(@self); end; function TClassType.InheritsFrom(AClass: TClass): boolean; {$ifdef FPC_OR_PUREPASCAL} var P: PTypeInfo; begin result := true; if ClassType=AClass then exit; P := DeRef(ParentInfo); while P<>nil do with P^.ClassType^ do if ClassType=AClass then exit else P := DeRef(ParentInfo); result := false; end; {$else} asm // eax=PClassType edx=AClass cmp [eax].TClassType.ClassType, edx jz @3 @2: mov eax, [eax].TClassType.ParentInfo test eax, eax jz @0 @1: mov eax, [eax] movzx ecx, byte ptr[eax].TTypeInfo.Name lea eax, [eax + ecx].TTypeInfo.Name[1] cmp edx, [eax].TClassType.ClassType jnz @2 @3: mov eax, 1 @0: end; {$endif} function SQLWhereIsEndClause(const Where: RawUTF8): boolean; begin result := IdemPCharArray(pointer(Where),['ORDER BY ','GROUP BY ', 'LIMIT ','OFFSET ','LEFT ','RIGHT ','INNER ','OUTER ','JOIN '])>=0; end; function SQLFromWhere(const Where: RawUTF8): RawUTF8; begin if Where='' then result := '' else if SQLWhereIsEndClause(Where) then result := ' '+Where else result := ' WHERE '+Where; end; function SQLFromSelect(const TableName, Select, Where, SimpleFields: RawUTF8): RawUTF8; begin if Select='*' then // don't send BLOB values to query: retrieve all other fields result := 'SELECT '+SimpleFields else result := 'SELECT '+Select; result := result+' FROM '+TableName+SQLFromWhere(Where); end; function SelectInClause(const PropName: RawUTF8; const Values: array of RawUTF8; const Suffix: RawUTF8; ValuesInlinedMax: integer): RawUTF8; var n, i: integer; temp: TTextWriterStackBuffer; begin n := length(Values); if n>0 then with TTextWriter.CreateOwnedStream(temp) do try AddString(PropName); if n=1 then begin if ValuesInlinedMax>1 then AddShort('=:(') else Add('='); AddQuotedStr(pointer(Values[0]),''''); if ValuesInlinedMax>1 then AddShort('):'); end else begin AddShort(' in ('); for i := 0 to n-1 do begin if ValuesInlinedMax>n then Add(':','('); AddQuotedStr(pointer(Values[i]),''''); if ValuesInlinedMax>n then AddShort('):,') else Add(','); end; CancelLastComma; Add(')'); end; AddString(Suffix); SetText(result); finally Free; end else result := ''; end; function SelectInClause(const PropName: RawUTF8; const Values: array of Int64; const Suffix: RawUTF8; ValuesInlinedMax: integer): RawUTF8; var n, i: integer; temp: TTextWriterStackBuffer; begin n := length(Values); if n>0 then with TTextWriter.CreateOwnedStream(temp) do try AddString(PropName); if n=1 then begin if ValuesInlinedMax>1 then AddShort('=:(') else Add('='); Add(Values[0]); if ValuesInlinedMax>1 then AddShort('):'); end else begin AddShort(' in ('); for i := 0 to n-1 do begin if ValuesInlinedMax>n then Add(':','('); Add(Values[i]); if ValuesInlinedMax>n then AddShort('):,') else Add(','); end; CancelLastComma; Add(')'); end; AddString(Suffix); SetText(result); finally Free; end else result := ''; end; { TSQLRecordFill } function TSQLRecordFill.GetJoinedFields: boolean; begin if self=nil then result := false else result := fJoinedFields; end; function TSQLRecordFill.TableMapFields: TSQLFieldBits; begin if self=nil then FillZero(result) else result := fTableMapFields; end; procedure TSQLRecordFill.AddMap(aRecord: TSQLRecord; aField: TSQLPropInfo; aIndex: integer); begin if (self=nil) or (aRecord=nil) then exit; if fTableMapCount>=length(fTableMap) then SetLength(fTableMap,fTableMapCount+fTableMapCount shr 1+16); with fTableMap[fTableMapCount] do begin Dest := aRecord; DestField := aField; TableIndex := aIndex; inc(fTableMapCount); end; end; procedure TSQLRecordFill.AddMap(aRecord: TSQLRecord; const aFieldName: RawUTF8; aIndex: integer); var aFieldIndex: integer; begin if (self<>nil) and (aRecord<>nil) then if IsRowID(pointer(aFieldName)) then AddMap(aRecord,nil,aIndex) else with aRecord.RecordProps do begin aFieldIndex := Fields.IndexByName(aFieldName); if aFieldIndex>=0 then begin // only map if column name is a valid field include(fTableMapFields,aFieldIndex); AddMap(aRecord,Fields.List[aFieldIndex],aIndex); end; end; end; procedure TSQLRecordFill.AddMapSimpleFields(aRecord: TSQLRecord; const aProps: array of TSQLPropInfo; var aIndex: integer); var i: integer; begin AddMap(aRecord,nil,aIndex); inc(aIndex); for i := 0 to high(aProps) do if aProps[i].SQLFieldTypeStored<>sftID then begin AddMap(aRecord,aProps[i],aIndex); inc(aIndex); end; end; destructor TSQLRecordFill.Destroy; begin try UnMap; // release fTable instance if necessary finally inherited; end; end; function TSQLRecordFill.Fill(aRow: integer): Boolean; begin if (self=nil) or (Table=nil) or (cardinal(aRow)>cardinal(Table.fRowCount)) then Result := False else begin Fill(@Table.fResults[aRow*Table.FieldCount]); Result := True; end; end; function TSQLRecordFill.Fill(aRow: integer; aDest: TSQLRecord): Boolean; begin if (self=nil) or (aDest=nil) or (Table=nil) or (cardinal(aRow)>cardinal(Table.fRowCount)) then Result := False else begin Fill(@Table.fResults[aRow*Table.FieldCount],aDest); Result := True; end; end; procedure TSQLRecordFill.Fill(aTableRow: PPUtf8CharArray); var f: integer; begin if (self<>nil) and (aTableRow<>nil) then for f := 0 to fTableMapCount-1 do with fTableMap[f] do if DestField=nil then SetID(aTableRow[TableIndex],Dest.fID) else DestField.SetValue(Dest,aTableRow[TableIndex],TableIndex in fTable.fFieldParsedAsString); end; procedure TSQLRecordFill.Fill(aTableRow: PPUtf8CharArray; aDest: TSQLRecord); var f: integer; begin if (self<>nil) and (aTableRow<>nil) then for f := 0 to fTableMapCount-1 do with fTableMap[f] do if DestField=nil then SetID(aTableRow[TableIndex],aDest.fID) else DestField.SetValue(aDest,aTableRow[TableIndex],TableIndex in fTable.fFieldParsedAsString); end; procedure TSQLRecordFill.ComputeSetUpdatedFieldBits(Props: TSQLRecordProperties; out Bits: TSQLFieldBits); begin if (self<>nil) and (fTable<>nil) and (fTableMapRecordManyInstances=nil) then // within FillPrepare/FillOne loop: update ID, TModTime and mapped fields Bits := fTableMapFields+Props.ComputeBeforeUpdateFieldsBits else // update all simple/custom fields (also for FillPrepareMany) Bits := Props.SimpleFieldsBits[soUpdate]; end; procedure TSQLRecordFill.Map(aRecord: TSQLRecord; aTable: TSQLTable; aCheckTableName: TSQLCheckTableName); var f: integer; ColumnName: PUTF8Char; FieldName: RawUTF8; Props: TSQLRecordProperties; begin if aTable=nil then // avoid any GPF exit; fTable := aTable; if aTable.fResults=nil then exit; // void content Props := aRecord.RecordProps; for f := 0 to aTable.FieldCount-1 do begin ColumnName := aTable.fResults[f]; if aCheckTableName=ctnNoCheck then Utf8ToRawUTF8(ColumnName,FieldName) else if IdemPChar(ColumnName,pointer(Props.SQLTableNameUpperWithDot)) then Utf8ToRawUTF8(ColumnName+length(Props.SQLTableNameUpperWithDot),FieldName) else if aCheckTableName=ctnMustExist then continue else Utf8ToRawUTF8(ColumnName,FieldName); AddMap(aRecord,FieldName,f); end; fFillCurrentRow := 1; // point to first data row (0 is field names) end; procedure TSQLRecordFill.UnMap; var i: integer; begin if self=nil then exit; fTableMapCount := 0; fFillCurrentRow := 0; // release TSQLRecordMany.fDestID^ instances set by TSQLRecord.FillPrepareMany() for i := 0 to length(fTableMapRecordManyInstances)-1 do with fTableMapRecordManyInstances[i] do begin TObject(fDestID^).Free; fDestID^ := 0; fSourceID^ := 0; end; fTableMapRecordManyInstances := nil; FillZero(fTableMapFields); // free any previous fTable if necessary if Table<>nil then try if Table.OwnerMustFree then Table.Free; finally fTable := nil; end; end; { TSQLRecord } constructor TSQLRecord.Create; var i: PtrInt; begin // auto-instanciate any TSQLRecordMany instance with RecordProps do if pointer(ManyFields)<>nil then for i := 0 to length(ManyFields)-1 do ManyFields[i].SetInstance(self,TSQLRecordClass(ManyFields[i].ObjectClass).Create); end; constructor TSQLRecord.Create(const aSimpleFields: array of const; aID: TID); begin Create; fID := aID; if not SimplePropertiesFill(aSimpleFields) then raise EORMException.CreateUTF8('Incorrect %.Create(aSimpleFields) call',[self]); end; function TSQLRecord.CreateCopy: TSQLRecord; var f: PtrInt; begin // create new instance result := RecordClass.Create; // copy properties content result.fID := fID; with RecordProps do for f := 0 to length(CopiableFields)-1 do CopiableFields[f].CopyValue(self,result); end; function TSQLRecord.CreateCopy(const CustomFields: TSQLFieldBits): TSQLRecord; var f: integer; begin result := RecordClass.Create; // copy properties content result.fID := fID; with RecordProps do for f := 0 to Fields.Count-1 do if (f in CustomFields) and (f in CopiableFieldsBits) then Fields.List[f].CopyValue(self,result); end; function TSQLRecord.GetNonVoidFields: TSQLFieldBits; var f: integer; begin FillZero(result); with RecordProps do for f := 0 to Fields.Count-1 do if (f in CopiableFieldsBits) and not Fields.List[f].IsValueVoid(self) then include(result,f); end; constructor TSQLRecord.Create(aClient: TSQLRest; aID: TID; ForUpdate: boolean=false); begin Create; if aClient<>nil then aClient.Retrieve(aID,self,ForUpdate); end; constructor TSQLRecord.Create(aClient: TSQLRest; aPublishedRecord: TSQLRecord; ForUpdate: boolean); begin Create; if aClient<>nil then aClient.Retrieve(aPublishedRecord.ID,self,ForUpdate); end; constructor TSQLRecord.Create(aClient: TSQLRest; const aSQLWhere: RawUTF8); begin Create; if aClient<>nil then aClient.Retrieve(aSQLWhere,self); end; constructor TSQLRecord.Create(aClient: TSQLRest; const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const); begin Create; if aClient<>nil then aClient.Retrieve(FormatUTF8(FormatSQLWhere,[],BoundsSQLWhere),self); end; constructor TSQLRecord.Create(aClient: TSQLRest; const FormatSQLWhere: RawUTF8; const ParamsSQLWhere, BoundsSQLWhere: array of const); begin Create; if aClient<>nil then aClient.Retrieve(FormatUTF8(FormatSQLWhere,ParamsSQLWhere,BoundsSQLWhere),self); end; constructor TSQLRecord.CreateFrom(const JSONRecord: RawUTF8); begin Create; FillFrom(JSONRecord); end; constructor TSQLRecord.CreateFrom(P: PUTF8Char); begin Create; FillFrom(P); end; {$ifndef NOVARIANTS} constructor TSQLRecord.CreateFrom(const aDocVariant: variant); begin Create; FillFrom(aDocVariant); end; {$endif} class procedure TSQLRecord.InitializeTable(Server: TSQLRestServer; const FieldName: RawUTF8; Options: TSQLInitializeTableOptions); var f: integer; begin // is not part of TSQLRecordProperties because has been declared as virtual if (self<>nil) and (Server<>nil) and (Options*INITIALIZETABLE_NOINDEX<>INITIALIZETABLE_NOINDEX) then begin // ensure ID/RowID column is indexed if not (itoNoIndex4ID in Options) then if (FieldName='') or IsRowID(pointer(FieldName)) then Server.CreateSQLIndex(self,'ID',true); // for external tables // automatic column indexation of fields which are commonly searched by value with RecordProps do for f := 0 to Fields.Count-1 do with Fields.List[f] do if (FieldName='') or IdemPropNameU(FieldName,Name) then if ((aIsUnique in Attributes) and not (itoNoIndex4UniqueField in Options)) or ((SQLFieldType=sftRecord) and not (itoNoIndex4RecordReference in Options)) or ((SQLFieldType=sftRecordVersion) and not (itoNoIndex4RecordVersion in Options)) or ((SQLFieldType=sftID) and not (itoNoIndex4NestedRecord in Options)) or ((SQLFieldType=sftTID) and not (itoNoIndex4TID in Options)) then Server.CreateSQLIndex(self,Name,false); end; // failure in Server.CreateSQLIndex() above is ignored (may already exist) end; procedure TSQLRecord.FillFrom(aRecord: TSQLRecord); begin if (self<>nil) and (aRecord<>nil) then FillFrom(aRecord, aRecord.RecordProps.CopiableFieldsBits); end; procedure TSQLRecord.FillFrom(aRecord: TSQLRecord; const aRecordFieldBits: TSQLFieldBits); var i, f: integer; S, D: TSQLRecordProperties; SP: TSQLPropInfo; wasString: boolean; tmp: RawUTF8; begin if (self=nil) or (aRecord=nil) or IsZero(aRecordFieldBits) then exit; D := RecordProps; if PSQLRecordClass(aRecord)^.InheritsFrom(PSQLRecordClass(self)^) then begin if PSQLRecordClass(aRecord)^=PSQLRecordClass(self)^ then fID := aRecord.fID; // same class -> ID values will match for f := 0 to D.Fields.Count-1 do if f in aRecordFieldBits then D.Fields.List[f].CopyValue(aRecord,self); exit; end; S := aRecord.RecordProps; // two diverse tables -> don't copy ID for i := 0 to S.Fields.Count-1 do if i in aRecordFieldBits then begin SP := S.Fields.List[i]; if D.Fields.List[i].Name=SP.Name then // optimistic match f := i else f := D.Fields.IndexByName(SP.Name); if f>=0 then begin SP.GetValueVar(aRecord,False,tmp,@wasString); D.Fields.List[f].SetValueVar(Self,tmp,wasString); end; end; end; procedure TSQLRecord.FillFrom(Table: TSQLTable; Row: integer); begin try FillPrepare(Table); if Table.InternalState<>fInternalState then fInternalState := Table.InternalState; FillRow(Row); finally FillClose; // avoid GPF in TSQLRecord.Destroy end; end; procedure TSQLRecord.FillFrom(const JSONTable: RawUTF8; Row: integer); var Table: TSQLTableJSON; tmp: TSynTempBuffer; // work on a private copy begin tmp.Init(JSONTable); try Table := TSQLTableJSON.Create('',tmp.buf,tmp.len); try FillFrom(Table,Row); finally Table.Free; end; finally tmp.Done; end; end; procedure TSQLRecord.FillFrom(const JSONRecord: RawUTF8; FieldBits: PSQLFieldBits); var tmp: TSynTempBuffer; // work on a private copy begin tmp.Init(JSONRecord); try FillFrom(tmp.buf,FieldBits); // now we can safely call FillFrom() finally tmp.Done; end; end; procedure TSQLRecord.FillFrom(P: PUTF8Char; FieldBits: PSQLFieldBits); (* two possible formats = first not expanded, 2nd is expanded (most useful) {"fieldCount":9,"values":["ID","Int","Test","Unicode","Ansi","ValFloat","ValWord", "ValDate","Next",0,0,"abcde+?ef+?+?","abcde+?ef+?+?","abcde+?ef+?+?", 3.14159265300000E+0000,1203,"2009-03-10T21:19:36",0]} {"ID":0,"Int":0,"Test":"abcde+?ef+?+?","Unicode":"abcde+?ef+?+?","Ansi": "abcde+?ef+?+?","ValFloat": 3.14159265300000E+0000,"ValWord":1203, "ValDate":"2009-03-10T21:19:36","Next":0} *) var F: array[0..MAX_SQLFIELDS-1] of PUTF8Char; // store field/property names wasString: boolean; i, n: integer; Prop, Value: PUTF8Char; begin if FieldBits<>nil then FillZero(FieldBits^); // go to start of object if P=nil then exit; while P^<>'{' do if P^=#0 then exit else inc(P); if Expect(P,FIELDCOUNT_PATTERN,14) then begin // not expanded format n := GetJSONIntegerVar(P)-1; if cardinal(n)>high(F) then exit; if Expect(P,ROWCOUNT_PATTERN,12) then GetJSONIntegerVar(P); // just ignore "rowCount":.. here if not Expect(P,VALUES_PATTERN,11) then exit; for i := 0 to n do F[i] := GetJSONField(P,P); for i := 0 to n do begin Value := GetJSONFieldOrObjectOrArray(P,@wasString,nil,true); FillValue(F[i],Value,wasString,FieldBits); // set properties from values end; end else if P^='{' then begin // expanded format inc(P); repeat Prop := GetJSONPropName(P); if (Prop=nil) or (P=nil) then break; Value := GetJSONFieldOrObjectOrArray(P,@wasString,nil,true); FillValue(Prop,Value,wasString,FieldBits); // set property from value until P=nil; end; end; {$ifndef NOVARIANTS} procedure TSQLRecord.FillFrom(const aDocVariant: variant); var json: RawUTF8; begin if _Safe(aDocVariant)^.Kind=dvObject then begin VariantSaveJSON(aDocVariant,twJSONEscape, json); FillFrom(pointer(json)); end; end; {$endif} procedure TSQLRecord.FillPrepare(Table: TSQLTable; aCheckTableName: TSQLCheckTableName); begin if self=nil then exit; if fFill=nil then fFill := TSQLRecordFill.Create else fFill.UnMap; fFill.Map(self,Table,aCheckTableName); end; function TSQLRecord.FillPrepare(aClient: TSQLRest; const aSQLWhere: RawUTF8; const aCustomFieldsCSV: RawUTF8; aCheckTableName: TSQLCheckTableName): boolean; var T: TSQLTable; begin result := false; FillClose; // so that no further FillOne will work if (self=nil) or (aClient=nil) then exit; T := aClient.MultiFieldValues(RecordClass,aCustomFieldsCSV,aSQLWhere); if T=nil then exit; T.OwnerMustFree := true; FillPrepare(T,aCheckTableName); result := true; end; function TSQLRecord.FillPrepare(aClient: TSQLRest; const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const; const aCustomFieldsCSV: RawUTF8=''): boolean; var sqlwhere: RawUTF8; begin sqlwhere := FormatUTF8(FormatSQLWhere,[],BoundsSQLWhere); result := FillPrepare(aClient,sqlwhere,aCustomFieldsCSV); end; function TSQLRecord.FillPrepare(aClient: TSQLRest; const FormatSQLWhere: RawUTF8; const ParamsSQLWhere, BoundsSQLWhere: array of const; const aCustomFieldsCSV: RawUTF8): boolean; var sqlwhere: RawUTF8; begin sqlwhere := FormatUTF8(FormatSQLWhere,ParamsSQLWhere,BoundsSQLWhere); result := FillPrepare(aClient,sqlwhere,aCustomFieldsCSV); end; const INLINED_MAX = 10; function TSQLRecord.FillPrepare(aClient: TSQLRest; const aIDs: array of Int64; const aCustomFieldsCSV: RawUTF8=''): boolean; begin if high(aIDs)<0 then result := false else result := FillPrepare(aClient,SelectInClause('id',aIDs,'',INLINED_MAX),aCustomFieldsCSV); end; function TSQLRecord.FillRow(aRow: integer; aDest: TSQLRecord): boolean; begin if self<>nil then if aDest=nil then result := fFill.Fill(aRow) else if fFill.fTableMapRecordManyInstances=nil then result := fFill.Fill(aRow,aDest) else raise EBusinessLayerException.CreateUTF8( '%.FillRow() forbidden after FillPrepareMany',[self]) else result := false; end; function TSQLRecord.FillOne(aDest: TSQLRecord=nil): boolean; begin if (self=nil) or (fFill=nil) or (fFill.Table=nil) or (fFill.Table.fRowCount=0) or // also check if FillTable is emtpy (cardinal(fFill.FillCurrentRow)>cardinal(fFill.Table.fRowCount)) then result := false else begin FillRow(fFill.FillCurrentRow,aDest); inc(fFill.fFillCurrentRow); result := true; end; end; function TSQLRecord.FillRewind: boolean; begin if (self=nil) or (fFill=nil) or (fFill.Table=nil) or (fFill.Table.fRowCount=0) then result := false else begin fFill.fFillCurrentRow := 1; result := true; end; end; procedure TSQLRecord.FillClose; begin if self<>nil then fFill.UnMap; end; procedure TSQLRecord.AppendFillAsJsonValues(W: TJSONSerializer); begin W.Add('['); while FillOne do begin GetJSONValues(W); W.Add(','); end; W.CancelLastComma; W.Add(']'); end; procedure TSQLRecord.FillValue(PropName: PUTF8Char; Value: PUTF8Char; wasString: boolean; FieldBits: PSQLFieldBits); var field: TSQLPropInfo; begin if self<>nil then if IsRowID(PropName) then SetID(Value,fID) else begin field := RecordProps.Fields.ByName(PropName); if field<>nil then begin field.SetValue(self,Value,wasString); if FieldBits<>nil then Include(FieldBits^,field.PropertyIndex); end; end; end; function TSQLRecord.SetFieldSQLVars(const Values: TSQLVarDynArray): boolean; var max, field: integer; begin result := false; max := length(Values)-1; with RecordProps do begin // expect exact Values[] type match with FieldType[] if max<>Fields.Count-1 then // must match field count exit else for field := 0 to max do if Fields.List[field].SQLDBFieldType<>Values[field].VType then exit; // now we can safely update field values for field := 0 to max do Fields.List[field].SetFieldSQLVar(self,Values[field]); end; result := true; end; procedure TSQLRecord.GetBinaryValues(W: TFileBufferWriter); var f: integer; begin with RecordProps do for f := 0 to Fields.Count-1 do Fields.List[f].GetBinary(self,W); end; procedure TSQLRecord.GetBinaryValuesSimpleFields(W: TFileBufferWriter); var f: integer; begin with RecordProps do for f := 0 to SimpleFieldCount-1 do SimpleFields[f].GetBinary(self,W); end; procedure TSQLRecord.GetBinaryValues(W: TFileBufferWriter; const aFields: TSQLFieldBits); var f: integer; begin with RecordProps do for f := 0 to Fields.Count-1 do if f in aFields then Fields.List[f].GetBinary(self,W); end; function TSQLRecord.GetBinary: RawByteString; var W: TFileBufferWriter; begin W := TFileBufferWriter.Create(TRawByteStringStream); try W.WriteVarUInt64(fID); GetBinaryValues(W); W.Flush; result := (W.Stream as TRawByteStringStream).DataString; finally W.Free; end; end; function TSQLRecord.SetBinary(P,PEnd: PAnsiChar): Boolean; begin P := pointer(FromVarUInt64Safe(pointer(P),pointer(PEnd),PQWord(@fID)^)); result := SetBinaryValues(P,PEnd); end; function TSQLRecord.SetBinary(const binary: RawByteString): Boolean; begin result := SetBinary(pointer(binary),PAnsiChar(pointer(binary))+length(binary)); end; function TSQLRecord.SetBinaryValues(var P: PAnsiChar; PEnd: PAnsiChar): boolean; var f: integer; begin result := false; if P=nil then exit; // on error with RecordProps do for f := 0 to Fields.Count-1 do begin P := Fields.List[f].SetBinary(self,P,PEnd); if P=nil then exit; end; result := true; end; function TSQLRecord.SetBinaryValuesSimpleFields(var P: PAnsiChar; PEnd: PAnsiChar): Boolean; var f: integer; begin result := false; with RecordProps do for f := 0 to SimpleFieldCount-1 do begin P := SimpleFields[f].SetBinary(self,P,PEnd); if P=nil then exit; // on error end; result := true; end; procedure TSQLRecord.GetJSONValues(W: TJSONSerializer); var i,n: integer; Props: TSQLPropInfoList; begin if self=nil then exit; // write the row data if W.Expand then begin W.Add('{'); if W.WithID then W.AddString(W.ColNames[0]); end; if W.WithID then begin W.Add(fID); W.Add(','); if (jwoID_str in W.fSQLRecordOptions) and W.Expand then begin W.AddShort('"ID_str":"'); W.Add(fID); W.Add('"',','); end; n := 1; end else n := 0; if W.Fields<>nil then begin Props := RecordProps.Fields; for i := 0 to length(W.Fields)-1 do begin if W.Expand then begin W.AddString(W.ColNames[n]); // '"'+ColNames[]+'":' inc(n); end; Props.List[W.Fields[i]].GetJSONValues(Self,W); W.Add(','); end; end; W.CancelLastComma; // cancel last ',' if W.Expand then W.Add('}'); end; procedure TSQLRecord.AppendAsJsonObject(W: TJSONSerializer; Fields: TSQLFieldBits); var i: integer; Props: TSQLPropInfoList; begin if Self=nil then begin W.AddShort('null'); exit; end; W.AddShort('{"ID":'); W.Add(fID); if IsZero(Fields) then Fields := RecordProps.SimpleFieldsBits[soSelect]; Props := RecordProps.Fields; for i := 0 to Props.Count-1 do if i in Fields then begin W.Add(',','"'); W.AddNoJSONEscape(pointer(Props.List[i].Name),length(Props.List[i].Name)); W.Add('"',':'); Props.List[i].GetJSONValues(Self,W); end; W.Add('}'); end; procedure TSQLRecord.AppendFillAsJsonArray(const FieldName: RawUTF8; W: TJSONSerializer; Fields: TSQLFieldBits=[]); begin if FieldName<>'' then W.AddFieldName(FieldName); W.Add('['); while FillOne do begin AppendAsJsonObject(W,Fields); W.Add(','); end; W.CancelLastComma; W.Add(']'); if FieldName<>'' then W.Add(','); end; {$ifndef NOVARIANTS} procedure TSQLRecord.ForceVariantFieldsOptions(aOptions: TDocVariantOptions); var i: integer; p: TSQLPropInfo; begin if self<>nil then with RecordProps do if sftVariant in HasTypeFields then for i := 0 to Fields.Count-1 do begin p := Fields.List[i]; if (p.SQLFieldType=sftVariant) and p.InheritsFrom(TSQLPropInfoRTTIVariant) then with TSQLPropInfoRTTIVariant(p) do if PropInfo.GetterIsField then with _Safe(PVariant(PropInfo.GetterAddr(self))^)^ do if Count>0 then Options := aOptions; end; end; {$endif} procedure TSQLRecord.GetJSONValuesAndFree(JSON : TJSONSerializer); begin if JSON<>nil then try // write the row data GetJSONValues(JSON); // end the JSON object if not JSON.Expand then JSON.AddNoJSONEscape(PAnsiChar(']}'),2); JSON.FlushFinal; finally JSON.Free; end; end; procedure TSQLRecord.GetJSONValues(JSON: TStream; Expand, withID: boolean; Occasion: TSQLOccasion; SQLRecordOptions: TJSONSerializerSQLRecordOptions); var serializer: TJSONSerializer; begin if self=nil then exit; with RecordProps do serializer := CreateJSONWriter(JSON,Expand,withID, SimpleFieldsBits[Occasion],{knownrows=}0); serializer.SQLRecordOptions := SQLRecordOptions; GetJSONValuesAndFree(serializer); end; function TSQLRecord.GetJSONValues(Expand, withID: boolean; const Fields: TSQLFieldBits; SQLRecordOptions: TJSONSerializerSQLRecordOptions): RawUTF8; var J: TRawByteStringStream; serializer: TJSONSerializer; begin J := TRawByteStringStream.Create; try serializer := RecordProps.CreateJSONWriter(J,Expand,withID,Fields,{knownrows=}0); serializer.SQLRecordOptions := SQLRecordOptions; GetJSONValuesAndFree(serializer); result := J.DataString; finally J.Free; end; end; function TSQLRecord.GetJSONValues(Expand, withID: boolean; const FieldsCSV: RawUTF8; SQLRecordOptions: TJSONSerializerSQLRecordOptions): RawUTF8; var bits: TSQLFieldBits; begin if RecordProps.FieldBitsFromCSV(FieldsCSV,bits) then result := GetJSONValues(Expand,withID,bits,SQLRecordOptions) else result := ''; end; function TSQLRecord.GetJSONValues(Expand, withID: boolean; Occasion: TSQLOccasion; UsingStream: TCustomMemoryStream; SQLRecordOptions: TJSONSerializerSQLRecordOptions): RawUTF8; var J: TRawByteStringStream; begin if not withID and IsZero(RecordProps.SimpleFieldsBits[Occasion]) then // no simple field to write -> quick return result := '' else if UsingStream<>nil then begin UsingStream.Seek(0,soFromBeginning); GetJSONValues(UsingStream,Expand,withID,Occasion,SQLRecordOptions); FastSetString(result,UsingStream.Memory,UsingStream.Seek(0,soFromCurrent)); end else begin J := TRawByteStringStream.Create; try GetJSONValues(J,Expand,withID,Occasion,SQLRecordOptions); result := J.DataString; finally J.Free; end; end; end; function GetVirtualTableSQLCreate(Props: TSQLRecordProperties): RawUTF8; var i: integer; SQL: RawUTF8; begin result := ''; // RowID is added by sqlite3_declare_vtab() for a Virtual Table for i := 0 to Props.Fields.Count-1 do with Props.Fields.List[i] do begin SQL := Props.SQLFieldTypeToSQL(i); // = '' for field with no matching DB column if SQL<>'' then result := result+Name+SQL; end; if result='' then result := ');' else pWord(@result[length(result)-1])^ := ord(')')+ord(';')shl 8; end; class function TSQLRecord.GetSQLCreate(aModel: TSQLModel): RawUTF8; // not implemented in TSQLRecordProperties since has been made virtual var i: integer; c: TClass; SQL, cname, tokenizer: RawUTF8; M: TSQLVirtualTableClass; Props: TSQLModelRecordProperties; fields: TSQLPropInfoList; begin if aModel=nil then raise EModelException.CreateUTF8('Invalid %.GetSQLCreate(nil) call',[self]); Props := aModel.Props[self]; if Props.Kind<>rSQLite3 then begin // create a FTS3/FTS4/RTREE virtual table result := 'CREATE VIRTUAL TABLE '+SQLTableName+' USING '; case Props.Kind of rFTS3: result := result+'fts3('; rFTS4: result := result+'fts4('; rFTS5: result := result+'fts5('; rRTree: result := result+'rtree(RowID,'; rRTreeInteger: result := result+'rtree_i32(RowID,'; rCustomForcedID, rCustomAutoID: begin M := aModel.VirtualTableModule(self); if M=nil then raise EModelException.CreateUTF8('No registered module for %',[self]); if Props.Props.Fields.Count=0 then raise EModelException.CreateUTF8( 'Virtual % class % should have published properties',[M.ModuleName,self]); result := result+M.ModuleName+'('; end; else raise EModelException.CreateUTF8('%.GetSQLCreate(%)?',[self,ToText(Props.Kind)^]); end; fields := Props.Props.Fields; case Props.Kind of rFTS3, rFTS4, rFTS5: begin if (Props.fFTSWithoutContentFields<>'') and (Props.fFTSWithoutContentTableIndex>=0) then begin result := FormatUTF8('%content="%",',[result, aModel.Tables[Props.fFTSWithoutContentTableIndex].SQLTableName]); if Props.Kind = rFTS5 then result := FormatUTF8('%content_rowid="ID",',[result]); end; for i := 0 to fields.Count-1 do result := result+fields.List[i].Name+','; tokenizer := 'simple'; c := self; repeat ToText(c,cname); // TSQLFTSTest = class(TSQLRecordFTS3Porter) if IdemPChar(pointer(cname),'TSQLRECORDFTS') and (cname[14] in ['3','4','5']) then begin if length(cname)>14 then tokenizer := copy(cname,15,100); // e.g. TSQLRecordFTS3Porter -> 'Porter' break; end; c := GetClassParent(c); until c=TSQLRecord; result := FormatUTF8('% tokenize=%)',[result,LowerCaseU(tokenizer)]); end; rRTree, rRTreeInteger: begin for i := 0 to fields.Count-1 do with fields.List[i] do if aAuxiliaryRTreeField in Attributes then // for SQlite3 >= 3.24.0 result := FormatUTF8('%+% %',[result,Name,Props.Props.SQLFieldTypeToSQL(i)]) else result := result+Name+','; result[length(result)] := ')'; end; rCustomForcedID, rCustomAutoID: result := result+GetVirtualTableSQLCreate(Props.Props); end; end else begin // inherits from TSQLRecord: create a "normal" SQLite3 table result := 'CREATE TABLE '+SQLTableName+ '(ID INTEGER PRIMARY KEY AUTOINCREMENT, '; // we always add an ID field which is an INTEGER PRIMARY KEY // column, as it is always created (as hidden RowID) by the SQLite3 engine with Props.Props do for i := 0 to Fields.Count-1 do with Fields.List[i] do begin SQL := SQLFieldTypeToSQL(i); // = '' for field with no matching DB column if SQL<>'' then begin result := result+Name+SQL; if i in IsUniqueFieldsBits then insert(' UNIQUE',result,length(result)-1); end; end; pWord(@result[length(result)-1])^ := ord(')')+ord(';')shl 8; end; end; function TSQLRecord.GetSQLSet: RawUTF8; var i: integer; V: RawUTF8; wasString: boolean; begin result := ''; if self=nil then exit; with RecordProps do for i := 0 to length(SimpleFields)-1 do with SimpleFields[i] do begin // format is 'COL1='VAL1', COL2='VAL2'' } GetValueVar(self,true,V,@wasString); if wasString then V := QuotedStr(V); result := result+Name+'='+V+', '; end; if result<>'' then SetLength(result,length(result)-2); end; function TSQLRecord.GetSQLValues: RawUTF8; var i: integer; V: RawUTF8; wasString: boolean; begin result := ''; if self<>nil then with RecordProps do if SimpleFields=nil then exit else begin if HasNotSimpleFields then // get 'COL1,COL2': no 'ID,' for INSERT (false below) result := SQLTableSimpleFieldsNoRowID; // always <> '*' result := result+' VALUES ('; for i := 0 to length(SimpleFields)-1 do with SimpleFields[i] do begin GetValueVar(self,true,V,@wasString); if wasString then V := QuotedStr(V); result := result+V+','; end; result[length(result)] := ')'; end; end; class function TSQLRecord.CaptionName(Action: PRawUTF8=nil; ForHint: boolean=false): string; begin if Action=nil then GetCaptionFromPCharLen(pointer(RecordProps.SQLTableName),result) else GetCaptionFromPCharLen(TrimLeftLowerCase(Action^),result); end; class function TSQLRecord.CaptionNameFromRTTI(Action: PShortString): string; var tmp: RawUTF8; begin if Action=nil then result := CaptionName(nil) else begin SetString(tmp,PAnsiChar(@Action^[1]),ord(Action^[0])); result := CaptionName(@tmp); end; end; function TSQLRecord.SameRecord(Reference: TSQLRecord): boolean; var i: integer; begin result := false; if (self=nil) or (Reference=nil) or (PSQLRecordClass(Reference)^<>PSQLRecordClass(Self)^) or (Reference.fID<>fID) then exit; with RecordProps do for i := 0 to length(SimpleFields)-1 do // compare not TSQLRawBlob/TSQLRecordMany fields with SimpleFields[i] do if CompareValue(self,Reference,false)<>0 then exit; // properties don't have the same value result := true; end; function TSQLRecord.SameValues(Reference: TSQLRecord): boolean; var O: TSQLPropInfo; i: integer; This,Ref: TSQLRecordProperties; begin result := false; if (self=nil) or (Reference=nil) or (Reference.fID<>fID) then // ID field must be tested by hand exit; if self<>Reference then if (PSQLRecordClass(Reference)^=PSQLRecordClass(self)^) then begin // faster comparison on same exact class with RecordProps do for i := 0 to length(SimpleFields)-1 do // compare not TSQLRawBlob/TSQLRecordMany fields with SimpleFields[i] do if CompareValue(self,Reference,false)<>0 then exit; // properties don't have the same value end else begin // comparison of all properties of Reference against self This := RecordProps; Ref := Reference.RecordProps; for i := 0 to length(Ref.SimpleFields)-1 do with Ref.SimpleFields[i] do begin // compare not TSQLRawBlob/TSQLRecordMany fields O := This.Fields.ByRawUTF8Name(Name); if O=nil then exit; // this Reference property doesn't exist in current object if GetValue(Reference,false,nil)<>O.GetValue(self,false,nil) then exit; // properties don't have the same value end; end; result := true; end; procedure TSQLRecord.ClearProperties; var i: integer; begin if self=nil then exit; fInternalState := 0; fID := 0; with RecordProps do if fFill.JoinedFields then begin for i := 0 to length(CopiableFields)-1 do if CopiableFields[i].SQLFieldType<>sftID then CopiableFields[i].SetValue(self,nil,false) else TSQLRecord(TSQLPropInfoRTTIInstance(CopiableFields[i]).GetInstance(Self)). ClearProperties; // clear nested allocated TSQLRecord end else for i := 0 to length(CopiableFields)-1 do CopiableFields[i].SetValue(self,nil,false); end; procedure TSQLRecord.ClearProperties(const aFieldsCSV: RawUTF8); var bits: TSQLFieldBits; f: integer; begin if (self=nil) or (aFieldsCSV='') then exit; with RecordProps do begin if aFieldsCSV='*' then bits := SimpleFieldsBits[soInsert] else if not FieldBitsFromCSV(aFieldsCSV,bits) then exit; for f := 0 to Fields.Count-1 do if (f in bits) and (Fields.List[f].SQLFieldType in COPIABLE_FIELDS) then Fields.List[f].SetValue(self,nil,false); // clear field value end; end; {$IFDEF PUREPASCAL} function TSQLRecord.RecordClass: TSQLRecordClass; begin if self=nil then Result := nil else Result := PSQLRecordClass(Self)^; end; {$else} function TSQLRecord.RecordClass: TSQLRecordClass; {$ifdef FPC} nostackframe; assembler; {$endif} asm test eax, eax jz @z mov eax, [eax] @z: end; {$endif} {$IFDEF PUREPASCAL} function TSQLRecord.ClassProp: PClassProp; begin if self<>nil then result := InternalClassProp(ClassType) else result := nil; // avoid GPF end; {$else} function TSQLRecord.ClassProp: PClassProp; {$ifdef FPC} nostackframe; assembler; {$endif} asm test eax, eax jz @z // avoid GPF mov eax, [eax] // get ClassType of this TSQLRecord instance test eax, eax jz @z // avoid GPF mov eax, [eax + vmtTypeInfo] test eax, eax jz @z // avoid GPF movzx edx, byte ptr[eax].TTypeInfo.Name lea eax, [eax + edx].TTypeInfo.Name[1] movzx edx, byte ptr[eax].TClassType.UnitName lea eax, [eax + edx].TClassType.UnitName[1].TClassProp @z: end; {$endif} function TSQLRecord.RecordReference(Model: TSQLModel): TRecordReference; begin if (self=nil) or (fID<=0) then result := 0 else begin result := Model.GetTableIndexExisting(PSQLRecordClass(Self)^); if result>63 then // TRecordReference handle up to 64=1 shl 6 tables result := 0 else inc(result,fID shl 6); end; end; destructor TSQLRecord.Destroy; var i: integer; props: TSQLRecordProperties; begin props := RecordProps; if fFill<>nil then begin if fFill.fJoinedFields then // free all TSQLRecord instances created by TSQLRecord.CreateJoined for i := 0 to length(props.JoinedFields)-1 do props.JoinedFields[i].GetInstance(self).Free; fFill.Free; // call UnMap -> release fTable instance if necessary end; // free all TSQLRecordMany instances created by TSQLRecord.Create if props.ManyFields<>nil then for i := 0 to length(props.ManyFields)-1 do props.ManyFields[i].GetInstance(self).Free; // free any registered T*ObjArray if props.DynArrayFieldsHasObjArray then for i := 0 to length(props.DynArrayFields)-1 do with props.DynArrayFields[i] do if ObjArray<>nil then ObjArrayClear(fPropInfo^.GetFieldAddr(self)^); inherited; end; function TSQLRecord.SimplePropertiesFill(const aSimpleFields: array of const): boolean; var i: integer; tmp: RawUTF8; begin if self=nil then result := false else // means error with RecordProps do if length(SimpleFields)<>length(aSimpleFields) then result := false else begin for i := 0 to high(aSimpleFields) do begin VarRecToUTF8(aSimpleFields[i],tmp); // will work for every handled type SimpleFields[i].SetValueVar(self,tmp,false); end; result := True; end; end; constructor TSQLRecord.CreateAndFillPrepare(aClient: TSQLRest; const aSQLWhere: RawUTF8; const aCustomFieldsCSV: RawUTF8=''); var aTable: TSQLTable; begin Create; aTable := aClient.MultiFieldValues(RecordClass,aCustomFieldsCSV,aSQLWhere); if aTable=nil then exit; aTable.OwnerMustFree := true; FillPrepare(aTable); end; constructor TSQLRecord.CreateAndFillPrepare(aClient: TSQLRest; const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const; const aCustomFieldsCSV: RawUTF8=''); var where: RawUTF8; begin where := FormatUTF8(FormatSQLWhere,[],BoundsSQLWhere); CreateAndFillPrepare(aClient,where,aCustomFieldsCSV); end; constructor TSQLRecord.CreateAndFillPrepare(aClient: TSQLRest; const FormatSQLWhere: RawUTF8; const ParamsSQLWhere, BoundsSQLWhere: array of const; const aCustomFieldsCSV: RawUTF8); var where: RawUTF8; begin where := FormatUTF8(FormatSQLWhere,ParamsSQLWhere,BoundsSQLWhere); CreateAndFillPrepare(aClient,where,aCustomFieldsCSV); end; constructor TSQLRecord.CreateAndFillPrepare(aClient: TSQLRest; const aIDs: array of Int64; const aCustomFieldsCSV: RawUTF8=''); begin Create; FillPrepare(aClient,aIDs,aCustomFieldsCSV); end; constructor TSQLRecord.CreateAndFillPrepare(const aJSON: RawUTF8); var aTable: TSQLTable; begin Create; aTable := TSQLTableJSON.CreateFromTables([RecordClass],'',aJSON); aTable.OwnerMustFree := true; FillPrepare(aTable); end; constructor TSQLRecord.CreateAndFillPrepare(aJSON: PUTF8Char; aJSONLen: integer); var aTable: TSQLTable; begin Create; aTable := TSQLTableJSON.CreateFromTables([RecordClass],'',aJSON,aJSONLen); aTable.OwnerMustFree := true; FillPrepare(aTable); end; constructor TSQLRecord.CreateAndFillPrepareJoined(aClient: TSQLRest; const aFormatSQLJoin: RawUTF8; const aParamsSQLJoin, aBoundsSQLJoin: array of const); var i,n: integer; props: TSQLModelRecordProperties; T: TSQLTableJSON; instance: TSQLRecord; SQL: RawUTF8; begin Create; props := aClient.Model.Props[PSQLRecordClass(Self)^]; if props.props.JoinedFields=nil then raise EORMException.CreateUTF8('No nested TSQLRecord to JOIN in %',[self]); SQL := props.SQL.SelectAllJoined; if aFormatSQLJoin<>'' then SQL := SQL+FormatUTF8(SQLFromWhere(aFormatSQLJoin),aParamsSQLJoin,aBoundsSQLJoin); T := aClient.ExecuteList(props.props.JoinedFieldsTable,SQL); if T=nil then exit; fFill := TSQLRecordFill.Create; fFill.fJoinedFields := True; fFill.fTable := T; fFill.fTable.OwnerMustFree := true; n := 0; with props.props do begin // follow SQL.SelectAllJoined columns fFill.AddMapSimpleFields(Self,SimpleFields,n); for i := 1 to length(JoinedFieldsTable)-1 do begin instance := JoinedFieldsTable[i].Create; JoinedFields[i-1].SetInstance(self,instance); fFill.AddMapSimpleFields(instance,JoinedFieldsTable[i].RecordProps.SimpleFields,n); end; end; fFill.fFillCurrentRow := 1; // point to first data row (0 is field names) end; constructor TSQLRecord.CreateJoined(aClient: TSQLRest; aID: TID); begin CreateAndFillPrepareJoined(aClient,'%.RowID=?',[RecordProps.SQLTableName],[aID]); FillOne; end; constructor TSQLRecord.CreateAndFillPrepareMany(aClient: TSQLRest; const aFormatSQLJoin: RawUTF8; const aParamsSQLJoin, aBoundsSQLJoin: array of const); begin Create; if Length(RecordProps.ManyFields)=0 then raise EModelException.CreateUTF8( '%.CreateAndFillPrepareMany() with no many-to-many fields',[self]); if not FillPrepareMany(aClient,aFormatSQLJoin,aParamsSQLJoin,aBoundsSQLJoin) then raise EModelException.CreateUTF8( '%.CreateAndFillPrepareMany(): FillPrepareMany() failure',[self]); end; function TSQLRecord.EnginePrepareMany(aClient: TSQLRest; const aFormatSQLJoin: RawUTF8; const aParamsSQLJoin, aBoundsSQLJoin: array of const; out ObjectsClass: TSQLRecordClassDynArray; out SQL: RawUTF8): RawUTF8; var aSQLFields, aSQLFrom, aSQLWhere, aSQLJoin: RawUTF8; aField: string[3]; aMany: RawUTF8; f, n, i, SQLFieldsCount: Integer; Props: TSQLRecordProperties; SQLFields: array of record SQL: string[3]; Prop: TSQLPropInfo; Instance: TSQLRecord; end; M: TSQLRecordMany; D: TSQLRecord; J,JBeg: PUTF8Char; Objects: array of TSQLRecord; function AddField(aProp: TSQLPropInfo): Boolean; begin if SQLFieldsCount>=MAX_SQLFIELDS then result := false else with SQLFields[SQLFieldsCount] do begin SQL := aField; Prop := aProp; Instance := Objects[f]; inc(SQLFieldsCount); result := true; end; end; function ProcessField(var P: PUTF8Char): RawUTF8; var B: PUTF8Char; field: TSQLPropInfo; i: integer; M: TSQLRecordMany; aManyField: string[63]; function GetManyField(F: PUTF8Char): boolean; var B: PUTF8Char; begin result := true; B := F; while tcIdentifier in TEXT_CHARS[F^] do inc(F); // go to end of sub-field name if B=F then begin result := false; exit; end; dec(B,2); // space for 'C.' SetString(aManyField,B,F-B); aManyField[2] := '.'; P := F; end; begin B := P; while tcIdentifier in TEXT_CHARS[P^] do inc(P); // go to end of field name FastSetString(result,B,P-B); if (result='') or IdemPropNameU(result,'AND') or IdemPropNameU(result,'OR') or IdemPropNameU(result,'LIKE') or IdemPropNameU(result,'NOT') or IdemPropNameU(result,'NULL') then exit; if not IsRowID(pointer(result)) then begin i := Props.Fields.IndexByName(result); if i<0 then exit; field := Props.Fields.List[i]; if field.SQLFieldType=sftMany then begin M := TSQLPropInfoRTTIInstance(field).GetInstance(self) as TSQLRecordMany; for i := 0 to n-1 do if Objects[i*2+1]=M then begin if IdemPChar(P,'.DEST.') then begin // special case of Many.Dest.* if GetManyField(P+6) then begin aManyField[1] := AnsiChar(i*2+67); result := RawUTF8(aManyField); exit; // Categories.Dest.Name=? -> C.Name=? end; end else if (P^='.') and GetManyField(P+1) then begin aManyField[1] := AnsiChar(i*2+66); result := RawUTF8(aManyField); exit; // Categories.Kind=? -> CC.Kind=? end; end; exit; end; end; result := 'A.'+result; // Owner=? -> A.Owner=? end; begin result := ''; FillClose; // so that no further FillOne will work if (self=nil) or (aClient=nil) then exit; // reset TSQLRecordFill object if fFill=nil then fFill := TSQLRecordFill.Create else fFill.UnMap; // compute generic joined SQL statement and initialize Objects*[]+SQLFields[] SetLength(SQLFields,MAX_SQLFIELDS); Props := RecordProps; n := Length(Props.ManyFields); if n=0 then exit; SetLength(Objects,n*2+1); SetLength(ObjectsClass,n*2+1); Objects[0] := self; ObjectsClass[0] := PSQLRecordClass(self)^; SetLength(fFill.fTableMapRecordManyInstances,n); // fFill.UnMap will release memory for f := 0 to n-1 do begin M := TSQLRecordMany(Props.ManyFields[f].GetInstance(self)); if M=nil then raise EORMException.CreateUTF8('%.Create should have created %:% for EnginePrepareMany', [self,Props.ManyFields[f].Name,Props.ManyFields[f].ObjectClass]); fFill.fTableMapRecordManyInstances[f] := M; Objects[f*2+1] := M; ObjectsClass[f*2+1] := PSQLRecordClass(M)^; with M.RecordProps do begin if (fRecordManySourceProp.ObjectClass<>PClass(self)^) or (fRecordManyDestProp.ObjectClass=nil) then raise EORMException.CreateUTF8('%.EnginePrepareMany %:% mismatch', [self,Props.ManyFields[f].Name,Props.ManyFields[f].ObjectClass]); ObjectsClass[f*2+2] := TSQLRecordClass(fRecordManyDestProp.ObjectClass); D := TSQLRecordClass(fRecordManyDestProp.ObjectClass).Create; // let TSQLRecordMany.Source and Dest point to real instances M.fSourceID^ := PtrInt(self); M.fDestID^ := PtrInt(D); end; Objects[f*2+2] := TSQLRecord(M.fDestID^); if Props.fSQLFillPrepareMany='' then begin aMany := AnsiChar(f*2+66); // Many=B,D,F... if aSQLWhere<>'' then aSQLWhere := aSQLWhere+' and '; aSQLWhere := FormatUTF8('%%.Source=A.RowID and %.Dest=%.RowID', [aSQLWhere,aMany,aMany,AnsiChar(f*2+67){Dest=C,E,G..}]); end; end; SQLFieldsCount := 0; aField := 'A00'; for f := 0 to length(ObjectsClass)-1 do with ObjectsClass[f].RecordProps do begin PWord(@aField[2])^ := ord('I')+ord('D')shl 8; if not AddField(nil) then Exit; // try to add the ID field if Props.fSQLFillPrepareMany='' then begin if aSQLFields<>'' then aSQLFields := aSQLFields+','; aSQLFields := FormatUTF8('%%.RowID %',[aSQLFields,aField[1],aField]); end; for i := 0 to length(SimpleFields)-1 do with SimpleFields[i] do begin if (f and 1=0) {self/dest} or not(IdemPropNameU(Name,'SOURCE') or IdemPropNameU(Name,'DEST')) {many} then begin PWord(@aField[2])^ := TwoDigitLookupW[i]; if not AddField(SimpleFields[i]) then Exit; // try to add this simple field if Props.fSQLFillPrepareMany='' then aSQLFields := FormatUTF8('%,%.% %',[aSQLFields,aField[1],Name,aField]); end; end; if Props.fSQLFillPrepareMany='' then begin if aSQLFrom<>'' then aSQLFrom := aSQLFrom+','; aSQLFrom := aSQLFrom+SQLTableName+' '+ToUTF8(aField[1]); end; inc(aField[1]); end; if Props.fSQLFillPrepareMany<>'' then SQL := Props.fSQLFillPrepareMany else begin FormatUTF8('select % from % where %',[aSQLFields,aSQLFrom,aSQLWhere],SQL); Props.fSQLFillPrepareMany := SQL; end; // process aFormatSQLJoin,aParamsSQLJoin and aBoundsSQLJoin parameters if aFormatSQLJoin<>'' then begin aSQLWhere := ''; FormatUTF8(aFormatSQLJoin,aParamsSQLJoin,aSQLJoin); JBeg := pointer(aSQLJoin); repeat J := JBeg; while not (tcIdentifier in TEXT_CHARS[J^]) do begin case J^ of '"': repeat inc(J) until J^ in [#0,'"']; '''': repeat inc(J) until J^ in [#0,'''']; end; if J^=#0 then break; inc(J); end; if J<>JBeg then begin // append ' ',')'.. FastSetString(aSQLFrom,JBeg,J-JBeg); aSQLWhere := aSQLWhere+aSQLFrom; JBeg := J; end; if J^=#0 then break; aSQLWhere := aSQLWhere+ProcessField(JBeg); until JBeg^=#0; SQL := SQL+' and ('+FormatUTF8(aSQLWhere,[],aBoundsSQLJoin)+')'; end; // execute SQL statement and retrieve the matching data result := aClient.EngineList(SQL); if result<>'' then // prepare Fill mapping on success - see FillPrepareMany() for i := 0 to SQLFieldsCount-1 do with SQLFields[i] do fFill.AddMap(Instance,Prop,i); end; function TSQLRecord.FillPrepareMany(aClient: TSQLRest; const aFormatSQLJoin: RawUTF8; const aParamsSQLJoin, aBoundsSQLJoin: array of const): boolean; var JSON,SQL: RawUTF8; ObjectsClass: TSQLRecordClassDynArray; T: TSQLTable; begin result := false; JSON := EnginePrepareMany(aClient,aFormatSQLJoin,aParamsSQLJoin,aBoundsSQLJoin, ObjectsClass,SQL); if JSON='' then exit; T := TSQLTableJSON.CreateFromTables(ObjectsClass,SQL,JSON); if (T=nil) or (T.fResults=nil) then begin T.Free; exit; end; { assert(T.FieldCount=SQLFieldsCount); for i := 0 to SQLFieldsCount-1 do assert(IdemPropName(SQLFields[i].SQL,T.fResults[i],StrLen(T.fResults[i]))); } fFill.fTable := T; T.OwnerMustFree := true; fFill.fFillCurrentRow := 1; // point to first data row (0 is field names) result := true; end; function TSQLRecord.GetID: TID; begin {$ifdef MSWINDOWS} if PtrUInt(self)MaxInt then raise EORMException.CreateUTF8('%.GetIDAsPointer is storing ID=%, which '+ 'cannot be stored in a pointer/TSQLRecord 32-bit instance: use '+ 'a TID/T*ID published field for 64-bit IDs',[self,fID]) else {$endif CPU64} result := pointer(PtrInt(fID)); {$else} if PtrUInt(self)<$100000 then // rough estimation, but works in practice result := self else try result := pointer(PtrInt(fID)); except result := self; end; {$endif MSWINDOWS} end; class procedure TSQLRecord.InternalRegisterCustomProperties(Props: TSQLRecordProperties); begin // do nothing by default end; class procedure TSQLRecord.InternalDefineModel(Props: TSQLRecordProperties); begin // do nothing by default end; function TSQLRecord.GetHasBlob: boolean; begin if Self=nil then result := false else result := RecordProps.BlobFields<>nil; end; function TSQLRecord.GetSimpleFieldCount: integer; begin if Self=nil then result := 0 else result := length(RecordProps.SimpleFields); end; function TSQLRecord.GetFillCurrentRow: integer; begin if (self=nil) or (fFill=nil) then result := 0 else result := fFill.FillCurrentRow; end; function TSQLRecord.GetFillReachedEnd: boolean; begin result := (self=nil) or (fFill=nil) or (fFill.Table.fRowCount=0) or (cardinal(fFill.FillCurrentRow)>cardinal(fFill.Table.fRowCount)); end; function TSQLRecord.GetTable: TSQLTable; begin if (self=nil) or (fFill=nil) then result := nil else result := fFill.Table; end; function TSQLRecord.GetFieldValue(const PropName: RawUTF8): RawUTF8; var P: TSQLPropInfo; begin result := ''; if self=nil then exit; P := RecordProps.Fields.ByName(pointer(PropName)); if P<>nil then P.GetValueVar(self,False,result,nil); end; procedure TSQLRecord.SetFieldValue(const PropName: RawUTF8; Value: PUTF8Char); var P: TSQLPropInfo; begin if self=nil then exit; P := RecordProps.Fields.ByName(pointer(PropName)); if P<>nil then P.SetValue(self,Value,false); end; {$ifndef NOVARIANTS} function TSQLRecord.GetAsDocVariant(withID: boolean; const withFields: TSQLFieldBits; options: PDocVariantOptions; replaceRowIDWithID: boolean): variant; begin GetAsDocVariant(withID,withFields,result,options,replaceRowIDWithID); end; procedure TSQLRecord.GetAsDocVariant(withID: boolean; const withFields: TSQLFieldBits; var result: variant; options: PDocVariantOptions; replaceRowIDWithID: boolean); const _ID: array[boolean] of RawUTF8 = ('RowID','ID'); var f,i: integer; Fields: TSQLPropInfoList; intvalues: TRawUTF8Interning; doc: TDocVariantData absolute result; begin VarClear(result); if self=nil then exit; Fields := RecordProps.Fields; doc.InitFast(Fields.Count+1,dvObject); intvalues := nil; if options<>nil then begin // force options PDocVariantData(@result)^.Options := options^; if dvoInternValues in options^ then intvalues := DocVariantType.InternValues; end; if withID then doc.Values[doc.InternalAdd(_ID[replaceRowIDWithID])] := fID; for f := 0 to Fields.Count-1 do if f in withFields then begin i := doc.InternalAdd(Fields.List[f].Name); Fields.List[f].GetVariant(self,doc.Values[i]); if intvalues<>nil then // doc.Values[i] set manually -> manual interning intvalues.UniqueVariant(doc.Values[i]); end; end; function TSQLRecord.GetSimpleFieldsAsDocVariant(withID: boolean; options: PDocVariantOptions): variant; begin if self=nil then VarClear(result) else GetAsDocVariant(withID,RecordProps.SimpleFieldsBits[soSelect],result,options); end; function TSQLRecord.GetFieldVariant(const PropName: string): Variant; var P: TSQLPropInfo; begin if self=nil then P := nil else P := RecordProps.Fields.ByRawUTF8Name({$ifdef UNICODE}StringToUTF8{$endif}(PropName)); if P=nil then VarClear(result) else P.GetVariant(self,result); end; procedure TSQLRecord.SetFieldVariant(const PropName: string; const Source: Variant); var P: TSQLPropInfo; begin if self=nil then P := nil else P := RecordProps.Fields.ByRawUTF8Name({$ifdef UNICODE}StringToUTF8{$endif}(PropName)); if P<>nil then P.SetVariant(self,Source); end; {$endif NOVARIANTS} var vmtAutoTableLock: TRTLCriticalSection; // atomic set of the VMT AutoTable entry function PropsCreate(aTable: TSQLRecordClass): TSQLRecordProperties; var PVMT: pointer; begin // private sub function makes the code faster in most case if not aTable.InheritsFrom(TSQLRecord) then begin result := nil; // invalid call exit; end; EnterCriticalSection(vmtAutoTableLock); try // TSQLRecordProperties instance is stored into "AutoTable" unused VMT entry PVMT := pointer(PtrInt(PtrUInt(aTable))+vmtAutoTable); result := PPointer(PVMT)^; if result=nil then begin // protect from (unlikely) concurrent call // create the properties information from RTTI result := TSQLRecordProperties.Create(aTable); PatchCodePtrUInt(PVMT,PtrUInt(result),true); // LeaveUnprotected=true // register to the internal garbage collection (avoid memory leak) GarbageCollectorFreeAndNil(PVMT^,result); // set to nil at finalization // overriden method may use RecordProps -> do it after the VMT is set aTable.InternalDefineModel(result); end; finally LeaveCriticalSection(vmtAutoTableLock); end; end; // since "var class" are not available in Delphi 6-7, and is inherited by // the children classes under latest Delphi versions (i.e. the "var class" is // shared by all inherited classes, whereas we want one var per class), we reused // one of the unused magic VMT slots (i.e. the one for automated methods, // AutoTable, a relic from Delphi 2 that is generally not used anymore) - see // http://hallvards.blogspot.com/2007/05/hack17-virtual-class-variables-part-ii.html {$ifdef FPC_OR_PUREPASCAL} class function TSQLRecord.RecordProps: TSQLRecordProperties; begin if Self<>nil then begin result := PPointer(PtrInt(PtrUInt(Self))+vmtAutoTable)^; if result=nil then result := PropsCreate(self); end else result := nil; end; {$else} class function TSQLRecord.RecordProps: TSQLRecordProperties; asm test eax, eax jz @null mov edx, [eax + vmtAutoTable] test edx, edx jz PropsCreate mov eax, edx @null: end; {$endif} function TSQLRecord.Filter(const aFields: TSQLFieldBits): boolean; var f, i: integer; Value, Old: RawUTF8; begin result := IsZero(aFields); if (self=nil) or result then // avoid GPF and handle case if no field was selected exit; with RecordProps do if Filters=nil then // no filter set yet -> process OK result := true else begin for f := 0 to Fields.Count-1 do if (Fields.List[f].SQLFieldType in COPIABLE_FIELDS) then for i := 0 to length(Filters[f])-1 do if Filters[f,i].InheritsFrom(TSynFilter) then begin Fields.List[f].GetValueVar(self,false,Value,nil); Old := Value; TSynFilter(Filters[f,i]).Process(f,Value); if Old<>Value then // value was changed -> store modified Fields.List[f].SetValueVar(self,Value,false); end; end; end; function TSQLRecord.Filter(const aFields: array of RawUTF8): boolean; var F: TSQLFieldBits; begin if RecordProps.FieldBitsFromRawUTF8(aFields,F) then // must always call the virtual Filter() method result := Filter(F) else result := false; end; class function TSQLRecord.SQLTableName: RawUTF8; begin if self=nil then result := '' else result := RecordProps.SQLTableName; end; class function TSQLRecord.AutoFree(varClassPairs: array of pointer): IAutoFree; var n,i: integer; begin n := length(varClassPairs); if (n=0) or (n and 1=1) then exit; n := n shr 1; if n=0 then exit; for i := 0 to n-1 do // convert TSQLRecordClass into TSQLRecord instances varClassPairs[i*2+1] := TSQLRecordClass(varClassPairs[i*2+1]).Create; result := TAutoFree.Create(varClassPairs); end; class function TSQLRecord.AutoFree(var localVariable): IAutoFree; begin result := TAutoFree.Create(localVariable,Create); end; class function TSQLRecord.AutoFree(var localVariable; Rest: TSQLRest; ID: TID): IAutoFree; begin result := TAutoFree.Create(localVariable,Create(Rest,ID)); end; class function TSQLRecord.AutoFree(var localVariable; Rest: TSQLRest; const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const; const aCustomFieldsCSV: RawUTF8): IAutoFree; begin result := TAutoFree.Create(localVariable,CreateAndFillPrepare(Rest, FormatSQLWhere,BoundsSQLWhere,aCustomFieldsCSV)); end; class function TSQLRecord.AutoFree(var localVariable; Rest: TSQLRest; const FormatSQLWhere: RawUTF8; const ParamsSQLWhere,BoundsSQLWhere: array of const; const aCustomFieldsCSV: RawUTF8): IAutoFree; begin result := TAutoFree.Create(localVariable,CreateAndFillPrepare(Rest, FormatSQLWhere,ParamsSQLWhere,BoundsSQLWhere,aCustomFieldsCSV)); end; class procedure TSQLRecord.AddFilterOrValidate(const aFieldName: RawUTF8; aFilter: TSynFilterOrValidate); begin RecordProps.AddFilterOrValidate(aFieldName,aFilter); end; class procedure TSQLRecord.AddFilterNotVoidText(const aFieldNames: array of RawUTF8); var i,f: Integer; begin with RecordProps do for i := 0 to high(aFieldNames) do begin f := Fields.IndexByNameOrExcept(aFieldNames[i]); AddFilterOrValidate(f,TSynFilterTrim.Create); AddFilterOrValidate(f,TSynValidateNonVoidText.Create); end; end; class procedure TSQLRecord.AddFilterNotVoidAllTextFields; var f: Integer; begin with RecordProps,Fields do for f := 0 to Count-1 do if List[f].SQLFieldType in RAWTEXT_FIELDS then begin AddFilterOrValidate(f,TSynFilterTrim.Create); AddFilterOrValidate(f,TSynValidateNonVoidText.Create); end; end; function TSQLRecord.Validate(aRest: TSQLRest; const aFields: TSQLFieldBits; aInvalidFieldIndex: PInteger; aValidator: PSynValidate): string; var f, i: integer; Value: RawUTF8; Validate: TSynValidate; ValidateRest: TSynValidateRest absolute Validate; valid: boolean; begin result := ''; if (self=nil) or IsZero(aFields) then // avoid GPF and handle case if no field was selected exit; with RecordProps do if Filters<>nil then for f := 0 to Fields.Count-1 do if Fields.List[f].SQLFieldType in COPIABLE_FIELDS then begin for i := 0 to length(Filters[f])-1 do begin Validate := TSynValidate(Filters[f,i]); if Validate.InheritsFrom(TSynValidate) then begin if Value='' then Fields.List[f].GetValueVar(self,false,Value,nil); if Validate.InheritsFrom(TSynValidateRest) then valid := TSynValidateRest(Validate).Validate(f,Value,result, aRest, self) else valid := Validate.Process(f,Value,result); if not valid then begin // TSynValidate process failed -> notify caller if aInvalidFieldIndex<>nil then aInvalidFieldIndex^ := f; if aValidator<>nil then aValidator^ := Validate; if result='' then // no custom message -> show a default message result := format(sValidationFailed,[ GetCaptionFromClass(Validate.ClassType)]); exit; end; end; end; Value := ''; end; end; function TSQLRecord.Validate(aRest: TSQLRest; const aFields: array of RawUTF8; aInvalidFieldIndex: PInteger; aValidator: PSynValidate): string; var F: TSQLFieldBits; begin if RecordProps.FieldBitsFromRawUTF8(aFields,F) then // must always call the virtual Validate() method result := Validate(aRest,F,aInvalidFieldIndex,aValidator) else result := ''; end; function TSQLRecord.FilterAndValidate(aRest: TSQLRest; out aErrorMessage: string; const aFields: TSQLFieldBits; aValidator: PSynValidate): boolean; var invalidField: Integer; begin Filter(aFields); aErrorMessage := Validate(aRest,aFields,@invalidField,aValidator); if aErrorMessage='' then result := true else begin if invalidField>=0 then aErrorMessage := FormatString('"%": %', [RecordProps.Fields.List[invalidField].GetNameDisplay,aErrorMessage]); result := false; end; end; function TSQLRecord.FilterAndValidate(aRest: TSQLRest; const aFields: TSQLFieldBits; aValidator: PSynValidate): RawUTF8; var msg: string; begin if FilterAndValidate(aRest,msg,aFields,aValidator) then result := '' else StringToUTF8(msg,result); end; function TSQLRecord.DynArray(const DynArrayFieldName: RawUTF8): TDynArray; var F: integer; begin with RecordProps do for F := 0 to length(DynArrayFields)-1 do with DynArrayFields[F] do if IdemPropNameU(Name,DynArrayFieldName) then begin GetDynArray(self,result); exit; end; result.Void; end; function TSQLRecord.DynArray(DynArrayFieldIndex: integer): TDynArray; var F: integer; begin if DynArrayFieldIndex>0 then with RecordProps do for F := 0 to length(DynArrayFields)-1 do with DynArrayFields[F] do if DynArrayIndex=DynArrayFieldIndex then begin GetDynArray(self,result); exit; end; result.Void; end; procedure TSQLRecord.ComputeFieldsBeforeWrite(aRest: TSQLRest; aOccasion: TSQLEvent); var F: integer; types: TSQLFieldTypes; i64: Int64; p: TSQLPropInfo; begin if (self<>nil) and (aRest<>nil) then with RecordProps do begin integer(types) := 0; if sftModTime in HasTypeFields then include(types,sftModTime); if (sftCreateTime in HasTypeFields) and (aOccasion=seAdd) then include(types,sftCreateTime); if integer(types)<>0 then begin i64 := aRest.ServerTimestamp; for F := 0 to Fields.Count-1 do begin p := Fields.List[f]; if p.SQLFieldType in types then TSQLPropInfoRTTIInt64(p).fPropInfo.SetInt64Prop(Self,i64); end; end; if sftSessionUserID in HasTypeFields then begin i64 := aRest.GetCurrentSessionUserID; if i64<>0 then for F := 0 to Fields.Count-1 do begin p := Fields.List[f]; if p.SQLFieldType=sftSessionUserID then TSQLPropInfoRTTIInt64(p).fPropInfo.SetInt64Prop(Self,i64); end; end; end; end; { TSQLRecordNoCase } class procedure TSQLRecordNoCase.InternalDefineModel(Props: TSQLRecordProperties); begin Props.SetCustomCollationForAll(sftUTF8Text,'NOCASE'); end; { TSQLRecordCaseSensitive } class procedure TSQLRecordCaseSensitive.InternalDefineModel( Props: TSQLRecordProperties); begin Props.SetCustomCollationForAll(sftUTF8Text,'BINARY'); end; { TSQLRecordNoCaseExtended } class procedure TSQLRecordNoCaseExtended.InternalDefineModel(Props: TSQLRecordProperties); begin inherited InternalDefineModel(Props); // set NOCASE collation Props.SetVariantFieldsDocVariantOptions(JSON_OPTIONS_FAST_EXTENDED); end; { TSQLRecordCaseSensitiveExtended } class procedure TSQLRecordCaseSensitiveExtended.InternalDefineModel( Props: TSQLRecordProperties); begin inherited InternalDefineModel(Props); // set BINARY collation Props.SetVariantFieldsDocVariantOptions(JSON_OPTIONS_FAST_EXTENDED); end; { TSQLRecordNoCaseExtendedNoID } class procedure TSQLRecordNoCaseExtendedNoID.InitializeTable(Server: TSQLRestServer; const FieldName: RawUTF8; Options: TSQLInitializeTableOptions); begin inherited InitializeTable(Server, FieldName, Options + [itoNoIndex4TID]); end; { TSQLRecordPropertiesMapping } procedure TSQLRecordPropertiesMapping.Init(Table: TSQLRecordClass; const MappedTableName: RawUTF8; MappedConnection: TObject; AutoComputeSQL: boolean; MappingOptions: TSQLRecordPropertiesMappingOptions); begin fOptions := MappingOptions; fProps := Table.RecordProps; if MappedTableName='' then fTableName := fProps.SQLTableName else fTableName := MappedTableName; fConnectionProperties := MappedConnection; fRowIDFieldName := 'ID'; fProps.Fields.NamesToRawUTF8DynArray(fExtFieldNames); fProps.Fields.NamesToRawUTF8DynArray(fExtFieldNamesUnQuotedSQL); FillcharFast(fFieldNamesMatchInternal,SizeOf(fFieldNamesMatchInternal),255); fAutoComputeSQL := AutoComputeSQL; fMappingVersion := 1; if fAutoComputeSQL then ComputeSQL; end; function TSQLRecordPropertiesMapping.MapField( const InternalName, ExternalName: RawUTF8): PSQLRecordPropertiesMapping; begin MapFields([InternalName,ExternalName]); result := @self; end; function TSQLRecordPropertiesMapping.MapFields( const InternalExternalPairs: array of RawUTF8): PSQLRecordPropertiesMapping; var i,int: Integer; begin for i := 0 to (length(InternalExternalPairs) shr 1)-1 do begin int := fProps.Fields.IndexByNameOrExcept(InternalExternalPairs[i*2]); if int<0 then begin fRowIDFieldName := InternalExternalPairs[i*2+1]; if IdemPropNameU(fRowIDFieldName,'ID') then include(fFieldNamesMatchInternal,0) else // [0]=ID exclude(fFieldNamesMatchInternal,0); end else begin fExtFieldNames[int] := InternalExternalPairs[i*2+1]; fExtFieldNamesUnQuotedSQL[int] := UnQuotedSQLSymbolName(fExtFieldNames[int]); if IdemPropNameU(fExtFieldNames[int],fProps.Fields.List[int].Name) then include(fFieldNamesMatchInternal,int+1) else // [0]=ID [1..n]=fields[i-1] exclude(fFieldNamesMatchInternal,int+1); end; end; inc(fMappingVersion); if fAutoComputeSQL then ComputeSQL; result := @self; end; function TSQLRecordPropertiesMapping.MapAutoKeywordFields: PSQLRecordPropertiesMapping; begin if @self<>nil then include(fOptions,rpmAutoMapKeywordFields); result := @self; end; function TSQLRecordPropertiesMapping.SetOptions( aOptions: TSQLRecordPropertiesMappingOptions): PSQLRecordPropertiesMapping; begin if @self<>nil then fOptions := aOptions; result := @self; end; procedure TSQLRecordPropertiesMapping.ComputeSQL; type // similar to TSQLModelRecordProperties.Create()/SetKind() TContent = (TableSimpleFields, UpdateSimple, UpdateSetAll, InsertAll); procedure SetSQL(W: TTextWriter; withID, withTableName: boolean; var result: RawUTF8; content: TContent=TableSimpleFields); var f: integer; begin W.CancelAll; if withID and (content=TableSimpleFields) then begin if withTableName then W.AddStrings([TableName,'.']); W.AddString(RowIDFieldName); if 0 in FieldNamesMatchInternal then W.Add(',') else W.AddShort(' as ID,'); end; with fProps do for f := 0 to Fields.Count-1 do with Fields.List[f] do if SQLFieldType in COPIABLE_FIELDS then // sftMany fields do not exist case content of TableSimpleFields: if f in SimpleFieldsBits[soSelect] then begin if withTableName then W.AddStrings([TableName,'.']); W.AddString(ExtFieldNames[f]); if not(f+1 in FieldNamesMatchInternal) then W.AddStrings([' as ',Name]); // to get expected JSON column name W.Add(','); end; UpdateSimple: if f in SimpleFieldsBits[soSelect] then W.AddStrings([ExtFieldNames[f],'=?,']); UpdateSetAll: W.AddStrings([ExtFieldNames[f],'=?,']); InsertAll: W.AddStrings([ExtFieldNames[f],',']); end; W.CancelLastComma; W.SetText(result); end; var W: TTextWriter; temp: TTextWriterStackBuffer; begin W := TTextWriter.CreateOwnedStream(temp); try // SQL.TableSimpleFields[withID: boolean; withTableName: boolean] SetSQL(W,false,false,fSQL.TableSimpleFields[false,false]); SetSQL(W,false,true,fSQL.TableSimpleFields[false,true]); SetSQL(W,true,false,fSQL.TableSimpleFields[true,false]); SetSQL(W,true,true,fSQL.TableSimpleFields[true,true]); // SQL.SelectAll: array[withRowID: boolean] fSQL.SelectAllWithRowID := SQLFromSelect(TableName,'*','', fSQL.TableSimpleFields[true,false]); fSQL.SelectAllWithID := fSQL.SelectAllWithRowID; SetSQL(W,false,false,fSQL.UpdateSetSimple,UpdateSimple); SetSQL(W,false,false,fSQL.UpdateSetAll,UpdateSetAll); SetSQL(W,false,false,fSQL.InsertSet,InsertAll); finally W.Free; end; end; function TSQLRecordPropertiesMapping.InternalToExternal(const FieldName: RawUTF8): RawUTF8; var int: integer; begin int := fProps.Fields.IndexByNameOrExcept(FieldName); if int<0 then result := RowIDFieldName else result := fExtFieldNames[int]; end; function TSQLRecordPropertiesMapping.InternalCSVToExternalCSV( const CSVFieldNames, Sep, SepEnd: RawUTF8): RawUTF8; var IntFields,ExtFields: TRawUTF8DynArray; begin CSVToRawUTF8DynArray(CSVFieldNames,Sep,SepEnd,IntFields); InternalToExternalDynArray(IntFields,ExtFields); result := RawUTF8ArrayToCSV(ExtFields,Sep)+SepEnd; end; procedure TSQLRecordPropertiesMapping.InternalToExternalDynArray( const IntFieldNames: array of RawUTF8; out result: TRawUTF8DynArray; IntFieldIndex: PIntegerDynArray); var i,n,ndx: integer; begin n := length(IntFieldNames); SetLength(result,n); if IntFieldIndex<>nil then SetLength(IntFieldIndex^,n); for i := 0 to n-1 do begin ndx := fProps.Fields.IndexByNameOrExcept(IntFieldNames[i]); if IntFieldIndex<>nil then IntFieldIndex^[i] := ndx; if ndx<0 then result[i] := RowIDFieldName else result[i] := fExtFieldNames[ndx]; end; end; function TSQLRecordPropertiesMapping.ExternalToInternalIndex( const ExtFieldName: RawUTF8): integer; begin if IdemPropNameU(ExtFieldName,RowIDFieldName) then result := -1 else begin // search for customized field mapping for result := 0 to length(fExtFieldNamesUnQuotedSQL)-1 do if IdemPropNameU(ExtFieldName,fExtFieldNamesUnQuotedSQL[result]) then exit; result := -2; // indicates not found end; end; function TSQLRecordPropertiesMapping.ExternalToInternalOrNull( const ExtFieldName: RawUTF8): RawUTF8; var i: integer; begin i := ExternalToInternalIndex(ExtFieldName); if i=-1 then result := 'ID' else if i>=0 then result := fProps.Fields.List[i].Name else result := ''; // indicates not found end; function TSQLRecordPropertiesMapping.AppendFieldName( FieldIndex: Integer; var Text: RawUTF8): boolean; begin result := false; // success if FieldIndex=VIRTUAL_TABLE_ROWID_COLUMN then Text := Text+RowIDFieldName else if cardinal(FieldIndex)>=cardinal(Length(ExtFieldNames)) then result := true else // FieldIndex out of range Text := Text+ExtFieldNames[FieldIndex]; end; function TSQLRecordPropertiesMapping.FieldNameByIndex(FieldIndex: Integer): RawUTF8; begin if FieldIndex=VIRTUAL_TABLE_ROWID_COLUMN then result := RowIDFieldName else if cardinal(FieldIndex)>=cardinal(Length(ExtFieldNames)) then result := '' else // FieldIndex out of range result := ExtFieldNames[FieldIndex]; end; { TSQLModelRecordProperties } constructor TSQLModelRecordProperties.Create(aModel: TSQLModel; aTable: TSQLRecordClass; aKind: TSQLRecordVirtualKind); var f: integer; begin // similar to TSQLRecordPropertiesMapping.ComputeSQL fModel := aModel; fTableIndex := fModel.GetTableIndexExisting(aTable); fProps := aTable.RecordProps; SetKind(aKind); with Props do for f := 0 to Fields.Count-1 do with Fields.List[f] do if SQLFieldType in COPIABLE_FIELDS then begin // sftMany fields do not exist // pre-computation of SQL statements SQL.UpdateSetAll := SQL.UpdateSetAll+Name+'=?,'; SQL.InsertSet := SQL.InsertSet+Name+','; if f in SimpleFieldsBits[soUpdate] then SQL.UpdateSetSimple := SQL.UpdateSetSimple+Name+'=?,'; // filter + validation of unique fields, i.e. if marked as "stored false" if f in IsUniqueFieldsBits then begin // must trim() text value before storage, and validate for unicity if SQLFieldType in [sftUTF8Text,sftAnsiText] then AddFilterOrValidate(f,TSynFilterTrim.Create); // register unique field pre-validation AddFilterOrValidate(f,TSynValidateUniqueField.Create); end; end; SetLength(SQL.InsertSet,length(SQL.InsertSet)-1); SetLength(SQL.UpdateSetAll,length(SQL.UpdateSetAll)-1); // 'COL1=?,COL2=?' if SQL.UpdateSetSimple<>'' then SetLength(SQL.UpdateSetSimple,length(SQL.UpdateSetSimple)-1); // 'COL1=?,COL2=?' Props.InternalRegisterModel(aModel,aModel.GetTableIndexExisting(aTable),self); end; constructor TSQLModelRecordProperties.CreateFrom(aModel: TSQLModel; aSource: TSQLModelRecordProperties); begin inherited Create; fModel := aModel; fTableIndex := aSource.fTableIndex; fFTSWithoutContentTableIndex := aSource.fFTSWithoutContentTableIndex; fFTSWithoutContentFields := aSource.fFTSWithoutContentFields; fProps := aSource.fProps; fKind := aSource.Kind; SQL := aSource.SQL; ExternalDB := aSource.ExternalDB; Props.InternalRegisterModel(fModel,fModel.GetTableIndexExisting(fProps.Table),self); end; procedure TSQLModelRecordProperties.SetKind(Value: TSQLRecordVirtualKind); function IntSQLTableSimpleFields(withID, withTableName: boolean): RawUTF8; const IDComma: array[TSQLRecordVirtualKind] of rawUTF8 = ('ID,','RowID,','RowID,','RowID,','RowID,','RowID,','RowID,','RowID,'); // rSQLite3, rFTS3, rFTS4, rFTS5, rRTree, rRTreeInteger, rCustomForcedID, rCustomAutoID var TableName: RawUTF8; i: integer; begin if withTableName then TableName := Props.SQLTableName+'.'; // calc TableName once if withID then if withTableName then result := TableName+IDComma[Kind] else result := IDComma[Kind] else result := ''; for i := 0 to length(Props.SimpleFields)-1 do begin if withTableName then result := result+TableName; result := result+Props.SimpleFields[i].Name+','; // valid simple fields end; if result<>'' then SetLength(result,length(result)-1); // trim last ',' end; var f: integer; expected: TSQLFieldType; begin case Value of // validates virtual table fields expectations rFTS3, rFTS4, rFTS5: begin if Props.Fields.Count=0 then raise EModelException.CreateUTF8( 'Virtual FTS class % should have published properties',[Props.Table]); for f := 0 to Props.Fields.Count-1 do with Props.Fields.List[f] do if SQLFieldTypeStored<>sftUTF8Text then raise EModelException.CreateUTF8('%.%: FTS field must be RawUTF8',[Props.Table,Name]) end; rRTree, rRTreeInteger: begin Props.RTreeCoordBoundaryFields := 0; if Value=rRTree then expected := sftFloat else expected := sftInteger; for f := 0 to Props.Fields.Count-1 do with Props.Fields.List[f] do if aAuxiliaryRTreeField in Attributes then // https://sqlite.org/rtree.html#auxiliary_columns expected := sftUnknown // will expect further columns to be auxiliary else if SQLFieldTypeStored<>expected then raise EModelException.CreateUTF8('%.%: RTREE field must be %', [Props.Table,Name,ToText(expected)^]) else inc(Props.RTreeCoordBoundaryFields); if (Props.RTreeCoordBoundaryFields<2) or (Props.RTreeCoordBoundaryFields>RTREE_MAX_DIMENSION*2) or (Props.RTreeCoordBoundaryFields and 1<>0) then raise EModelException.CreateUTF8('% has % fields: RTREE expects 2,4,6..% boundary columns', [Props.Table,Props.RTreeCoordBoundaryFields,RTREE_MAX_DIMENSION*2]); end; end; fKind := Value; // SQL.TableSimpleFields[withID: boolean; withTableName: boolean] SQL.TableSimpleFields[false,false] := IntSQLTableSimpleFields(false,false); SQL.TableSimpleFields[false,true] := IntSQLTableSimpleFields(false,true); SQL.TableSimpleFields[true,false] := IntSQLTableSimpleFields(true,false); SQL.TableSimpleFields[true,true] := IntSQLTableSimpleFields(true,true); if Props.SQLTableSimpleFieldsNoRowID<>SQL.TableSimpleFields[false,false] then raise EModelException.CreateUTF8('SetKind(%)',[Props.Table]); SQL.SelectAllWithRowID := SQLFromSelectWhere('*',''); SQL.SelectAllWithID := SQL.SelectAllWithRowID; if IdemPChar(PUTF8Char(pointer(SQL.SelectAllWithID))+7,'ROWID') then delete(SQL.SelectAllWithID,8,3); // 'SELECT RowID,..' -> 'SELECT ID,' end; function TSQLModelRecordProperties.SQLFromSelectWhere( const SelectFields, Where: RawUTF8): RawUTF8; begin result := SQLFromSelect(Props.SQLTableName,SelectFields,Where, SQL.TableSimpleFields[true,false]); end; procedure TSQLModelRecordProperties.FTS4WithoutContent(ContentTable: TSQLRecordClass); var i: integer; field: RawUTF8; begin if not(Kind in [rFTS4,rFTS5]) then raise EModelException.CreateUTF8('FTS4WithoutContent: % is not a FTS4/5 table',[Props.Table]); fFTSWithoutContentTableIndex := fModel.GetTableIndexExisting(ContentTable); for i := 0 to Props.Fields.Count-1 do begin field := Props.Fields.List[i].Name; if ContentTable.RecordProps.Fields.IndexByName(field)<0 then raise EModelException.CreateUTF8('FTS4WithoutContent: %.% is not a % field', [Props.Table,field,ContentTable]); fFTSWithoutContentFields := fFTSWithoutContentFields+',new.'+field; end; if fFTSWithoutContentFields='' then raise EModelException.CreateUTF8('FTS4WithoutContent: % has no field',[Props.Table]); end; function TSQLModelRecordProperties.GetProp(const PropName: RawUTF8): TSQLPropInfo; begin if self<>nil then result := Props.Fields.ByName(pointer(PropName)) else result := nil; end; { TSQLModel } function TSQLModel.GetTableIndexSafe(aTable: TSQLRecordClass; RaiseExceptionIfNotExisting: boolean): integer; begin for result := 0 to fTablesMax do // manual search: GetTableIndex() may fail if fTables[result]=aTable then exit; if RaiseExceptionIfNotExisting then raise EModelException.CreateUTF8('% must include %',[self,aTable]); result := -1; end; procedure TSQLModel.SetTableProps(aIndex: integer); var j,f: integer; t: TSQLFieldType; Kind: TSQLRecordVirtualKind; Table, TableID: TSQLRecordClass; aTableName,aFieldName: RawUTF8; Props: TSQLModelRecordProperties; fields: TSQLPropInfoList; W: TTextWriter; procedure RegisterTableForRecordReference(aFieldType: TSQLPropInfo; aFieldTable: TClass); var R: integer; begin if (aFieldTable=nil) or (aFieldTable=TSQLRecord) or not aFieldTable.InheritsFrom(TSQLRecord) then exit; // no associated table to track deletion R := length(fRecordReferences); SetLength(fRecordReferences,R+1); with fRecordReferences[R] do begin TableIndex := aIndex; FieldType := aFieldType; FieldTable := pointer(aFieldTable); FieldTableIndex := GetTableIndexSafe(FieldTable,false); if FieldTableIndex<0 then FieldTableIndex := -2; // allow lazy table index identification if aFieldType.InheritsFrom(TSQLPropInfoRTTIRecordReference) then CascadeDelete := TSQLPropInfoRTTIRecordReference(aFieldType).CascadeDelete; end; end; begin if (cardinal(aIndex)>cardinal(fTablesMax)) or (fTableProps[aIndex]<>nil) then raise EModelException.Create('TSQLModel.SetTableProps'); Table := fTables[aIndex]; if Table.InheritsFrom(TSQLRecordFTS5) then Kind := rFTS5 else if Table.InheritsFrom(TSQLRecordFTS4) then Kind := rFTS4 else if Table.InheritsFrom(TSQLRecordFTS3) then Kind := rFTS3 else if Table.InheritsFrom(TSQLRecordVirtualTableForcedID) then Kind := rCustomForcedID else if Table.InheritsFrom(TSQLRecordRTree) then Kind := rRTree else if Table.InheritsFrom(TSQLRecordRTreeInteger) then Kind := rRTreeInteger else if Table.InheritsFrom(TSQLRecordVirtual) then Kind := rCustomAutoID else Kind := rSQLite3; Props := TSQLModelRecordProperties.Create(self,Table,Kind); Props.Props.InternalRegisterModel(Self,aIndex,Props); for t := low(t) to high(t) do if fCustomCollationForAll[t]<>'' then Props.Props.SetCustomCollationForAll(t,fCustomCollationForAll[t]); fTableProps[aIndex] := Props; aTableName := Props.Props.SQLTableName; UpperCaseCopy(aTableName,fSortedTablesNameUpper[aIndex]); fSortedTablesNameIndex[aIndex] := aIndex; fields := Props.Props.Fields; for f := 0 to fields.Count-1 do case fields.List[f].SQLFieldType of sftRecord: RegisterTableForRecordReference(fields.List[f],Table); // Table not used sftID: RegisterTableForRecordReference( fields.List[f],(fields.List[f] as TSQLPropInfoRTTIInstance).ObjectClass); sftTID: begin TableID := (fields.List[f] as TSQLPropInfoRTTITID).RecordClass; if TableID=nil then // T*ID name didn't match any TSQLRecord type fields.List[f].fSQLFieldType := sftInteger else RegisterTableForRecordReference(fields.List[f],TableID); end; sftMany: GetTableIndexSafe(pointer((fields.List[f] as TSQLPropInfoRTTIMany).ObjectClass),true); end; if Props.Props.JoinedFieldsTable<>nil then begin W := TTextWriter.CreateOwnedStream; try W.AddShort('SELECT '); // JoinedFieldsTable[0] is the class itself with Props.Props do begin W.Add('%.RowID as `%.RowID`,',[SQLTableName,SQLTableName]); for f := 0 to length(SimpleFields)-1 do if SimpleFields[f].SQLFieldType<>sftID then W.Add('%.% as `%.%`,',[SQLTableName,SimpleFields[f].Name, SQLTableName,SimpleFields[f].Name]); end; // add JoinedFieldsTable[1..] fields for j := 1 to high(Props.Props.JoinedFieldsTable) do begin aFieldName := Props.Props.JoinedFields[j-1].Name; W.Add('%.RowID as `%.RowID`,',[aFieldName,aFieldName]); with Props.Props.JoinedFieldsTable[j].RecordProps do for f := 0 to High(SimpleFields) do if SimpleFields[f].SQLFieldType<>sftID then W.Add('%.% as `%.%`,',[aFieldName,SimpleFields[f].Name, aFieldName,SimpleFields[f].Name]); end; W.CancelLastComma; // add LEFT JOIN clause W.AddStrings([' FROM ',aTableName]); for j := 1 to high(Props.Props.JoinedFieldsTable) do begin aFieldName := Props.Props.JoinedFields[j-1].Name; with Props.Props.JoinedFieldsTable[j].RecordProps do W.Add(' LEFT JOIN % AS % ON %.%=%.RowID',[ SQLTableName,aFieldName,aTableName,aFieldName,aFieldName]); end; W.SetText(Props.SQL.SelectAllJoined); finally W.Free; end; end; end; function TSQLModel.GetTableProps(aClass: TSQLRecordClass): TSQLModelRecordProperties; begin result := fTableProps[GetTableIndexExisting(aClass)]; end; function TSQLModel.AddTable(aTable: TSQLRecordClass; aTableIndexCreated: PInteger=nil): boolean; var n: integer; begin // first register for JSONToObject() and for TSQLPropInfoRTTITID.Create() TJSONSerializer.RegisterClassForJSON(aTable); // insert only once if GetTableIndex(aTable)>=0 then begin result := false; exit; end; // add to the model list inc(fTablesMax); n := fTablesMax+1; SetLength(fTables,n); SetLength(fSortedTablesNameUpper,n); SetLength(fSortedTablesNameIndex,n); SetLength(fTableProps,n); fTables[fTablesMax] := aTable; SetTableProps(fTablesMax); QuickSortRawUTF8(fSortedTablesNameUpper,fTablesMax+1,@fSortedTablesNameIndex); if aTableIndexCreated<>nil then aTableIndexCreated^ := fTablesMax; result := true; end; function TSQLModel.AddTableInherited(aTable: TSQLRecordClass): pointer; var ndx: integer; begin ndx := GetTableIndexInheritsFrom(aTable); if ndx<0 then if not AddTable(aTable,@ndx) then raise EModelException.CreateUTF8('%.AddTableInherited(%)',[self,aTable]); result := Tables[ndx]; end; function TSQLModel.GetTableInherited(aTable: TSQLRecordClass): TSQLRecordClass; var ndx: integer; begin ndx := GetTableIndexInheritsFrom(aTable); if ndx<0 then result := aTable else result := Tables[ndx]; end; constructor TSQLModel.Create(CloneFrom: TSQLModel); var i: integer; begin if CloneFrom=nil then raise EModelException.CreateUTF8('%.Create(CloneFrom=nil)',[self]); fTables := CloneFrom.fTables; fTablesMax := CloneFrom.fTablesMax; if fTablesMax<>High(fTables) then raise EModelException.CreateUTF8('%.Create: incorrect CloneFrom.TableMax',[self]); SetRoot(CloneFrom.fRoot); fActions := CloneFrom.fActions; fEvents := CloneFrom.fEvents; fRestOwner := CloneFrom.fRestOwner; fSortedTablesNameUpper := CloneFrom.fSortedTablesNameUpper; fSortedTablesNameIndex := CloneFrom.fSortedTablesNameIndex; fRecordReferences := CloneFrom.fRecordReferences; fVirtualTableModule := CloneFrom.fVirtualTableModule; fCustomCollationForAll := CloneFrom.fCustomCollationForAll; SetLength(fTableProps,fTablesMax+1); for i := 0 to fTablesMax do fTableProps[i] := TSQLModelRecordProperties.CreateFrom( self,CloneFrom.fTableProps[i]); end; constructor TSQLModel.Create(Owner: TSQLRest; TabParameters: PSQLRibbonTabParameters; TabParametersCount, TabParametersSize: integer; const NonVisibleTables: array of TSQLRecordClass; Actions, Events: PTypeInfo; const aRoot: RawUTF8); var i: integer; Tables: array of TSQLRecordClass; begin if (TabParameters=nil) or (TabParametersCount<=0) or (cardinal(TabParametersSize)'') and (aRoot[length(aRoot)]='/') then fRoot := copy(aRoot,1,Length(aRoot)-1) else fRoot := aRoot; UpperCaseCopy(fRoot,fRootUpper); end; constructor TSQLModel.Create(const Tables: array of TSQLRecordClass; const aRoot: RawUTF8); var N, i: integer; begin N := length(Tables); if N>SizeOf(SUPERVISOR_ACCESS_RIGHTS.Get)*8 then // TSQLAccessRights bits size raise EModelException.CreateUTF8('% % has too many Tables: %>%', [self,aRoot,N,SizeOf(SUPERVISOR_ACCESS_RIGHTS.Get)*8]); // e.g. N>64 // set the Tables to be associated with this Model, as TSQLRecord classes fTablesMax := N-1; SetLength(fTables,N); MoveFast(Tables[0],fTables[0],N*SizeOf(Tables[0])); for i := 0 to N-1 do // first register for JSONToObject() and for TSQLPropInfoRTTITID.Create() TJSONSerializer.RegisterClassForJSON(Tables[i]); SetLength(fSortedTablesNameUpper,N); SetLength(fSortedTablesNameIndex,N); SetLength(fTableProps,N); // initialize internal properties for i := 0 to fTablesMax do SetTableProps(i); QuickSortRawUTF8(fSortedTablesNameUpper,fTablesMax+1,@fSortedTablesNameIndex); // set the optional Root URI path of this Model if aRoot<>'' then SetRoot(aRoot); end; function TSQLModel.GetIsUnique(aTable: TSQLRecordClass; aFieldIndex: integer): boolean; var i: integer; begin i := GetTableIndex(aTable); if (i<0) or (Cardinal(aFieldIndex)>=MAX_SQLFIELDS) then result := false else result := aFieldIndex in TableProps[i].Props.IsUniqueFieldsBits; end; function GetTableNameFromSQLSelect(const SQL: RawUTF8; EnsureUniqueTableInFrom: boolean): RawUTF8; var i,j,k: integer; begin i := PosI(' FROM ',SQL); if i>0 then begin inc(i,6); while SQL[i] in [#1..' '] do inc(i); j := 0; while tcIdentifier in TEXT_CHARS[SQL[i+j]] do inc(j); if cardinal(j-1)<64 then begin k := i+j; while SQL[k] in [#1..' '] do inc(k); if not EnsureUniqueTableInFrom or (SQL[k]<>',') then begin FastSetString(result,PAnsiChar(PtrInt(SQL)+i-1),j); exit; end; end; end; result := ''; end; function GetTableNamesFromSQLSelect(const SQL: RawUTF8): TRawUTF8DynArray; var i,j,k,n: integer; begin result := nil; n := 0; i := PosI(' FROM ',SQL); if i>0 then begin inc(i,6); repeat while SQL[i] in [#1..' '] do inc(i); j := 0; while tcIdentifier in TEXT_CHARS[SQL[i+j]] do inc(j); if cardinal(j-1)>64 then begin result := nil; exit; // seems too big end; k := i+j; while SQL[k] in [#1..' '] do inc(k); SetLength(result,n+1); FastSetString(result[n],PAnsiChar(PtrInt(SQL)+i-1),j); inc(n); if SQL[k]<>',' then break; i := k+1; until false; end; end; function TSQLModel.GetTableIndexFromSQLSelect(const SQL: RawUTF8; EnsureUniqueTableInFrom: boolean): integer; var TableName: RawUTF8; begin TableName := GetTableNameFromSQLSelect(SQL,EnsureUniqueTableInFrom); result := GetTableIndex(TableName); end; function TSQLModel.GetTableIndexesFromSQLSelect(const SQL: RawUTF8): TIntegerDynArray; var TableNames: TRawUTF8DynArray; i,t,n,ndx: integer; begin result := nil; TableNames := GetTableNamesFromSQLSelect(SQL); t := length(TableNames); if t=0 then exit; SetLength(result,t); n := 0; for i := 0 to t-1 do begin ndx := GetTableIndex(TableNames[i]); if ndx<0 then continue; result[n] := ndx; inc(n); end; if n<>t then SetLength(result,n); end; function TSQLModel.GetTablesFromSQLSelect(const SQL: RawUTF8): TSQLRecordClassDynArray; var t: TIntegerDynArray; n,i: integer; begin result := nil; t := GetTableIndexesFromSQLSelect(SQL); n := length(t); if n=0 then exit; SetLength(result,n); for i := 0 to n-1 do result[i] := Tables[t[i]]; end; function TSQLModel.GetTable(const SQLTableName: RawUTF8): TSQLRecordClass; var i: integer; begin i := GetTableIndex(SQLTableName); if i>=0 then result := Tables[i] else result := nil; end; function TSQLModel.GetTableExactClass(const TableName: RawUTF8): TSQLRecordClass; var i: integer; begin i := GetTableExactIndex(TableName); if i>=0 then result := Tables[i] else result := nil; end; function TSQLModel.GetTableIndex(aTable: TSQLRecordClass): integer; var i: PtrInt; Props: TSQLRecordProperties; c: PSQLRecordClass; begin if (self<>nil) and (aTable<>nil) then begin Props := PPointer(PtrInt(PtrUInt(aTable))+vmtAutoTable)^; if (Props<>nil) and (Props.fModelMaxnil) and (aTable<>nil) and (aTable<>TSQLRecord) then for result := 0 to fTablesMax do if Tables[result].InheritsFrom(aTable) then exit; result := -1; end; function TSQLModel.GetTableIndexExisting(aTable: TSQLRecordClass): integer; begin if self=nil then raise EModelException.Create('nil.GetTableIndexExisting'); if aTable=nil then raise EModelException.CreateUTF8('%.GetTableIndexExisting(nil) %',[self,Root]); result := GetTableIndex(aTable); if result<0 then raise EModelException.CreateUTF8('% is not part of % %',[aTable,self,Root]); end; function TSQLModel.GetTableExactIndex(const TableName: RawUTF8): integer; var L: integer; begin if self<>nil then begin L := length(TableName); for result := 0 to fTablesMax do if Tables[result]<>nil then // avoid GPF if IdemPropName( // new TObject.ClassName is UnicodeString (Delphi 20009) -> inline code // using vmtClassName = UTF-8 encoded text stored as shortstring PShortString(PPointer(PtrInt(PtrUInt(Tables[result]))+vmtClassName)^)^, pointer(TableName),L) then exit; // case insensitive search end; result := -1; end; function TSQLModel.GetTableIndex(const SQLTableName: RawUTF8): integer; begin if (self<>nil) and (SQLTableName<>'') then begin result := FastFindUpperPUTF8CharSorted( // O(log(n)) binary search pointer(fSortedTablesNameUpper),fTablesMax,pointer(SQLTableName),length(SQLTableName)); if result>=0 then result := fSortedTablesNameIndex[result]; end else result := -1; end; function TSQLModel.GetTableIndexPtr(SQLTableName: PUTF8Char): integer; begin if (self<>nil) and (SQLTableName<>nil) then begin result := FastFindUpperPUTF8CharSorted( // O(log(n)) binary search pointer(fSortedTablesNameUpper),fTablesMax,SQLTableName,StrLen(SQLTableName)); if result>=0 then result := fSortedTablesNameIndex[result]; end else result := -1; end; function TSQLModel.getURI(aTable: TSQLRecordClass): RawUTF8; begin result := ''; if self=nil then exit; if aTable<>nil then result := aTable.RecordProps.SQLTableName else begin result := Root; exit; end; if Root<>'' then result := Root+'/'+result; end; function TSQLModel.URIMatch(const URI: RawUTF8): TSQLRestModelMatch; var URILen: integer; begin result := rmNoMatch; if (self=nil) or (fRoot='') or (URI='') then exit; if IdemPChar(pointer(URI),pointer(fRootUpper)) then begin URILen := length(fRoot); if URI[URILen+1] in [#0,'/','?'] then if CompareMemFixed(pointer(URI),pointer(fRoot),URILen) then result := rmMatchExact else result := rmMatchWithCaseChange; end; end; function TSQLModel.SQLFromSelectWhere(const Tables: array of TSQLRecordClass; const SQLSelect, SQLWhere: RawUTF8): RawUTF8; var i: integer; aProps: array[0..31] of TSQLModelRecordProperties; begin if self=nil then raise EORMException.Create('Model required'); if high(Tables)=0 then begin // fastest common call with one TSQLRecordClass result := Props[Tables[0]].SQLFromSelectWhere(SQLSelect,SQLWhere); exit; end; // 'SELECT T1.F1,T1.F2,T1.F3,T2.F1,T2.F2 FROM T1,T2 WHERE ..' e.g. if cardinal(high(Tables))>high(aProps) then raise EModelException.CreateUTF8('%.SQLFromSelectWhere() up to % Tables[]', [self,Length(aProps)]); for i := 0 to high(Tables) do aProps[i] := Props[Tables[i]]; // raise EModelException if not found if SQLSelect='*' then // don't send BLOB values to query: retrieve all other fields if high(Tables)=0 then result := 'SELECT '+aProps[0].SQL.TableSimpleFields[true,false] else begin result := 'SELECT '+aProps[0].SQL.TableSimpleFields[true,true]; for i := 1 to high(Tables) do result := result+','+aProps[i].SQL.TableSimpleFields[true,true]; end else result := 'SELECT '+SQLSelect; result := result+' FROM '+aProps[0].Props.SQLTableName; for i := 1 to high(Tables) do result := result+','+aProps[i].Props.SQLTableName; result := result+SQLFromWhere(SQLWhere); end; procedure TSQLModel.SetCustomCollationForAll(aFieldType: TSQLFieldType; const aCollationName: RawUTF8); var i: integer; begin if self=nil then exit; if fCustomCollationForAll[aFieldType]<>'' then raise EModelException.CreateUTF8('%.SetCustomCollationForAll(%)'+ ' shall be called only once',[self,aCollationName]); fCustomCollationForAll[aFieldType] := aCollationName; for i := 0 to high(fTableProps) do fTableProps[i].fProps.SetCustomCollationForAll(aFieldType,aCollationName); end; procedure TSQLModel.SetMaxLengthValidatorForAllTextFields(IndexIsUTF8Length: boolean); var i: integer; begin if self<>nil then for i := 0 to high(fTableProps) do fTableProps[i].fProps.SetMaxLengthValidatorForTextFields(IndexIsUTF8Length); end; procedure TSQLModel.SetMaxLengthFilterForAllTextFields(IndexIsUTF8Length: boolean); var i: integer; begin if self<>nil then for i := 0 to high(fTableProps) do fTableProps[i].fProps.SetMaxLengthFilterForTextFields(IndexIsUTF8Length); end; {$ifndef NOVARIANTS} procedure TSQLModel.SetVariantFieldsDocVariantOptions(const Options: TDocVariantOptions); var i: integer; begin if self<>nil then for i := 0 to high(fTableProps) do fTableProps[i].fProps.SetVariantFieldsDocVariantOptions(Options); end; {$endif} function TSQLModel.SetIDGenerator(aTable: TSQLRecordClass; aIdentifier: TSynUniqueIdentifierProcess; const aSharedObfuscationKey: RawUTF8): TSynUniqueIdentifierGenerator; var i: integer; begin i := GetTableIndexExisting(aTable); if i>=length(fIDGenerator) then SetLength(fIDGenerator,fTablesMax+1); result := TSynUniqueIdentifierGenerator.Create(aIdentifier,aSharedObfuscationKey); fIDGenerator[i].Free; fIDGenerator[i] := result; end; function TSQLModel.GetIDGenerator(aTable: TSQLRecordClass): TSynUniqueIdentifierGenerator; var i: cardinal; begin i := GetTableIndexExisting(aTable); if icardinal(fTablesMax)) then result := '' else result := Tables[aTableIndex].GetSQLCreate(self); end; function TSQLModel.GetSQLAddField(aTableIndex: integer; aFieldIndex: integer): RawUTF8; begin if (self=nil) or (cardinal(aTableIndex)>cardinal(fTablesMax)) then result := '' else result := TableProps[aTableIndex].Props.SQLAddField(aFieldIndex); end; function TSQLModel.isLocked(aTable: TSQLRecordClass; aID: TID): boolean; begin result := GetLocks(aTable)^.isLocked(aID); end; function TSQLModel.isLocked(aRec: TSQLRecord): boolean; begin if aRec=nil then result := false else result := isLocked(PSQLRecordClass(aRec)^,aRec.fID); end; function TSQLModel.Lock(aTable: TSQLRecordClass; aID: TID): boolean; begin if self=nil then result := false else begin if fLocks=nil then SetLength(fLocks,fTablesMax+1); // initialize fLocks[] if necessary result := GetLocks(aTable)^.Lock(aID); end; end; function TSQLModel.Lock(aTableIndex: integer; aID: TID): boolean; begin if (self=nil) or (Cardinal(aTableIndex)>cardinal(fTablesMax)) then result := false else begin if fLocks=nil then SetLength(fLocks,fTablesMax+1); // initialize fLocks[] if necessary result := fLocks[aTableIndex].Lock(aID); end; end; function TSQLModel.Lock(aRec: TSQLRecord): boolean; begin if aRec=nil then result := false else result := Lock(PSQLRecordClass(aRec)^,aRec.fID); end; procedure TSQLModel.PurgeOlderThan(MinutesFromNow: cardinal); var i: PtrInt; begin if fLocks<>nil then for i := 0 to length(fLocks)-1 do fLocks[i].PurgeOlderThan(MinutesFromNow); end; function TSQLModel.UnLock(aTable: TSQLRecordClass; aID: TID): boolean; begin if (self=nil) or (fLocks=nil) then result := false else result := GetLocks(aTable)^.UnLock(aID); end; function TSQLModel.UnLock(aTableIndex: integer; aID: TID): boolean; begin if (self=nil) or (cardinal(aTableIndex)>=cardinal(length(fLocks))) then result := false else result := fLocks[aTableIndex].UnLock(aID); end; function TSQLModel.UnLock(aRec: TSQLRecord): boolean; begin if aRec=nil then result := false else result := UnLock(PSQLRecordClass(aRec)^,aRec.fID); end; function TSQLModel.GetLocks(aTable: TSQLRecordClass): PSQLLocks; begin if (self=nil) or (fLocks=nil) then result := nil else result := @fLocks[GetTableIndexExisting(aTable)]; end; procedure TSQLModel.UnLockAll; var i: PtrInt; begin for i := 0 to length(fLocks)-1 do fLocks[i].Count := 0; end; function TSQLModel.getURIID(aTable: TSQLRecordClass; aID: TID): RawUTF8; begin result := getURI(aTable); if aID>0 then result := result+'/'+Int64ToUtf8(aID); end; function TSQLModel.getURICallBack(const aMethodName: RawUTF8; aTable: TSQLRecordClass; aID: TID): RawUTF8; begin result := getURIID(aTable,aID)+'/'+aMethodName; end; function TSQLModel.ActionName(const Action): string; begin if (Self=nil) or (fActions=nil) then result := '' else result := TSQLRecord.CaptionNameFromRTTI(fActions^.GetEnumName(byte(Action))); end; function TSQLModel.EventName(const Event): string; begin if (Self=nil) or (fEvents=nil) then result := '' else result := TSQLRecord.CaptionNameFromRTTI(fEvents^.GetEnumName(byte(Event))); end; function TSQLModel.RecordReference(Table: TSQLRecordClass; ID: TID): TRecordReference; begin if (self=nil) or (ID<=0) then result := 0 else begin result := GetTableIndexExisting(Table); if result>63 then // TRecordReference handle up to 64=1 shl 6 tables result := 0 else inc(result,ID shl 6); end; end; function TSQLModel.RecordReferenceTable(const Ref: TRecordReference): TSQLRecordClass; var i: integer; begin i := Ref and 63; if i<=fTablesMax then result := fTables[i] else result := nil; end; function TSQLModel.VirtualTableRegister(aClass: TSQLRecordClass; aModule: TSQLVirtualTableClass; const aExternalTableName: RawUTF8; aExternalDataBase: TObject; aMappingOptions: TSQLRecordPropertiesMappingOptions): boolean; var i: integer; begin result := false; if aClass=nil then exit; i := GetTableIndexExisting(aClass); with TableProps[i] do begin if not (Kind in IS_CUSTOM_VIRTUAL) then if Kind=rSQLite3 then SetKind(rCustomAutoID) else // SetKind() recompute all SQL raise EModelException.CreateUTF8('Invalid %.VirtualTableRegister(%) call: '+ 'impossible to set class as virtual',[self,aClass]); ExternalDB.Init(aClass,aExternalTableName,aExternalDataBase,true,aMappingOptions); end; if high(fVirtualTableModule)<>fTablesMax then SetLength(fVirtualTableModule,fTablesMax+1); fVirtualTableModule[i] := aModule; result := true; end; function TSQLModel.VirtualTableModule(aClass: TSQLRecordClass): TSQLVirtualTableClass; var i: integer; begin result := nil; if (self=nil) or (fVirtualTableModule=nil) then exit; i := GetTableIndexExisting(aClass); if TableProps[i].Kind in IS_CUSTOM_VIRTUAL then result := fVirtualTableModule[i]; end; destructor TSQLModel.Destroy; var i,j: integer; begin for i := 0 to fTablesMax do begin with TableProps[i].Props do begin EnterCriticalSection(fLock); // may be called from several threads at once try for j := 0 to fModelMax do if fModel[j].Model=self then begin // un-associate this TSQLRecord with this model MoveFast(fModel[j+1],fModel[j],(fModelMax-j)*SizeOf(fModel[j])); dec(fModelMax); break; end; TableProps[i].Free; finally LeaveCriticalSection(fLock); end; end; end; ObjArrayClear(fIDGenerator); inherited; end; { TSQLRestBatch } constructor TSQLRestBatch.Create(aRest: TSQLRest; aTable: TSQLRecordClass; AutomaticTransactionPerRow: cardinal; Options: TSQLRestBatchOptions; InternalBufferSize: cardinal); begin if aRest=nil then raise EORMException.CreateUTF8('%.Create(aRest=nil)',[self]); fRest := aRest; if InternalBufferSize<4096 then InternalBufferSize := 4096; fInternalBufferSize := InternalBufferSize; Reset(aTable,AutomaticTransactionPerRow,Options); end; procedure TSQLRestBatch.Reset(aTable: TSQLRecordClass; AutomaticTransactionPerRow: cardinal; Options: TSQLRestBatchOptions); begin fBatch.Free; // full reset for SetExpandedJSONWriter fBatch := TJSONSerializer.CreateOwnedStream(fInternalBufferSize); fBatch.Expand := true; FillZero(fBatchFields); fBatchCount := 0; fAddCount := 0; fUpdateCount := 0; fDeleteCount := 0; fDeletedCount := 0; fTable := aTable; if aTable<>nil then begin fTableIndex := fRest.Model.GetTableIndexExisting(aTable); fBatch.Add('{'); // sending data is '{"Table":["cmd":values,...]}' fBatch.AddFieldName(aTable.SQLTableName); end else fTableIndex := -1; fBatch.Add('['); fAutomaticTransactionPerRow := AutomaticTransactionPerRow; if AutomaticTransactionPerRow>0 then begin // should be the first command fBatch.AddShort('"automaticTransactionPerRow",'); fBatch.Add(AutomaticTransactionPerRow); fBatch.Add(','); end; fOptions := Options; if boExtendedJSON in Options then include(fBatch.fCustomOptions,twoForceJSONExtended); Options := Options-[boExtendedJSON,boPostNoSimpleFields]; // client-side only if byte(Options)<>0 then begin fBatch.AddShort('"options",'); fBatch.Add(byte(Options)); fBatch.Add(','); end; end; procedure TSQLRestBatch.Reset; begin if self<>nil then Reset(fTable,fAutomaticTransactionPerRow,fOptions); end; destructor TSQLRestBatch.Destroy; begin FreeAndNil(fBatch); inherited; end; function TSQLRestBatch.GetCount: integer; begin if self=nil then result := 0 else result := fBatchCount; end; function TSQLRestBatch.GetSizeBytes: cardinal; begin if self=nil then result := 0 else result := fBatch.TextLength; end; procedure TSQLRestBatch.SetExpandedJSONWriter(Props: TSQLRecordProperties; ForceResetFields, withID: boolean; const WrittenFields: TSQLFieldBits); begin if (self=nil) or (fBatch=nil) then exit; if not ForceResetFields then if fBatch.Expand and (fBatch.WithID=withID) and IsEqual(fBatchFields,WrittenFields) then exit; // already set -> do not compute it again fBatchFields := WrittenFields; fBatch.ChangeExpandedFields(withID,FieldBitsToIndex(WrittenFields,Props.Fields.Count)); Props.SetJSONWriterColumnNames(fBatch,0); end; function TSQLRestBatch.RawAppend(FullRow: boolean): TTextWriter; begin if FullRow then inc(fBatchCount); result := fBatch; end; function TSQLRestBatch.RawAdd(const SentData: RawUTF8): integer; begin // '{"Table":[...,"POST",{object},...]}' if (fBatch=nil) or (fTable=nil) then raise EORMException.CreateUTF8('%.RawAdd %',[self,SentData]); fBatch.AddShort('"POST",'); fBatch.AddString(SentData); fBatch.Add(','); result := fBatchCount; inc(fBatchCount); inc(fAddCount); end; function TSQLRestBatch.RawUpdate(const SentData: RawUTF8; ID: TID): integer; var sentID: TID; begin // '{"Table":[...,"PUT",{object},...]}' if (fBatch=nil) or (fTable=nil) then raise EORMException.CreateUTF8('%.RawUpdate % %',[self,ID,SentData]); if JSONGetID(pointer(SentData),sentID) and (sentID<>ID) then raise EORMException.CreateUTF8('%.RawUpdate ID=% <> %',[self,ID,SentData]); fBatch.AddShort('"PUT",{ID:'); fBatch.Add(ID); fBatch.Add(','); fBatch.AddStringCopy(SentData,2,maxInt shr 2); fBatch.Add(','); result := fBatchCount; inc(fBatchCount); inc(fUpdateCount); end; function TSQLRestBatch.Add(Value: TSQLRecord; SendData,ForceID: boolean; const CustomFields: TSQLFieldBits; DoNotAutoComputeFields: boolean): integer; var Props: TSQLRecordProperties; FieldBits: TSQLFieldBits; PostSimpleFields: boolean; f: integer; begin result := -1; if (self=nil) or (Value=nil) or (fBatch=nil) then exit; // invalid parameters, or not opened BATCH sequence if (fTable<>nil) and (PSQLRecordClass(Value)^<>fTable) then exit; Props := Value.RecordProps; if SendData and (fRest.Model.Props[PSQLRecordClass(Value)^].Kind in INSERT_WITH_ID) then ForceID := true; // same format as TSQLRestClient.Add if SendData and not ForceID and IsZero(CustomFields) and not(boPostNoSimpleFields in fOptions) then begin PostSimpleFields := true; fBatch.AddShort('"SIMPLE'); end else begin PostSimpleFields := false; fBatch.AddShort('"POST'); end; if fTable<>nil then // '{"Table":[...,"POST",{object},...]}' fBatch.AddShort('",') else begin fBatch.Add('@'); // '[...,"POST@Table",{object}',...]' fBatch.AddString(Props.SQLTableName); fBatch.Add('"',','); end; if SendData then begin if IsZero(CustomFields) then FieldBits := Props.SimpleFieldsBits[soInsert] else if DoNotAutoComputeFields then FieldBits := CustomFields else FieldBits := CustomFields+Props.ComputeBeforeAddFieldsBits; SetExpandedJSONWriter(Props,fTablePreviousSendData<>PSQLRecordClass(Value)^, (Value.IDValue<>0) and ForceID,FieldBits); fTablePreviousSendData := PSQLRecordClass(Value)^; if not DoNotAutoComputeFields then // update TModTime/TCreateTime fields Value.ComputeFieldsBeforeWrite(fRest,seAdd); if PostSimpleFields then begin fBatch.Add('['); for f := 0 to length(Props.SimpleFields)-1 do begin Props.SimpleFields[f].GetJSONValues(Value,fBatch); fBatch.Add(','); end; fBatch.CancelLastComma; fBatch.Add(']'); end else Value.GetJSONValues(fBatch); if fCalledWithinRest and ForceID then fRest.fCache.Notify(Value,soInsert); end else fBatch.Add('{','}'); // '{"Table":[...,"POST",{},...]}' fBatch.Add(','); result := fBatchCount; inc(fBatchCount); inc(fAddCount); if Assigned(fOnWrite) then fOnWrite(self,soInsert,PSQLRecordClass(Value)^,Value.IDValue,Value,FieldBits); end; procedure AddID(var Values: TIDDynArray; var ValuesCount: integer; Value: TID); begin if ValuesCount=length(Values) then SetLength(Values,NextGrow(ValuesCount)); Values[ValuesCount] := Value; inc(ValuesCount); end; procedure AddID(var Values: TIDDynArray; Value: TID); var n: integer; begin n := length(Values); SetLength(Values,n+1); Values[n] := Value; end; function TSQLRestBatch.Delete(Table: TSQLRecordClass; ID: TID): integer; begin if (self=nil) or (fBatch=nil) or (Table=nil) or (ID<=0) or not fRest.RecordCanBeUpdated(Table,ID,seDelete) then begin result := -1; // invalid parameters, or not opened BATCH sequence exit; end; AddID(fDeletedRecordRef,fDeletedCount,fRest.Model.RecordReference(Table,ID)); fBatch.AddShort('"DELETE@'); // '[...,"DELETE@Table",ID,...]}' fBatch.AddString(Table.RecordProps.SQLTableName); fBatch.Add('"',','); fBatch.Add(ID); fBatch.Add(','); result := fBatchCount; inc(fBatchCount); inc(fDeleteCount); if Assigned(fOnWrite) then fOnWrite(self,soDelete,Table,ID,nil,[]); end; function TSQLRestBatch.Delete(ID: TID): integer; begin if (self=nil) or (fTable=nil) or (ID<=0) or not fRest.RecordCanBeUpdated(fTable,ID,seDelete) then begin result := -1; // invalid parameters, or not opened BATCH sequence exit; end; AddID(fDeletedRecordRef,fDeletedCount,RecordReference(fTableIndex,ID)); fBatch.AddShort('"DELETE",'); // '{"Table":[...,"DELETE",ID,...]}' fBatch.Add(ID); fBatch.Add(','); result := fBatchCount; inc(fBatchCount); inc(fDeleteCount); if Assigned(fOnWrite) then fOnWrite(self,soDelete,fTable,ID,nil,[]); end; function TSQLRestBatch.PrepareForSending(out Data: RawUTF8): boolean; var i: integer; begin if (self=nil) or (fBatch=nil) then // no opened BATCH sequence result := false else begin if fBatchCount>0 then begin // if something to send for i := 0 to fDeletedCount-1 do if fDeletedRecordRef[i]<>0 then fRest.Cache.NotifyDeletion(fDeletedRecordRef[i] and 63,fDeletedRecordRef[i] shr 6); fBatch.CancelLastComma; fBatch.Add(']'); if fTable<>nil then fBatch.Add('}'); // end sequence array '{"Table":["cmd":values,...]}' fBatch.SetText(Data); end; result := true; end; end; function TSQLRestBatch.Update(Value: TSQLRecord; const CustomFields: TSQLFieldBits; DoNotAutoComputeFields,ForceCacheUpdate: boolean): integer; var Props: TSQLRecordProperties; FieldBits: TSQLFieldBits; ID: TID; tableIndex: integer; begin result := -1; if (Value=nil) or (fBatch=nil) then exit; ID := Value.IDValue; if (ID<=0) or not fRest.RecordCanBeUpdated(Value.RecordClass,ID,seUpdate) then exit; // invalid parameters, or not opened BATCH sequence Props := Value.RecordProps; if fTable<>nil then if PSQLRecordClass(Value)^<>fTable then exit else begin // '{"Table":[...,"PUT",{object},...]}' tableIndex := fTableIndex; fBatch.AddShort('"PUT",'); end else begin tableIndex := fRest.Model.GetTableIndexExisting(Props.Table); fBatch.AddShort('"PUT@'); // '[...,"PUT@Table",{object}',...]' fBatch.AddString(Props.SQLTableName); fBatch.Add('"',','); end; // same format as TSQLRest.Update, BUT including the ID if IsZero(CustomFields) then Value.FillContext.ComputeSetUpdatedFieldBits(Props,FieldBits) else if DoNotAutoComputeFields then FieldBits := CustomFields*Props.CopiableFieldsBits else FieldBits := CustomFields*Props.CopiableFieldsBits+Props.FieldBits[sftModTime]; SetExpandedJSONWriter(Props,fTablePreviousSendData<>PSQLRecordClass(Value)^, {withID=}true,FieldBits); fTablePreviousSendData := PSQLRecordClass(Value)^; if not DoNotAutoComputeFields then Value.ComputeFieldsBeforeWrite(fRest,seUpdate); // update sftModTime fields Value.GetJSONValues(fBatch); fBatch.Add(','); if fCalledWithinRest and (FieldBits-Props.SimpleFieldsBits[soUpdate]=[]) then ForceCacheUpdate := true; // safe to update the cache with supplied values if ForceCacheUpdate then fRest.Cache.Notify(Value,soUpdate) else // may not contain all cached fields -> delete from cache AddID(fDeletedRecordRef,fDeletedCount,RecordReference(tableIndex,ID)); result := fBatchCount; inc(fBatchCount); inc(fUpdateCount); if Assigned(fOnWrite) then fOnWrite(self,soUpdate,PSQLRecordClass(Value)^,Value.IDValue,Value,FieldBits); end; function TSQLRestBatch.Update(Value: TSQLRecord; const CustomCSVFields: RawUTF8; DoNotAutoComputeFields,ForceCacheUpdate: boolean): integer; begin if (Value=nil) or (fBatch=nil) then result := -1 else result := Update(Value,Value.RecordProps.FieldBitsFromCSV(CustomCSVFields), DoNotAutoComputeFields,ForceCacheUpdate); end; { TSQLRestBatchLocked } constructor TSQLRestBatchLocked.Create(aRest: TSQLRest; aTable: TSQLRecordClass; AutomaticTransactionPerRow: cardinal; Options: TSQLRestBatchOptions; InternalBufferSize: cardinal); begin inherited; fSafe.Init; end; destructor TSQLRestBatchLocked.Destroy; begin fSafe.Done; inherited; end; procedure TSQLRestBatchLocked.Reset(aTable: TSQLRecordClass; AutomaticTransactionPerRow: cardinal; Options: TSQLRestBatchOptions); begin inherited; fTix := GetTickCount64; end; { TSQLRest } constructor TSQLRest.Create(aModel: TSQLModel); var cmd: TSQLRestServerURIContextCommand; begin inherited Create; fPrivateGarbageCollector := TSynObjectList.Create; fModel := aModel; for cmd := Low(cmd) to high(cmd) do fAcquireExecution[cmd] := TSQLRestAcquireExecution.Create; AcquireWriteMode := amLocked; AcquireWriteTimeOut := 5000; // default 5 seconds fRoutingClass := TSQLRestRoutingREST; {$ifdef WITHLOG} SetLogClass(SQLite3Log); // by default {$endif} end; destructor TSQLRest.Destroy; var cmd: TSQLRestServerURIContextCommand; i: integer; begin InternalLog('TSQLRest.Destroy %',[fModel.SafeRoot],sllInfo); // self->GPF AsynchBatchStop(nil); FreeAndNil(fBackgroundTimer); FreeAndNil(fServices); FreeAndNil(fCache); FreeAndNil(fCustomEncryptAES); if (fModel<>nil) and (fModel.fRestOwner=self) then // make sure we are the Owner (TSQLRestStorage has fModel<>nil e.g.) FreeAndNil(fModel); for cmd := Low(cmd) to high(cmd) do FreeAndNil(fAcquireExecution[cmd]); // should be done BEFORE private GC if fPrivateGarbageCollector<>nil then begin for i := fPrivateGarbageCollector.Count-1 downto 0 do // last in, first out try fPrivateGarbageCollector.Delete(i); // will call fPrivate...[i].Free except on Exception do ; // just ignore exceptions in such destructors end; fPrivateGarbageCollector.Free; end; inherited Destroy; end; var GlobalDefinitions: array of TSQLRestClass; class procedure TSQLRest.RegisterClassNameForDefinition; begin ObjArrayAddOnce(GlobalDefinitions,TObject(self)); // TClass stored as TObject end; procedure TSQLRest.DefinitionTo(Definition: TSynConnectionDefinition); begin if Definition<>nil then Definition.Kind := ClassName; end; function TSQLRest.DefinitionToJSON(Key: cardinal=0): RawUTF8; var Definition: TSynConnectionDefinition; begin Definition := TSynConnectionDefinition.Create; try Definition.Key := Key; DefinitionTo(Definition); result := Definition.SaveToJSON; finally Definition.Free; end; end; procedure TSQLRest.DefinitionToFile(const aJSONFile: TFileName; aKey: cardinal); begin FileFromString(JSONReformat(DefinitionToJSON(aKey)),aJSONFile); end; class function TSQLRest.ClassFrom(aDefinition: TSynConnectionDefinition): TSQLRestClass; var ndx: integer; begin for ndx := 0 to length(GlobalDefinitions)-1 do if GlobalDefinitions[ndx].ClassNameIs(aDefinition.Kind) then begin result := GlobalDefinitions[ndx]; exit; end; result := nil; end; constructor TSQLRest.RegisteredClassCreateFrom(aModel: TSQLModel; aDefinition: TSynConnectionDefinition); begin Create(aModel); end; class function TSQLRest.CreateFrom(aModel: TSQLModel; aDefinition: TSynConnectionDefinition): TSQLRest; var C: TSQLRestClass; begin C := ClassFrom(aDefinition); if C=nil then raise EORMException.CreateUTF8('%.CreateFrom: unknown % class - please '+ 'add a reference to its implementation unit',[self,aDefinition.Kind]); result := C.RegisteredClassCreateFrom(aModel,aDefinition); end; class function TSQLRest.CreateTryFrom(aModel: TSQLModel; aDefinition: TSynConnectionDefinition; aServerHandleAuthentication: boolean): TSQLRest; var C: TSQLRestClass; begin C := ClassFrom(aDefinition); if C=nil then result := nil else if C.InheritsFrom(TSQLRestServer) then result := TSQLRestServerClass(C).RegisteredClassCreateFrom( aModel,aServerHandleAuthentication,aDefinition) else result := C.RegisteredClassCreateFrom(aModel,aDefinition); end; class function TSQLRest.CreateFromJSON(aModel: TSQLModel; const aJSONDefinition: RawUTF8; aKey: cardinal): TSQLRest; var Definition: TSynConnectionDefinition; begin Definition := TSynConnectionDefinition.CreateFromJSON(aJSONDefinition,aKey); try result := CreateFrom(aModel,Definition); finally Definition.Free; end; end; class function TSQLRest.CreateFromFile(aModel: TSQLModel; const aJSONFile: TFileName; aKey: cardinal): TSQLRest; begin result := CreateFromJSON(aModel,AnyTextFileToRawUTF8(aJSONFile,true),aKey); end; procedure TSQLRest.InternalLog(const Text: RawUTF8; Level: TSynLogInfo); begin {$ifdef WITHLOG} if (self<>nil) and (fLogFamily<>nil) and (Level in fLogFamily.Level) then fLogFamily.SynLog.Log(Level,Text,self); {$endif} end; procedure TSQLRest.InternalLog(const Format: RawUTF8; const Args: array of const; Level: TSynLogInfo); begin {$ifdef WITHLOG} if (self<>nil) and (fLogFamily<>nil) and (Level in fLogFamily.Level) then fLogFamily.SynLog.Log(Level,Format,Args,self); {$endif} end; {$ifdef WITHLOG} procedure TSQLRest.SetLogClass(aClass: TSynLogClass); begin fLogClass := aClass; fLogFamily := fLogClass.Family; end; function TSQLRest.GetLogClass: TSynLogClass; begin if self=nil then result := SQLite3Log else result := fLogClass; end; {$endif WITHLOG} function TSQLRest.NewBackgroundThreadMethod(const Format: RawUTF8; const Args: array of const): TSynBackgroundThreadMethod; begin result := TSynBackgroundThreadMethod.Create(nil,FormatUTF8(Format,Args), BeginCurrentThread,EndCurrentThread); end; function TSQLRest.NewParallelProcess(ThreadCount: integer; const Format: RawUTF8; const Args: array of const): TSynParallelProcess; begin result := TSynParallelProcess.Create(ThreadCount,FormatUTF8(Format,Args), BeginCurrentThread,EndCurrentThread); end; function TSQLRest.NewBackgroundThreadProcess( aOnProcess: TOnSynBackgroundThreadProcess; aOnProcessMS: cardinal; const Format: RawUTF8; const Args: array of const; aStats: TSynMonitorClass): TSynBackgroundThreadProcess; var name: RawUTF8; begin FormatUTF8(Format,Args,name); if self=nil then result := TSynBackgroundThreadProcess.Create(name,aOnProcess,aOnProcessMS, nil,nil,aStats) else result := TSynBackgroundThreadProcess.Create(name,aOnProcess,aOnProcessMS, BeginCurrentThread,EndCurrentThread,aStats); end; function TSQLRest.EnsureBackgroundTimerExists: TSQLRestBackgroundTimer; begin if fBackgroundTimer=nil then fBackgroundTimer := TSQLRestBackgroundTimer.Create(self); result := fBackgroundTimer; end; function TSQLRest.TimerEnable(aOnProcess: TOnSynBackgroundTimerProcess; aOnProcessSecs: cardinal): TSynBackgroundTimer; begin result := nil; if self=nil then exit; if aOnProcessSecs=0 then begin TimerDisable(aOnProcess); exit; end; result := EnsureBackgroundTimerExists; result.Enable(aOnProcess,aOnProcessSecs); end; function TSQLRest.TimerDisable(aOnProcess: TOnSynBackgroundTimerProcess): boolean; begin if (self=nil) or (fBackgroundTimer=nil) then result := false else result := fBackgroundTimer.Disable(aOnProcess); end; function TSQLRest.AsynchBatchStart(Table: TSQLRecordClass; SendSeconds, PendingRowThreshold, AutomaticTransactionPerRow: integer; Options: TSQLRestBatchOptions): boolean; begin if self=nil then result := false else result := EnsureBackgroundTimerExists.AsynchBatchStart( Table,SendSeconds,PendingRowThreshold,AutomaticTransactionPerRow,Options); end; function TSQLRest.AsynchBatchStop(Table: TSQLRecordClass): boolean; begin if (self=nil) or (fBackgroundTimer=nil) or (fBackgroundTimer.fBackgroundBatch=nil) then result := false else result := fBackgroundTimer.AsynchBatchStop(Table); end; function TSQLRest.AsynchBatchAdd(Value: TSQLRecord; SendData,ForceID: boolean; const CustomFields: TSQLFieldBits; DoNotAutoComputeFields: boolean): integer; begin if (self=nil) or (fBackgroundTimer=nil) or (fBackgroundTimer.fBackgroundBatch=nil) then result := -1 else result := fBackgroundTimer.AsynchBatchAdd( Value,SendData,ForceID,CustomFields,DoNotAutoComputeFields); end; function TSQLRest.AsynchBatchRawAdd(Table: TSQLRecordClass; const SentData: RawUTF8): integer; begin if (self=nil) or (fBackgroundTimer=nil) or (fBackgroundTimer.fBackgroundBatch=nil) then result := -1 else result := fBackgroundTimer.AsynchBatchRawAdd(Table,SentData); end; procedure TSQLRest.AsynchBatchRawAppend(Table: TSQLRecordClass; SentData: TTextWriter); begin if (self<>nil) and (fBackgroundTimer<>nil) and (fBackgroundTimer.fBackgroundBatch<>nil) then fBackgroundTimer.AsynchBatchRawAppend(Table,SentData); end; function TSQLRest.AsynchBatchUpdate(Value: TSQLRecord; const CustomFields: TSQLFieldBits; DoNotAutoComputeFields: boolean): integer; begin if (self=nil) or (fBackgroundTimer=nil) or (fBackgroundTimer.fBackgroundBatch=nil) then result := -1 else result := fBackgroundTimer.AsynchBatchUpdate(Value,CustomFields,DoNotAutoComputeFields); end; function TSQLRest.AsynchBatchDelete(Table: TSQLRecordClass; ID: TID): integer; begin if (self=nil) or (fBackgroundTimer=nil) or (fBackgroundTimer.fBackgroundBatch=nil) then result := -1 else result := fBackgroundTimer.AsynchBatchDelete(Table,ID); end; procedure TSQLRest.AsynchRedirect(const aGUID: TGUID; const aDestinationInterface: IInvokable; out aCallbackInterface; const aOnResult: TOnAsynchRedirectResult); begin if self<>nil then EnsureBackgroundTimerExists.AsynchRedirect( aGUID,aDestinationInterface,aCallbackInterface,aOnResult); end; procedure TSQLRest.AsynchRedirect(const aGUID: TGUID; const aDestinationInstance: TInterfacedObject; out aCallbackInterface; const aOnResult: TOnAsynchRedirectResult); begin if self<>nil then EnsureBackgroundTimerExists.AsynchRedirect( aGUID,aDestinationInstance,aCallbackInterface,aOnResult); end; procedure TSQLRest.AsynchInterning(Interning: TRawUTF8Interning; InterningMaxRefCount, PeriodMinutes: integer); begin if self<>nil then EnsureBackgroundTimerExists.AsynchInterning( Interning,InterningMaxRefCount,PeriodMinutes); end; function TSQLRest.SystemUseTrack(periodSec: integer): TSystemUse; begin result := nil; if self=nil then exit; result := TSystemUse.Current; if (result.Timer=nil) or ((BackgroundTimer<>nil) and (result.Timer=BackgroundTimer)) then begin if periodSec>0 then result.Timer := EnsureBackgroundTimerExists; TimerEnable(result.BackgroundExecute,periodSec); // disable if periodSec=0 end; end; procedure DoSign(const signer: TSynSigner; const url,body: RawUTF8; bodylen: integer; out hash: THash512Rec); var L: integer; P: PAnsiChar; sign: TSynSigner; begin sign := signer; // thread-safe copy P := pointer(url); L := length(url); if P^='/' then begin inc(P); dec(L); end; sign.Update(P,L); sign.Update(pointer(body),bodylen); sign.Final(hash); end; procedure TSQLRest.InternalCustomDecrypt(Sender: TSQLRest; var Body,Head,Url: RawUTF8); var ct: RawUTF8; L: integer; hash: THash512Rec; begin if (fCustomEncryptContentPrefix='') or (Body='') or (Sender<>self) or (Url='') or IdemPChar(pointer(Url),pointer(fCustomEncryptUrlIgnore)) then exit; FindNameValue(Head,HEADER_CONTENT_TYPE_UPPER,ct); if IdemPChar(pointer(ct),pointer(fCustomEncryptContentPrefixUpper)) then begin if fCustomEncryptAES<>nil then begin // decrypt using PKCS7 + initial random/unique IV at the beginning Body := fCustomEncryptAES.DecryptPKCS7(Body,true,false); if Body='' then begin InternalLog('CustomEncrypt %.DecryptPKCS7 reject',[fCustomEncryptAES.ClassType],sllUserAuth); exit; end; end; if fCustomEncryptCompress<>nil then begin // optionally uncompresss Body+signature Body := fCustomEncryptCompress.Decompress(Body); if Body='' then begin InternalLog('CustomEncrypt %.Decompress reject %',[fCustomEncryptCompress.ClassType]); exit; end; end; L := length(Body)-fCustomEncryptSign.SignatureSize; if (L>0) and (fCustomEncryptSign.SignatureSize<>0) then begin // validate the binary signature of supplied Url+Body at the Body end DoSign(fCustomEncryptSign,Url,Body,L,hash); if not CompareMemFixed(@hash,@PByteArray(Body)[L],fCustomEncryptSign.SignatureSize) then begin Body := ''; InternalLog('CustomEncrypt % reject',[ToText(fCustomEncryptSign.Algo)^],sllUserAuth); exit; end; SetLength(Body,L); end; system.delete(ct,1,length(fCustomEncryptContentPrefix)); UpdateIniNameValue(Head,'',HEADER_CONTENT_TYPE_UPPER,ct); end else begin Body := ''; InternalLog('CustomEncrypt no % -> reject',[fCustomEncryptContentPrefix]); end; end; procedure TSQLRest.InternalCustomEncrypt(Sender: TSQLRest; var Body,Head,Url: RawUTF8); var ct: RawUTF8; L: integer; hash: THash512Rec; begin if (fCustomEncryptContentPrefix='') or (Body='') or (Sender<>self) or (Url='') or IdemPChar(pointer(Url),pointer(fCustomEncryptUrlIgnore)) then exit; if fCustomEncryptSign.SignatureSize<>0 then begin // append the binary signature of supplied Url+Body to the Body L := length(Body); DoSign(fCustomEncryptSign,Url,Body,L,hash); SetLength(Body,L+fCustomEncryptSign.SignatureSize); MoveSmall(@hash,@PByteArray(Body)[L],fCustomEncryptSign.SignatureSize); end; if fCustomEncryptCompress<>nil then // optionally compresss Body+signature Body := fCustomEncryptCompress.Compress(Body); if fCustomEncryptAES<>nil then // encrypt using PKCS7 + initial random/unique IV at the beginning Body := fCustomEncryptAES.EncryptPKCS7(Body,true); FindNameValue(Head,HEADER_CONTENT_TYPE_UPPER,ct); if ct='' then // not specified -> append 'application/json' ct := JSON_CONTENT_TYPE_VAR; UpdateIniNameValue(Head,HEADER_CONTENT_TYPE,HEADER_CONTENT_TYPE_UPPER, fCustomEncryptContentPrefix+ct) end; procedure TSQLRest.SetCustomEncryption(aes: TAESAbstract; sign: PSynSigner; comp: TAlgoCompress; const uriignore: RawUTF8); var tmp: PShortString; // temp variable to circumvent FPC bug s: RawUTF8; begin fCustomEncryptContentPrefix := ''; // disable encryption fCustomEncryptCompress := nil; fCustomEncryptUrlIgnore := ''; FreeAndNil(fCustomEncryptAES); fOnDecryptBody := nil; fOnEncryptBody := nil; fCustomEncryptAES := aes; if aes=nil then if sign=nil then exit else fCustomEncryptContentPrefix := '0' else begin tmp := ClassNameShort(aes); // TAESECB_API -> 'AESECB' FastSetString(s,@tmp^[2],6); fCustomEncryptContentPrefix := s+UInt32ToUtf8(aes.KeySize); end; if (sign=nil) or (sign^.SignatureSize=0) then begin FillCharFast(fCustomEncryptSign,SizeOf(fCustomEncryptSign),0); fCustomEncryptContentPrefix := fCustomEncryptContentPrefix+'0/'; end else begin fCustomEncryptSign := sign^; tmp := ToText(sign^.Algo); // saSha3384 -> 'sha3384' FastSetString(s,@tmp^[3],ord(tmp^[0])-2); fCustomEncryptContentPrefix := fCustomEncryptContentPrefix+s+'/'; end; fCustomEncryptContentPrefix := LowerCase(fCustomEncryptContentPrefix); fCustomEncryptContentPrefixUpper := UpperCase(fCustomEncryptContentPrefix); fCustomEncryptCompress := comp; fCustomEncryptUrlIgnore := UpperCase(uriignore); fOnDecryptBody := InternalCustomDecrypt; fOnEncryptBody := InternalCustomEncrypt; end; procedure TSQLRest.AdministrationExecute(const DatabaseName,SQL: RawUTF8; var result: TServiceCustomAnswer); begin if (SQL<>'') and (SQL[1]='#') then begin // pseudo SQL for a given TSQLRest[Server] instance case IdemPCharArray(@SQL[2],['TIME','MODEL','REST','HELP']) of 0: result.Content := Int64ToUtf8(ServerTimestamp); 1: result.Content := ObjectToJSON(Model); 2: result.Content := ObjectToJSON(self); 3: begin result.Content[length(result.Content)] := '|'; result.Content := result.Content+'#time|#model|#rest"'; end; end; end else if isSelect(pointer(SQL)) then result.Content := ExecuteJson(Model.GetTablesFromSQLSelect(SQL),SQL) else Execute(SQL); end; function TSQLRest.EngineUpdateFieldIncrement(TableModelIndex: integer; ID: TID; const FieldName: RawUTF8; Increment: Int64): boolean; var Value: Int64; Table: TSQLRecordClass; begin if (TableModelIndex<0) or (ID<0) then result := false else begin Table := Model.Tables[TableModelIndex]; result := OneFieldValue(Table,FieldName,'ID=?',[],[ID],Value) and UpdateField(Table,ID,FieldName,[Value+Increment]); end; end; procedure TSQLRest.SetRoutingClass(aServicesRouting: TSQLRestServerURIContextClass); begin if self<>nil then if aServicesRouting<>fRoutingClass then if (aServicesRouting=nil) or (aServicesRouting=TSQLRestServerURIContext) then raise EServiceException.CreateUTF8('Unexpected %.SetRoutingClass(%)', [self,aServicesRouting]) else fRoutingClass := aServicesRouting; end; function TSQLRest.MultiFieldValue(Table: TSQLRecordClass; const FieldName: array of RawUTF8; var FieldValue: array of RawUTF8; WhereID: TID): boolean; begin result := MultiFieldValue(Table,FieldName,FieldValue, 'RowID=:('+Int64ToUtf8(WhereID)+'):'); end; function TSQLRest.OneFieldValue(Table: TSQLRecordClass; const FieldName, WhereClause: RawUTF8): RawUTF8; var Res: array[0..0] of RawUTF8; begin if MultiFieldValue(Table,[FieldName],Res,WhereClause) then result := Res[0] else result := ''; end; function TSQLRest.OneFieldValueInt64(Table: TSQLRecordClass; const FieldName, WhereClause: RawUTF8; Default: Int64): Int64; var Res: array[0..0] of RawUTF8; begin if not MultiFieldValue(Table,[FieldName],Res,WhereClause) or not ToInt64(Res[0],result) then result := Default; end; function TSQLRest.OneFieldValue(Table: TSQLRecordClass; const FieldName: RawUTF8; const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const): RawUTF8; begin result := OneFieldValue(Table,FieldName,FormatUTF8(FormatSQLWhere,[],BoundsSQLWhere)); end; function TSQLRest.OneFieldValue(Table: TSQLRecordClass; const FieldName: RawUTF8; const WhereClauseFmt: RawUTF8; const Args, Bounds: array of const): RawUTF8; begin result := OneFieldValue(Table,FieldName,FormatUTF8(WhereClauseFmt,Args,Bounds)); end; function TSQLRest.OneFieldValue(Table: TSQLRecordClass; const FieldName: RawUTF8; WhereID: TID): RawUTF8; var Res: array[0..0] of RawUTF8; begin if (WhereID>0) and MultiFieldValue(Table,[FieldName],Res,'RowID=:('+Int64ToUtf8(WhereID)+'):') then result := Res[0] else result := ''; end; function TSQLRest.MemberExists(Table: TSQLRecordClass; ID: TID): boolean; begin if fCache.Retrieve(Model.GetTableIndexExisting(Table),ID)<>'' then result := true else result := OneFieldValue(Table,'RowID',ID)<>''; // try from DB end; function TSQLRest.OneFieldValue(Table: TSQLRecordClass; const FieldName: RawUTF8; const WhereClauseFmt: RawUTF8; const Args, Bounds: array of const; out Data: Int64): boolean; var Res: array[0..0] of RawUTF8; err: integer; where: RawUTF8; begin result := false; where := FormatUTF8(WhereClauseFmt,Args,Bounds); if MultiFieldValue(Table,[FieldName],Res,where) then if Res[0]<>'' then begin Data := GetInt64(pointer(Res[0]),err); if err=0 then result := true; end; end; function TSQLRest.OneFieldValues(Table: TSQLRecordClass; const FieldName, WhereClause: RawUTF8; out Data: TRawUTF8DynArray): boolean; var T: TSQLTableJSON; begin result := false; T := MultiFieldValues(Table,FieldName,WhereClause); if T<>nil then try result := T.GetRowValues(0,Data)>0; finally T.Free; end; end; function TSQLRest.OneFieldValues(Table: TSQLRecordClass; const FieldName, WhereClause: RawUTF8; Strings: TStrings; IDToIndex: PID=nil): Boolean; var Row: integer; aID: TID; T: TSQLTableJSON; begin result := false; if (Strings<>nil) and (self<>nil) and (Table<>nil) then try {$ifndef LVCL} Strings.BeginUpdate; {$endif} Strings.Clear; T := ExecuteList([Table], SQLFromSelect(Table.SQLTableName,'ID,'+FieldName,WhereClause,'')); if T<>nil then try if (T.FieldCount=2) and (T.fRowCount>0) then begin for Row := 1 to T.fRowCount do begin // ignore Row 0 i.e. field names aID := GetInt64(T.Get(Row,0)); Strings.AddObject(T.GetString(Row,1),pointer(PtrInt(aID))); if (IDToIndex<>nil) and (aID=IDToIndex^) then begin IDToIndex^ := Row-1; IDToIndex := nil; // set once end; end; result := true; end; finally T.Free; end; finally {$ifndef LVCL} Strings.EndUpdate; {$endif} end; if IDToIndex<>nil then IDToIndex^ := -1; // ID not found end; function TSQLRest.OneFieldValues(Table: TSQLRecordClass; const FieldName, WhereClause, Separator: RawUTF8): RawUTF8; var i, Len, SepLen, L: integer; Lens: TIntegerDynArray; T: TSQLTableJSON; P: PUTF8Char; begin result := ''; T := MultiFieldValues(Table,FieldName,WhereClause); if T<>nil then try if (T.FieldCount<>1) or (T.fRowCount<=0) then exit; // calculate row values CSV needed memory SetLength(Lens,T.fRowCount); SepLen := length(Separator); Len := 0; for i := 1 to T.fRowCount do begin L := StrLen(T.fResults[i]); // ignore fResults[0] i.e. field name inc(Len,L+SepLen); Lens[i-1] := L; end; dec(Len,SepLen); SetLength(result,Len); // add row values as CSV P := pointer(result); i := 1; repeat L := Lens[i-1]; if L<>0 then begin MoveFast(T.fResults[i]^,P^,L); inc(P,L); end; if i=T.fRowCount then break; MoveFast(pointer(Separator)^,P^,SepLen); inc(P,SepLen); inc(i); until false; //assert(P-pointer(result)=Len); finally T.Free; end; end; function TSQLRest.OneFieldValues(Table: TSQLRecordClass; const FieldName, WhereClause: RawUTF8; var Data: TInt64DynArray; SQL: PRawUTF8=nil): boolean; var T: TSQLTableJSON; V: Int64; Prop: RawUTF8; P: PUTF8Char; begin Data := nil; // handle naive expressions like SELECT ID from Table where ID=10 if IsRowID(pointer(FieldName)) and (length(WhereClause)>2) then begin P := pointer(WhereClause); GetNextFieldProp(P,Prop); if IsRowIDShort(Prop) and (StrPosI('AND',P)=nil) and (StrPosI('OR',P)=nil) then case P^ of '=': begin // SELECT RowID from Table where RowID=10 P := GotoNextNotSpace(P+1); if PWord(P)^=ord(':')+ord('(')shl 8 then inc(P,2); // handle inlined parameters SetInt64(P,V); if V>0 then begin SetLength(Data,1); Data[0] := V; result := true; exit; end; end; 'i','I': if P[1] in ['n','N'] then begin P := GotoNextNotSpace(P+2); if (P^='(') and (GotoNextNotSpace(P+1)^ in ['0'..'9']) then begin CSVToInt64DynArray(P+1,Data); if Data<>nil then begin result := true; exit; end; end; end; end; end; // retrieve the content from database result := false; T := MultiFieldValues(Table,FieldName,WhereClause); if T<>nil then try if (T.FieldCount<>1) or (T.fRowCount<=0) then exit; T.GetRowValues(0,Data); if SQL<>nil then SQL^ := T.QuerySQL; result := true; finally T.Free; end; end; function TSQLRest.SQLComputeForSelect(Table: TSQLRecordClass; const FieldNames, WhereClause: RawUTF8): RawUTF8; begin result := ''; if (self=nil) or (Table=nil) then exit; if FieldNames='' then result := Model.Props[Table].SQLFromSelectWhere('*',WhereClause) else with Table.RecordProps do if FieldNames='*' then result := SQLFromSelect(SQLTableName,SQLTableRetrieveAllFields,WhereClause,'') else if (PosExChar(',',FieldNames)=0) and (PosExChar('(',FieldNames)=0) and not IsFieldName(FieldNames) then result := '' else // prevent SQL error result := SQLFromSelect(SQLTableName,FieldNames,WhereClause,''); end; function TSQLRest.MultiFieldValues(Table: TSQLRecordClass; const FieldNames, WhereClause: RawUTF8): TSQLTableJSON; var sql: RawUTF8; begin sql := SQLComputeForSelect(Table,FieldNames,WhereClause); if sql='' then result := nil else result := ExecuteList([Table],sql); end; function TSQLRest.MultiFieldValues(Table: TSQLRecordClass; const FieldNames: RawUTF8; const WhereClauseFormat: RawUTF8; const BoundsSQLWhere: array of const): TSQLTableJSON; var where: RawUTF8; begin where := FormatUTF8(WhereClauseFormat,[],BoundsSQLWhere); result := MultiFieldValues(Table,FieldNames,where); end; function TSQLRest.MultiFieldValues(Table: TSQLRecordClass; const FieldNames: RawUTF8; const WhereClauseFormat: RawUTF8; const Args, Bounds: array of const): TSQLTableJSON; var where: RawUTF8; begin where := FormatUTF8(WhereClauseFormat,Args,Bounds); result := MultiFieldValues(Table,FieldNames,where); end; function TSQLRest.MultiFieldValue(Table: TSQLRecordClass; const FieldName: array of RawUTF8; var FieldValue: array of RawUTF8; const WhereClause: RawUTF8): boolean; var SQL: RawUTF8; n,i: integer; T: TSQLTableJSON; P: PUTF8Char; begin result := false; n := length(FieldName); if (self<>nil) and (Table<>nil) and (n=length(FieldValue)) then with Table.RecordProps do begin if (n=1) and IdemPChar(pointer(FieldName[0]),'COUNT(*)') then SQL := 'SELECT COUNT(*) FROM '+SQLTableName+SQLFromWhere(WhereClause) else begin for i := 0 to high(FieldName) do if not IsFieldNameOrFunction(FieldName[i]) then exit else // prevent SQL error or security breach if SQL='' then SQL := 'SELECT '+FieldName[i] else SQL := SQL+','+FieldName[i]; SQL := SQL+' FROM '+SQLTableName+SQLFromWhere(WhereClause)+' LIMIT 1'; end; T := ExecuteList([Table],SQL); if T<>nil then try if (T.FieldCount<>length(FieldName)) or (T.fRowCount<=0) then exit; // get field values from the first (and unique) row for i := 0 to T.FieldCount-1 do begin P := T.fResults[T.FieldCount+i]; FastSetString(FieldValue[i],P,StrLen(P)); end; result := true; finally T.Free; end; end; end; function TSQLRest.Retrieve(const SQLWhere: RawUTF8; Value: TSQLRecord; const aCustomFieldsCSV: RawUTF8): boolean; var T: TSQLTable; begin result := false; if (self=nil) or (Value=nil) then exit; T := MultiFieldValues(PSQLRecordClass(Value)^,aCustomFieldsCSV,SQLWhere); if T<>nil then try if T.fRowCount>=1 then begin Value.FillFrom(T,1); // fetch data from first result row result := true; end else Value.fID := 0; finally T.Free; end; end; function TSQLRest.RetrieveList(Table: TSQLRecordClass; const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const; const aCustomFieldsCSV: RawUTF8): TObjectList; var T: TSQLTable; begin result := nil; if (self=nil) or (Table=nil) then exit; T := MultiFieldValues(Table,aCustomFieldsCSV,FormatSQLWhere,BoundsSQLWhere); if T<>nil then try result := TObjectList.Create; T.ToObjectList(result,Table); finally T.Free; end; end; function TSQLRest.RetrieveListJSON(Table: TSQLRecordClass; const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const; const aCustomFieldsCSV: RawUTF8; aForceAJAX: boolean): RawJSON; var where: RawUTF8; begin where := FormatUTF8(FormatSQLWhere,[],BoundsSQLWhere); result := RetrieveListJSON(Table,where,aCustomFieldsCSV,aForceAJAX) end; function TSQLRest.RetrieveListJSON(Table: TSQLRecordClass; const SQLWhere: RawUTF8; const aCustomFieldsCSV: RawUTF8; aForceAJAX: boolean): RawJSON; var sql: RawUTF8; begin sql := SQLComputeForSelect(Table,aCustomFieldsCSV,SQLWhere); if sql='' then result := '' else result := EngineList(sql,aForceAJAX); end; function TSQLRest.RetrieveListObjArray(var ObjArray; Table: TSQLRecordClass; const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const; const aCustomFieldsCSV: RawUTF8): boolean; var T: TSQLTable; begin result := false; if (self=nil) or (Table=nil) then exit; T := MultiFieldValues(Table,aCustomFieldsCSV,FormatSQLWhere,BoundsSQLWhere); if T<>nil then try result := T.ToObjArray(ObjArray,Table); finally T.Free; end; end; procedure TSQLRest.AppendListAsJsonArray(Table: TSQLRecordClass; const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const; const OutputFieldName: RawUTF8; W: TJSONSerializer; const CustomFieldsCSV: RawUTF8); var Rec: TSQLRecord; begin if (self=nil) or (Table=nil) or (W=nil) then exit; Rec := Table.CreateAndFillPrepare(Self,FormatSQLWhere,BoundsSQLWhere,CustomFieldsCSV); try Rec.AppendFillAsJsonArray(OutputFieldName,W,Rec.fFill.TableMapFields); finally Rec.Free; end; end; function TSQLRest.RTreeMatch(DataTable: TSQLRecordClass; const DataTableBlobFieldName: RawUTF8; RTreeTable: TSQLRecordRTreeClass; const DataTableBlobField: RawByteString; var DataID: TIDDynArray): boolean; var Blob: PPropInfo; Res: TSQLTableJSON; BDouble: TSQLRecordTreeCoords; BInteger: TSQLRecordTreeCoordsInteger absolute BDouble; Where, SQL: RawUTF8; Data, RTree: TSQLRecordProperties; i: integer; begin result := false; if (self=nil) or (DataTable=nil) or (RTreeTable=nil) or (DataTableBlobField='') then exit; RTree := RTreeTable.RecordProps; Data := DataTable.RecordProps; Blob := Data.BlobFieldPropFromRawUTF8(DataTableBlobFieldName); if Blob=nil then exit; if RTreeTable.InheritsFrom(TSQLRecordRTree) then begin TSQLRecordRTree(RTreeTable).BlobToCoord(pointer(DataTableBlobField)^,BDouble); for i := 0 to (RTree.RTreeCoordBoundaryFields shr 1)-1 do Where := FormatUTF8('%%>=:(%): and %<=:(%): and ', [Where,RTree.Fields.List[i*2].Name,BDouble[i].Min*(1-0.00000012), RTree.Fields.List[i*2+1].Name,BDouble[i].Max*(1+0.00000012)]); { from http://sqlite.org/rtree.html: For a "contained-within" style query, rounding the bounding boxes outward might cause some entries to be excluded from the result set if the edge of the entry bounding box corresponds to the edge of the query bounding box. To guard against this, applications should expand their contained-within query boxes slightly (by 0.000012%) by rounding down the lower coordinates and rounding up the top coordinates, in each dimension. } end else if RTreeTable.InheritsFrom(TSQLRecordRTreeInteger) then begin TSQLRecordRTreeInteger(RTreeTable).BlobToCoord(pointer(DataTableBlobField)^,BInteger); for i := 0 to (RTree.RTreeCoordBoundaryFields shr 1)-1 do Where := FormatUTF8('%%>=:(%): and %<=:(%): and ', [Where,RTree.Fields.List[i*2].Name,BInteger[i].Min, RTree.Fields.List[i*2+1].Name,BInteger[i].Max]); end else exit; FormatUTF8('select %.RowID from %,% where %.RowID=%.RowID and %%(%,:(%):);', [RTree.SQLTableName,Data.SQLTableName,RTree.SQLTableName,Data.SQLTableName, Where,RTreeTable.RTreeSQLFunctionName,Data.SQLTableName, BinToBase64WithMagic(DataTableBlobField)],sql); Res := ExecuteList([DataTable,RTreeTable],sql); if Res<>nil then try if (Res.FieldCount<>1) or (Res.fRowCount<=0) then exit; Res.GetRowValues(0,TInt64DynArray(DataID)); result := true; finally Res.Free; end; end; {$ifndef NOVARIANTS} function TSQLRest.RetrieveDocVariantArray(Table: TSQLRecordClass; const ObjectName: RawUTF8; const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const; const CustomFieldsCSV: RawUTF8; FirstRecordID,LastRecordID: PID): variant; var T: TSQLTable; res: variant; begin TVarData(res).VType := varNull; if (self<>nil) and (Table<>nil) then begin T := MultiFieldValues(Table,CustomFieldsCSV,FormatSQLWhere,BoundsSQLWhere); if T<>nil then try T.ToDocVariant(res,{readonly=}false); // not readonly -> TDocVariant dvArray if FirstRecordID<>nil then FirstRecordID^ := T.IDColumnHiddenValue(1); if LastRecordID<>nil then LastRecordID^ := T.IDColumnHiddenValue(T.fRowCount); finally T.Free; end; end; if ObjectName<>'' then result := _ObjFast([ObjectName,res]) else result := res; end; function TSQLRest.RetrieveOneFieldDocVariantArray(Table: TSQLRecordClass; const FieldName, FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const): variant; var T: TSQLTable; row: Integer; res: TDocVariantData absolute result; begin VarClear(result); if (self<>nil) and (Table<>nil) then begin T := MultiFieldValues(Table,FieldName,FormatSQLWhere,BoundsSQLWhere); if T<>nil then try res.InitFast(T.fRowCount,dvArray); res.SetCount(T.fRowCount); for row := 1 to T.fRowCount do T.GetAsVariant(row,0,res.Values[row-1],false,false,false,JSON_OPTIONS_FAST); finally T.Free; end; end; end; function TSQLRest.RetrieveDocVariantArray(Table: TSQLRecordClass; const ObjectName, CustomFieldsCSV: RawUTF8; FirstRecordID,LastRecordID: PID): variant; begin result := RetrieveDocVariantArray(Table,ObjectName,'',[],CustomFieldsCSV, FirstRecordID,LastRecordID); end; function TSQLRest.RetrieveDocVariant(Table: TSQLRecordClass; const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const; const CustomFieldsCSV: RawUTF8): variant; var T: TSQLTable; bits: TSQLFieldBits; Rec: TSQLRecord; ID: TID; begin SetVariantNull(result); if (self<>nil) and (Table<>nil) then begin with Table.RecordProps do // optimized primary key direct access if Cache.IsCached(Table) and (length(BoundsSQLWhere)=1) and VarRecToInt64(BoundsSQLWhere[0],Int64(ID)) and FieldBitsFromCSV(CustomFieldsCSV,bits) and (IdemPropNameU('RowID=?',FormatSQLWhere) or IdemPropNameU('ID=?',FormatSQLWhere)) then begin if IsZero(bits) then // get all simple fields, like MultiFieldValues() bits := SimpleFieldsBits[soSelect]; if bits-SimpleFieldsBits[soSelect]=[] then begin Rec := Table.Create(self,ID); // use the cache try Rec.GetAsDocVariant(true,bits,result,nil,{"id"=}true); finally Rec.Free; end; exit; end; end; T := MultiFieldValues(Table,CustomFieldsCSV,FormatSQLWhere,BoundsSQLWhere); if T<>nil then try T.ToDocVariant(1,result) finally T.Free; end; end; end; {$endif} function TSQLRest.Retrieve(aID: TID; Value: TSQLRecord; ForUpdate: boolean): boolean; var TableIndex: integer; // used by EngineRetrieve() for SQL statement caching Resp: RawUTF8; begin // this version handles locking and use fast EngineRetrieve() method // check parameters result := false; if Value=nil then exit; // avoid GPF Value.fID := 0; if (self=nil) or (aID=0) then exit; TableIndex := Model.GetTableIndexExisting(PSQLRecordClass(Value)^); // try to lock before retrieval (if ForUpdate) if ForUpdate and not Model.Lock(TableIndex,aID) then exit; // try to retrieve existing JSON from internal cache Resp := fCache.Retrieve(TableIndex,aID); if Resp='' then begin // get JSON object '{...}' in Resp from corresponding EngineRetrieve() method Resp := EngineRetrieve(TableIndex,aID); if Resp='' then begin fCache.NotifyDeletion(TableIndex,aID); exit; end; fCache.Notify(Tableindex,aID,Resp,soSelect); end; Value.fID := aID; // Resp may not contain the "RowID": field after Update // fill Value from JSON if was correctly retrieved Value.FillFrom(Resp); result := true; end; function TSQLRest.Retrieve(const WhereClauseFmt: RawUTF8; const Args,Bounds: array of const; Value: TSQLRecord; const aCustomFieldsCSV: RawUTF8): boolean; var where: RawUTF8; begin where := FormatUTF8(WhereClauseFmt,Args,Bounds); result := Retrieve(where,Value,aCustomFieldsCSV); end; function TSQLRest.Retrieve(Reference: TRecordReference; ForUpdate: boolean): TSQLRecord; var aClass: TSQLRecordClass; begin result := nil; if (self=nil) or (RecordRef(Reference).ID=0) then exit; aClass := RecordRef(Reference).Table(Model); if aClass=nil then exit; result := aClass.Create(self,RecordRef(Reference).ID,ForUpdate); if result.fID=0 then FreeAndNil(result); // error during value retrieval end; function TSQLRest.Retrieve(aPublishedRecord, aValue: TSQLRecord): boolean; begin result := Retrieve(aPublishedRecord.ID,aValue); end; function TSQLRest.UnLock(Rec: TSQLRecord): boolean; begin if (self=nil) or (Rec=nil) or (Rec.fID<=0) then result := false else result := UnLock(PSQLRecordClass(Rec)^,Rec.fID); end; procedure TSQLRest.Commit(SessionID: cardinal; RaiseException: boolean); begin if self<>nil then begin fAcquireExecution[execORMWrite].Safe.Lock; try if (fTransactionActiveSession<>0) and (fTransactionActiveSession=SessionID) then begin fTransactionActiveSession := 0; // by default, just release flag fTransactionTable := nil; end; finally fAcquireExecution[execORMWrite].Safe.UnLock; end; end; end; procedure TSQLRest.RollBack(SessionID: cardinal); begin if self<>nil then begin fAcquireExecution[execORMWrite].Safe.Lock; try if (fTransactionActiveSession<>0) and (fTransactionActiveSession=SessionID) then begin fTransactionActiveSession := 0; // by default, just release flag fTransactionTable := nil; end; finally fAcquireExecution[execORMWrite].Safe.UnLock; end; end; end; function TSQLRest.TransactionBegin(aTable: TSQLRecordClass; SessionID: cardinal): boolean; begin result := false; fAcquireExecution[execORMWrite].Safe.Lock; try if fTransactionActiveSession=0 then begin // nested transactions are not allowed fTransactionActiveSession := SessionID; fTransactionTable := aTable; result := true; end; finally fAcquireExecution[execORMWrite].Safe.UnLock; end; end; function TSQLRest.TransactionActiveSession: cardinal; begin if self=nil then result := 0 else begin fAcquireExecution[execORMWrite].Safe.Lock; try result := fTransactionActiveSession; finally fAcquireExecution[execORMWrite].Safe.UnLock; end; end; end; function TSQLRest.BatchSend(Batch: TSQLRestBatch; var Results: TIDDynArray): integer; var Data: RawUTF8; begin result := HTTP_BADREQUEST; if (self=nil) or (Batch=nil) then // no opened BATCH sequence exit; InternalLog('BatchSend %',[Batch]); if Batch.PrepareForSending(Data) then if Data='' then // i.e. Batch.Count=0 result := HTTP_SUCCESS else try result := EngineBatchSend(Batch.Table,Data,Results,Batch.Count); except on Exception do // e.g. from TSQLRestServer.EngineBatchSend() result := HTTP_SERVERERROR; end; end; function TSQLRest.BatchSend(Batch: TSQLRestBatch): integer; var DummyRes: TIDDynArray; begin result := BatchSend(Batch,DummyRes); end; function TSQLRest.RecordCanBeUpdated(Table: TSQLRecordClass; ID: TID; Action: TSQLEvent; ErrorMsg: PRawUTF8=nil): boolean; begin result := true; // accept by default -> override this method to customize this end; function TSQLRest.Delete(Table: TSQLRecordClass; ID: TID): boolean; var tableIndex: integer; begin tableIndex := Model.GetTableIndexExisting(Table); if not RecordCanBeUpdated(Table,ID,seDelete) then result := false else begin fCache.NotifyDeletion(tableIndex,ID); fAcquireExecution[execORMWrite].fSafe.Lock; try // may be within a batch in another thread result := EngineDelete(tableIndex,ID); finally fAcquireExecution[execORMWrite].fSafe.Unlock; end; end; end; function TSQLRest.InternalDeleteNotifyAndGetIDs(Table: TSQLRecordClass; const SQLWhere: RawUTF8; var IDs: TIDDynArray): boolean; var tableIndex, i: integer; begin tableIndex := Model.GetTableIndexExisting(Table); result := false; if OneFieldValues(Table,'RowID',SQLWhere,TInt64DynArray(IDs)) and (IDs<>nil) then begin for i := 0 to length(IDs)-1 do if not RecordCanBeUpdated(Table,IDs[i],seDelete) then exit; fCache.NotifyDeletions(tableIndex,TInt64DynArray(IDs)); end; result := true; end; function TSQLRest.Delete(Table: TSQLRecordClass; const SQLWhere: RawUTF8): boolean; var IDs: TIDDynArray; begin if InternalDeleteNotifyAndGetIDs(Table,SQLWhere,IDs) then begin fAcquireExecution[execORMWrite].fSafe.Lock; try // may be within a batch in another thread result := EngineDeleteWhere(Model.GetTableIndexExisting(Table),SQLWhere,IDs); finally fAcquireExecution[execORMWrite].fSafe.Unlock; end; end else result := false; end; function TSQLRest.Delete(Table: TSQLRecordClass; const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const): boolean; var where: RawUTF8; begin where := FormatUTF8(FormatSQLWhere,[],BoundsSQLWhere); result := Delete(Table,where); end; function TSQLRest.Update(Value: TSQLRecord; const CustomFields: TSQLFieldBits; DoNotAutoComputeFields: boolean): boolean; var JSONValues: RawUTF8; TableIndex: integer; FieldBits: TSQLFieldBits; begin if (self=nil) or (Value=nil) or (Value.fID=0) or not RecordCanBeUpdated(PSQLRecordClass(Value)^,Value.fID,seUpdate) then begin result := false; // current user don't have enough right to update this record exit; end; TableIndex := Model.GetTableIndexExisting(PSQLRecordClass(Value)^); if not DoNotAutoComputeFields then Value.ComputeFieldsBeforeWrite(self,seUpdate); // update sftModTime fields if IsZero(CustomFields) then if (Value.fFill<>nil) and (Value.fFill.Table<>nil) and (Value.fFill.fTableMapRecordManyInstances=nil) then // within FillPrepare/FillOne loop: update ID, TModTime and mapped fields FieldBits := Value.fFill.fTableMapFields+Value.RecordProps.FieldBits[sftModTime] else // update all simple/custom fields (also for FillPrepareMany) FieldBits := Value.RecordProps.SimpleFieldsBits[soUpdate] else // CustomFields<>[] -> update specified (and TModTime fields) if DoNotAutoComputeFields then FieldBits := CustomFields else FieldBits := CustomFields+Value.RecordProps.FieldBits[sftModTime]; if IsZero(FieldBits) then begin result := true; // a TSQLRecord with NO simple fields (e.g. ID/blob pair) exit; end; fCache.Notify(Value,soUpdate); // will serialize Value (JSONValues may not be enough) JSONValues := Value.GetJSONValues(true,false,FieldBits); fAcquireExecution[execORMWrite].fSafe.Lock; try // may be within a batch in another thread result := EngineUpdate(TableIndex,Value.fID,JSONValues); finally fAcquireExecution[execORMWrite].fSafe.UnLock; end; end; function TSQLRest.Update(Value: TSQLRecord; const CustomCSVFields: RawUTF8; DoNotAutoComputeFields: boolean): boolean; begin if (self=nil) or (Value=nil) then result := false else result := Update(Value,Value.RecordProps.FieldBitsFromCSV(CustomCSVFields), DoNotAutoComputeFields); end; function TSQLRest.Update(aTable: TSQLRecordClass; aID: TID; const aSimpleFields: array of const): boolean; var Value: TSQLRecord; begin result := false; // means error if (self=nil) or (aTable=nil) or (aID=0) then exit; Value := aTable.Create; try if not Value.SimplePropertiesFill(aSimpleFields) then exit; Value.fID := aID; result := Update(Value); finally Value.Free; end; end; function TSQLRest.UpdateField(Table: TSQLRecordClass; ID: TID; const FieldName: RawUTF8; const FieldValue: array of const): boolean; var tableIndex: integer; begin tableIndex := Model.GetTableIndexExisting(Table); result := UpdateField(Table,'RowID',[ID],FieldName,FieldValue); if result then fCache.NotifyDeletion(tableIndex,ID); end; function TSQLRest.UpdateField(Table: TSQLRecordClass; const WhereFieldName: RawUTF8; const WhereFieldValue: array of const; const FieldName: RawUTF8; const FieldValue: array of const): boolean; var tableIndex: integer; SetValue,WhereValue: RawUTF8; begin result := false; if (length(FieldValue)<>1) or (WhereFieldName='') or (length(WhereFieldValue)<>1) then exit; VarRecToInlineValue(WhereFieldValue[0],WhereValue); VarRecToInlineValue(FieldValue[0],SetValue); tableIndex := Model.GetTableIndexExisting(Table); result := EngineUpdateField(tableIndex,FieldName,SetValue,WhereFieldName,WhereValue); // warning: this may not update the internal cache end; {$ifndef NOVARIANTS} function TSQLRest.UpdateField(Table: TSQLRecordClass; ID: TID; const FieldName: RawUTF8; const FieldValue: Variant): boolean; var tableIndex: integer; begin tableIndex := Model.GetTableIndexExisting(Table); result := UpdateField(Table,'RowID',ID,FieldName,FieldValue); if result then fCache.NotifyDeletion(tableIndex,ID); end; function TSQLRest.UpdateField(Table: TSQLRecordClass; const WhereFieldName: RawUTF8; const WhereFieldValue: Variant; const FieldName: RawUTF8; const FieldValue: Variant): boolean; var tableIndex: integer; SetValue,WhereValue: RawUTF8; begin VariantToInlineValue(WhereFieldValue,WhereValue); VariantToInlineValue(FieldValue,SetValue); tableIndex := Model.GetTableIndexExisting(Table); result := EngineUpdateField(TableIndex,FieldName,SetValue,WhereFieldName,WhereValue); // warning: this may not update the internal cache end; function TSQLRest.UpdateField(Table: TSQLRecordClass; const IDs: array of Int64; const FieldName: RawUTF8; const FieldValue: variant): boolean; var SetValue,Where: RawUTF8; tableindex: integer; begin tableIndex := Model.GetTableIndexExisting(Table); VariantToInlineValue(FieldValue,SetValue); Where := SelectInClause('RowID',IDs,'',INLINED_MAX); if length(IDs)<=INLINED_MAX then result := ExecuteFmt('update % set %=:(%): where %', [Table.SQLTableName, FieldName,SetValue,Where]) else // don't cache such a statement result := ExecuteFmt('update % set %=% where %', [Table.SQLTableName, FieldName,SetValue,Where]); if result then fCache.NotifyDeletions(tableIndex,IDs); end; {$endif NOVARIANTS} function TSQLRest.UpdateFieldIncrement(Table: TSQLRecordClass; ID: TID; const FieldName: RawUTF8; Increment: Int64): boolean; var tableIndex: integer; begin if ID<>0 then begin tableIndex := Model.GetTableIndexExisting(Table); result := EngineUpdateFieldIncrement(tableIndex,ID,FieldName,Increment); if fCache<>nil then fCache.NotifyDeletion(tableIndex,ID); end else result := false; end; procedure TSQLRest.GetJSONValuesForAdd(TableIndex: integer; Value: TSQLRecord; ForceID, DoNotAutoComputeFields, WithBlobs: boolean; CustomFields: PSQLFieldBits; var result: RawUTF8); var fields: TSQLFieldBits; props: TSQLRecordProperties; begin if not DoNotAutoComputeFields then // update TModTime/TCreateTime fields Value.ComputeFieldsBeforeWrite(self,seAdd); if Model.TableProps[TableIndex].Kind in INSERT_WITH_ID then ForceID := true; if (Model.fIDGenerator<>nil) and (Model.fIDGenerator[TableIndex]<>nil) then begin if (Value.fID=0) or not ForceID then begin Value.fID := Model.fIDGenerator[TableIndex].ComputeNew; ForceID := true; end; end else if Value.fID=0 then ForceID := false; props := Value.RecordProps; if CustomFields <> nil then if DoNotAutoComputeFields then fields := CustomFields^*props.CopiableFieldsBits else fields := CustomFields^*props.CopiableFieldsBits+props.ComputeBeforeAddFieldsBits else if withBlobs then fields := props.CopiableFieldsBits else fields := props.SimpleFieldsBits[soInsert]; if not ForceID and IsZero(fields) then result := '' else result := Value.GetJSONValues(true,ForceID,fields); end; function TSQLRest.InternalAdd(Value: TSQLRecord; SendData: boolean; CustomFields: PSQLFieldBits; ForceID, DoNotAutoComputeFields: boolean): TID; var json: RawUTF8; TableIndex: integer; begin if Value=nil then begin result := 0; exit; end; TableIndex := Model.GetTableIndexExisting(PSQLRecordClass(Value)^); if SendData then GetJSONValuesForAdd(TableIndex,Value,ForceID,DoNotAutoComputeFields,false,CustomFields,json) else json := ''; // on success, returns the new RowID value; on error, returns 0 fAcquireExecution[execORMWrite].fSafe.Lock; try // may be within a batch in another thread result := EngineAdd(TableIndex,json); // will call static if necessary finally fAcquireExecution[execORMWrite].fSafe.Unlock; end; // on success, Value.ID is updated with the new RowID Value.fID := result; if SendData and (result<>0) then fCache.Notify(PSQLRecordClass(Value)^,result,json,soInsert); end; function TSQLRest.Add(Value: TSQLRecord; SendData,ForceID,DoNotAutoComputeFields: boolean): TID; begin result := InternalAdd(Value,SendData,nil,ForceID,DoNotAutoComputeFields); end; function TSQLRest.Add(Value: TSQLRecord; const CustomCSVFields: RawUTF8; ForceID, DoNotAutoComputeFields: boolean): TID; var f: TSQLFieldBits; begin with Value.RecordProps do if CustomCSVFields='*' then // FieldBitsFromCSV('*') will use [soSelect] f := SimpleFieldsBits[soInsert] else f := FieldBitsFromCSV(CustomCSVFields); result := InternalAdd(Value,true,@f,ForceID,DoNotAutoComputeFields); end; function TSQLRest.Add(Value: TSQLRecord; const CustomFields: TSQLFieldBits; ForceID, DoNotAutoComputeFields: boolean): TID; begin result := InternalAdd(Value,true,@CustomFields,ForceID,DoNotAutoComputeFields); end; function TSQLRest.AddSimple(aTable: TSQLRecordClass; const aSimpleFields: array of const; ForcedID: TID): TID; var Value: TSQLRecord; begin result := 0; // means error if (self=nil) or (aTable=nil) then exit; Value := aTable.Create; try if Value.SimplePropertiesFill(aSimpleFields) then begin if ForcedID<>0 then Value.fID := ForcedID; result := Add(Value,true,(ForcedID<>0)); end; finally Value.Free; end; end; function TSQLRest.AddWithBlobs(Value: TSQLRecord; ForceID, DoNotAutoComputeFields: boolean): TID; var TableIndex: integer; json: RawUTF8; begin if Value=nil then begin result := 0; exit; end; TableIndex := Model.GetTableIndexExisting(PSQLRecordClass(Value)^); GetJSONValuesForAdd(TableIndex,Value,ForceID,DoNotAutoComputeFields,true,nil,json); // on success, returns the new RowID value; on error, returns 0 fAcquireExecution[execORMWrite].fSafe.Lock; try // may be within a batch in another thread result := EngineAdd(TableIndex,json); // will call static if necessary finally fAcquireExecution[execORMWrite].fSafe.Unlock; end; // on success, Value.ID is updated with the new RowID Value.fID := result; // here fCache.Notify is not called, since the JSONValues is verbose end; function TSQLRest.AddOrUpdate(Value: TSQLRecord; ForceID: boolean): TID; begin if (self=nil) or (Value=nil) then begin result := 0; exit; end; if ForceID or (Value.fID=0) then begin result := Add(Value,true,ForceID); if (result<>0) or (Value.fID=0) then exit; end; if Update(Value) then result := Value.fID else result := 0; end; procedure TSQLRest.QueryAddCustom(aTypeInfo: pointer; aEvent: TSQLQueryEvent; const aOperators: TSQLQueryOperators); var Enum: PEnumType; i,n: integer; begin if (self=nil) or not Assigned(aEvent) or (aTypeInfo=nil) or (PTypeInfo(aTypeInfo)^.Kind<>tkEnumeration) then exit; Enum := PTypeInfo(aTypeInfo)^.EnumBaseType; n := length(QueryCustom); SetLength(QueryCustom,n+Enum^.MaxValue+1); for i := 0 to Enum^.MaxValue do with QueryCustom[i+n] do begin EnumType := Enum; EnumIndex := i; Event := aEvent; Operators := aOperators; end; end; class function TSQLRest.QueryIsTrue(aTable: TSQLRecordClass; aID: TID; FieldType: TSQLFieldType; Value: PUTF8Char; Operator: integer; Reference: PUTF8Char): boolean; begin // use mostly the same fast comparison functions as for sorting result := false; if aID=0 then exit; // invalid input field if Reference=nil then exit; // avoid most GPF if FieldType=sftMany then exit; // nothing is stored directly, but in a separate pivot table if FieldType in [sftUnknown,sftBlob,sftBlobDynArray,sftBlobCustom,sftObject, sftUTF8Custom{$ifndef NOVARIANTS},sftVariant,sftNullable{$endif}] then FieldType := sftUTF8Text; // unknown or blob fields are compared as UTF-8 { TODO: handle proper sftBlobDynArray/sftBlobCustom/sftBlobRecord comparison } case TSQLQueryOperator(Operator) of qoNone: result := true; qoEqualTo: result := SQLFieldTypeComp[FieldType](Value,Reference)=0; qoNotEqualTo: result := SQLFieldTypeComp[FieldType](Value,Reference)<>0; qoLessThan: result := SQLFieldTypeComp[FieldType](Value,Reference)<0; qoLessThanOrEqualTo: result := SQLFieldTypeComp[FieldType](Value,Reference)<=0; qoGreaterThan: result := SQLFieldTypeComp[FieldType](Value,Reference)>0; qoGreaterThanOrEqualTo: result := SQLFieldTypeComp[FieldType](Value,Reference)>=0; qoEqualToWithCase: result := StrComp(Value,Reference)=0; qoNotEqualToWithCase: result := StrComp(Value,Reference)<>0; qoContains: result := PosIU(Reference,Value)<>0; qoBeginWith: result := IdemPCharU(Value,Reference); qoSoundsLikeEnglish, qoSoundsLikeFrench, qoSoundsLikeSpanish: result := PSynSoundEx(Reference)^.UTF8(Value); end; end; function TSQLRest.RetrieveBlob(Table: TSQLRecordClass; aID: TID; const BlobFieldName: RawUTF8; out BlobStream: THeapMemoryStream): boolean; var BlobData: TSQLRawBlob; begin BlobStream := THeapMemoryStream.Create; result := RetrieveBlob(Table,aID,BlobFieldName,BlobData); if not result or (BlobData='') then exit; if BlobStream.Write(pointer(BlobData)^,length(BlobData))<>length(BlobData) then result := false; BlobStream.Seek(0,soFromBeginning); // rewind end; function TSQLRest.UpdateBlob(Table: TSQLRecordClass; aID: TID; const BlobFieldName: RawUTF8; BlobData: TStream): boolean; var Blob: TSQLRawBlob; L: integer; begin result := false; if (self=nil) or (BlobData=nil) then exit; L := BlobData.Seek(0,soFromEnd); SetLength(Blob,L); BlobData.Seek(0,soFromBeginning); if BlobData.Read(pointer(Blob)^,L)<>L then exit; result := UpdateBlob(Table,aID,BlobFieldName,Blob); end; function TSQLRest.UpdateBlob(Table: TSQLRecordClass; aID: TID; const BlobFieldName: RawUTF8; BlobData: pointer; BlobSize: integer): boolean; var Blob: TSQLRawBlob; begin if (self=nil) or (BlobData=nil) or (BlobSize<0) then result := false else begin SetString(Blob,PAnsiChar(BlobData),BlobSize); result := UpdateBlob(Table,aID,BlobFieldName,Blob); end; end; function TSQLRest.RetrieveBlob(Table: TSQLRecordClass; aID: TID; const BlobFieldName: RawUTF8; out BlobData: TSQLRawBlob): boolean; var BlobField: PPropInfo; begin result := false; if (self=nil) or (aID<=0) then exit; BlobField := Table.RecordProps.BlobFieldPropFromRawUTF8(BlobFieldName); if BlobField=nil then exit; result := EngineRetrieveBlob( Model.GetTableIndexExisting(Table),aID,BlobField,BlobData); end; function TSQLRest.UpdateBlob(Table: TSQLRecordClass; aID: TID; const BlobFieldName: RawUTF8; const BlobData: TSQLRawBlob): boolean; var BlobField: PPropInfo; begin result := false; if (self=nil) or (aID<=0) or not RecordCanBeUpdated(Table,aID,seUpdate) then exit; BlobField := Table.RecordProps.BlobFieldPropFromRawUTF8(BlobFieldName); if BlobField=nil then exit; result := EngineUpdateBlob( Model.GetTableIndexExisting(Table),aID,BlobField,BlobData); end; function TSQLRest.UpdateBlobFields(Value: TSQLRecord): boolean; var BlobData: RawByteString; TableIndex, i: integer; begin result := false; if (Value=nil) or (Value.fID<=0) then exit; with Value.RecordProps do if BlobFields<>nil then begin TableIndex := self.fModel.GetTableIndexExisting(PSQLRecordClass(Value)^); for i := 0 to length(BlobFields)-1 do begin BlobFields[i].PropInfo.GetLongStrProp(Value,BlobData); if not EngineUpdateBlob(TableIndex,Value.fID,BlobFields[i].PropInfo,BlobData) then exit; end; end; result := true; end; function TSQLRest.RetrieveBlobFields(Value: TSQLRecord): boolean; var BlobData: TSQLRawBlob; TableIndex, i: integer; begin result := false; if (Self=nil) or (Value=nil) or (Value.fID<=0) then exit; with Value.RecordProps do if BlobFields<>nil then begin TableIndex := self.fModel.GetTableIndexExisting(PSQLRecordClass(Value)^); for i := 0 to length(BlobFields)-1 do if EngineRetrieveBlob(TableIndex,Value.fID,BlobFields[i].PropInfo,BlobData) then BlobFields[i].PropInfo.SetLongStrProp(Value,BlobData) else exit; end; result := true; end; function TSQLRest.TableRowCount(Table: TSQLRecordClass): Int64; var T: TSQLTableJSON; begin if (self=nil) or (Table=nil) then T := nil else T := ExecuteList([Table],'SELECT Count(*) FROM '+Table.RecordProps.SQLTableName); if T<>nil then try Result := T.GetAsInt64(1,0); finally T.Free; end else Result := -1; end; function TSQLRest.TableHasRows(Table: TSQLRecordClass): boolean; var T: TSQLTableJSON; begin if (self=nil) or (Table=nil) then T := nil else T := ExecuteList([Table],'SELECT RowID FROM '+Table.RecordProps.SQLTableName+' LIMIT 1'); if T<>nil then try Result := T.fRowCount>0; finally T.Free; end else Result := false; end; function TSQLRest.TableMaxID(Table: TSQLRecordClass): TID; var T: TSQLTableJSON; begin if (self=nil) or (Table=nil) then T := nil else T := ExecuteList([Table],'SELECT max(RowID) FROM '+Table.RecordProps.SQLTableName); if T<>nil then try Result := T.GetAsInt64(1,0); finally T.Free; end else Result := -1; end; function TSQLRest.ExecuteList(const Tables: array of TSQLRecordClass; const SQL: RawUTF8): TSQLTableJSON; var JSON: RawUTF8; begin JSON := EngineList(SQL,false); if JSON<>'' then result := TSQLTableJSON.CreateFromTables(Tables,SQL,JSON) else result := nil; end; function TSQLRest.ExecuteJson(const Tables: array of TSQLRecordClass; const SQL: RawUTF8; ForceAJAX: Boolean; ReturnedRowCount: PPtrInt): RawJSON; begin result := EngineList(SQL,ForceAjax,ReturnedRowCount); end; function TSQLRest.Execute(const aSQL: RawUTF8): boolean; begin result := EngineExecute(aSQL); end; function TSQLRest.ExecuteFmt(const SQLFormat: RawUTF8; const Args: array of const): boolean; var SQL: RawUTF8; begin FormatUTF8(SQLFormat,Args,SQL); result := EngineExecute(SQL); end; function TSQLRest.ExecuteFmt(const SQLFormat: RawUTF8; const Args, Bounds: array of const): boolean; var SQL: RawUTF8; begin SQL := FormatUTF8(SQLFormat,Args,Bounds); result := EngineExecute(SQL); end; function TSQLRest.MainFieldValue(Table: TSQLRecordClass; ID: TID; ReturnFirstIfNoUnique: boolean=false): RawUTF8; begin if (self=nil) or (Table=nil) or (ID<=0) then result := '' else begin result := Table.RecordProps.MainFieldName(ReturnFirstIfNoUnique); if result<>'' then result := OneFieldValue(Table,Result,ID); end; end; function TSQLRest.MainFieldID(Table: TSQLRecordClass; const Value: RawUTF8): TID; var aMainField: integer; begin result := 0; if (self<>nil) and (Value<>'') and (Table<>nil) then with Table.RecordProps do begin aMainField := MainField[false]; if aMainField>=0 then SetID(OneFieldValue(Table,'RowID', Fields.List[aMainField].Name+'=:('+QuotedStr(Value,'''')+'):'),result); end; end; function TSQLRest.MainFieldIDs(Table: TSQLRecordClass; const Values: array of RawUTF8; out IDs: TIDDynArray): boolean; var aMainField, id: TID; begin if (self<>nil) and (high(Values)>=0) and (Table<>nil) then if high(Values)=0 then begin // handle special case of one Values[] item id := MainFieldID(Table,Values[0]); if id>0 then begin SetLength(IDs,1); IDs[0] := id; end; end else with Table.RecordProps do begin // request all Values[] IDs at once aMainField := MainField[false]; if aMainField>=0 then OneFieldValues(Table,'RowID', SelectInClause(Fields.List[aMainField].Name,Values),TInt64DynArray(IDs)); end; result := IDs<>nil; end; function TSQLRest.FTSMatch(Table: TSQLRecordFTS3Class; const WhereClause: RawUTF8; var DocID: TIDDynArray): boolean; begin // FTS3 tables don't have any ID, but RowID or DocID result := OneFieldValues(Table,'RowID',WhereClause,TInt64DynArray(DocID)); end; function TSQLRest.FTSMatch(Table: TSQLRecordFTS3Class; const MatchClause: RawUTF8; var DocID: TIDDynArray; const PerFieldWeight: array of double; limit,offset: integer): boolean; var WhereClause: RawUTF8; i: integer; begin result := false; with Table.RecordProps do if length(PerFieldWeight)<>length(SimpleFields) then exit else WhereClause := FormatUTF8('% MATCH ? ORDER BY rank(matchinfo(%)', [SQLTableName,SQLTableName],[MatchClause]); for i := 0 to high(PerFieldWeight) do WhereClause := FormatUTF8('%,?',[WhereClause],[PerFieldWeight[i]]); WhereClause := WhereClause+') DESC'; if limit>0 then WhereClause := FormatUTF8('% LIMIT % OFFSET %',[WhereClause,limit,offset]); result := FTSMatch(Table,WhereClause,DocID); end; function TSQLRest.GetServerTimestamp: TTimeLog; var Tix: cardinal; begin Tix := GetTickCount64 shr 9; // resolution change 1 ms -> 512 ms if fServerTimestampCacheTix=Tix then result := fServerTimestampCacheValue.Value else begin fServerTimestampCacheTix := Tix; fServerTimestampCacheValue.From(NowUTC+fServerTimestampOffset); result := fServerTimestampCacheValue.Value; end; end; procedure TSQLRest.SetServerTimestamp(const Value: TTimeLog); begin fServerTimestampOffset := PTimeLogBits(@Value)^.ToDateTime-NowUTC; if fServerTimestampOffset=0 then fServerTimestampOffset := 0.000001; // retrieve server date/time only once end; function TSQLRest.GetCache: TSQLRestCache; begin if self=nil then result := nil else begin if fCache=nil then fCache := TSQLRestCache.Create(self); result := fCache; end; end; function TSQLRest.CacheOrNil: TSQLRestCache; begin if self=nil then result := nil else result := fCache; end; function TSQLRest.CacheWorthItForTable(aTableIndex: cardinal): boolean; begin result := true; // always worth caching by default end; procedure TSQLRest.BeginCurrentThread(Sender: TThread); begin // nothing do to at this level -> see TSQLRestServer.BeginCurrentThread end; procedure TSQLRest.EndCurrentThread(Sender: TThread); begin // most will be done e.g. in TSQLRestServer.EndCurrentThread {$ifdef WITHLOG} fLogFamily.OnThreadEnded(Sender); {$endif} end; procedure TSQLRest.WriteLock; begin fAcquireExecution[execORMWrite].Safe.Lock; end; procedure TSQLRest.WriteUnLock; begin fAcquireExecution[execORMWrite].Safe.UnLock; end; function TSQLRest.GetAcquireExecutionMode(Cmd: TSQLRestServerURIContextCommand): TSQLRestServerAcquireMode; begin result := fAcquireExecution[Cmd].Mode; end; procedure TSQLRest.SetAcquireExecutionMode(Cmd: TSQLRestServerURIContextCommand; Value: TSQLRestServerAcquireMode); begin fAcquireExecution[Cmd].Mode := Value; end; function TSQLRest.GetAcquireExecutionLockedTimeOut(Cmd: TSQLRestServerURIContextCommand): cardinal; begin result := fAcquireExecution[Cmd].LockedTimeOut; end; procedure TSQLRest.SetAcquireExecutionLockedTimeOut(Cmd: TSQLRestServerURIContextCommand; Value: cardinal); begin fAcquireExecution[Cmd].LockedTimeOut := Value; end; function TSQLRest.InternalBatchStart(Method: TSQLURIMethod; BatchOptions: TSQLRestBatchOptions): boolean; begin result := false; end; procedure TSQLRest.InternalBatchStop; begin raise EORMException.CreateUTF8('Unexpected %.InternalBatchStop',[self]); end; function TSQLRest.EngineBatchSend(Table: TSQLRecordClass; var Data: RawUTF8; var Results: TIDDynArray; ExpectedResultsCount: integer): integer; begin raise EORMException.CreateUTF8('BATCH not supported by %',[self]); end; {$ifdef ISDELPHI2010} // Delphi 2009/2010 generics support is buggy :( function TSQLRest.Service: T; var service: TServiceFactory; begin service := fServices.Info(TypeInfo(T)); if (service=nil) or not service.Get(result) then result := Default(T); end; function TSQLRest.RetrieveList(const aCustomFieldsCSV: RawUTF8): TObjectList; begin result := RetrieveList('',[],aCustomFieldsCSV); end; function TSQLRest.RetrieveList(const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const; const aCustomFieldsCSV: RawUTF8): TObjectList; var Table: TSQLTable; begin result := nil; if self=nil then exit; Table := MultiFieldValues(TSQLRecordClass(T),aCustomFieldsCSV,FormatSQLWhere,BoundsSQLWhere); if Table<>nil then try result := Table.ToObjectList; finally Table.Free; end; end; {$endif} { TSQLRestCacheEntry } procedure TSQLRestCacheEntry.Init; begin Value.InitSpecific(TypeInfo(TSQLRestCacheEntryValueDynArray), Values,djInt64,@Count); // will search/sort by first ID: TID field Mutex.Init; end; procedure TSQLRestCacheEntry.Done; begin Mutex.Done; end; procedure TSQLRestCacheEntry.Clear; begin Mutex.Lock; try Value.Clear; CacheAll := false; CacheEnable := false; TimeOutMS := 0; finally Mutex.UnLock; end; end; procedure TSQLRestCacheEntry.FlushCacheEntry(Index: Integer); begin if cardinal(Index)=0) then begin Rec.ID := aID; Rec.Timestamp512 := 0; // indicates no value cache yet Rec.Tag := 0; Value.FastAddSorted(i,Rec); end; // do nothing if aID is already in Values[] finally Mutex.UnLock; end; end; procedure TSQLRestCacheEntry.SetJSON(aID: TID; const aJSON: RawUTF8; aTag: cardinal); var Rec: TSQLRestCacheEntryValue; i: integer; begin Rec.ID := aID; Rec.JSON := aJSON; Rec.Timestamp512 := GetTickCount64 shr 9; Rec.Tag := aTag; Mutex.Lock; try if Value.FastLocateSorted(Rec,i) then Values[i] := Rec else if CacheAll and (i>=0) then Value.FastAddSorted(i,Rec); finally Mutex.UnLock; end; end; procedure TSQLRestCacheEntry.SetJSON(aRecord: TSQLRecord); begin // soInsert = include all fields SetJSON(aRecord.fID,aRecord.GetJSONValues(true,false,soInsert)); end; function TSQLRestCacheEntry.RetrieveJSON(aID: TID; var aJSON: RawUTF8; aTag: PCardinal): boolean; var i: integer; begin result := false; Mutex.Lock; try i := Value.Find(aID); // fast O(log(n)) binary search by first ID field if i>=0 then with Values[i] do if Timestamp512<>0 then // 0 when there is no JSON value cached if (TimeOutMS<>0) and ((GetTickCount64-TimeOutMS) shr 9>Timestamp512) then FlushCacheEntry(i) else begin if aTag<>nil then aTag^ := Tag; aJSON := JSON; result := true; // found a non outdated serialized value in cache end; finally Mutex.UnLock; end; end; function TSQLRestCacheEntry.RetrieveJSON(aID: TID; aValue: TSQLRecord; aTag: PCardinal): boolean; var JSON: RawUTF8; begin if RetrieveJSON(aID,JSON,aTag) then begin aValue.FillFrom(JSON); aValue.fID := aID; // override RowID field (may be not present after Update) result := true; end else result := false; end; function TSQLRestCacheEntry.CachedMemory(FlushedEntriesCount: PInteger): cardinal; var i: integer; tix512: cardinal; begin result := 0; if CacheEnable and (Count>0) then begin tix512 := (GetTickCount64-TimeOutMS) shr 9; Mutex.Lock; try for i := Count-1 downto 0 do with Values[i] do if Timestamp512<>0 then if (TimeOutMS<>0) and (tix512>Timestamp512) then begin FlushCacheEntry(i); if FlushedEntriesCount<>nil then inc(FlushedEntriesCount^); end else inc(result,length(JSON)+(SizeOf(TSQLRestCacheEntryValue)+16)); finally Mutex.UnLock; end; end; end; { TSQLRestCache } constructor TSQLRestCache.Create(aRest: TSQLRest); var i: PtrInt; begin if aRest=nil then EBusinessLayerException.CreateUTF8('%.Create',[self]); fRest := aRest; SetLength(fCache,length(fRest.Model.Tables)); for i := 0 to length(fCache)-1 do fCache[i].Init; end; destructor TSQLRestCache.Destroy; var i: PtrInt; begin for i := 0 to length(fCache)-1 do fCache[i].Done; inherited; end; function TSQLRestCache.CachedEntries: cardinal; var i,j: PtrInt; begin result := 0; if self<>nil then for i := 0 to length(fCache)-1 do with fCache[i] do if CacheEnable then begin Mutex.Lock; try for j := 0 to Count-1 do if Values[j].Timestamp512<>0 then inc(result); finally Mutex.UnLock; end; end; end; function TSQLRestCache.CachedMemory(FlushedEntriesCount: PInteger): cardinal; var i: PtrInt; begin result := 0; if FlushedEntriesCount<>nil then FlushedEntriesCount^ := 0; if self<>nil then for i := 0 to length(fCache)-1 do inc(result,fCache[i].CachedMemory(FlushedEntriesCount)); end; function TSQLRestCache.SetTimeOut(aTable: TSQLRecordClass; aTimeoutMS: Cardinal): boolean; var i: PtrInt; begin result := false; if (self=nil) or (aTable=nil) then exit; i := Rest.Model.GetTableIndexExisting(aTable); if Rest.CacheWorthItForTable(i) then if Cardinal(i)=cardinal(Length(fCache)) then exit; if Rest.CacheWorthItForTable(i) then fCache[i].SetCache(aID); result := True; end; function TSQLRestCache.SetCache(aTable: TSQLRecordClass; const aIDs: array of TID): boolean; var i: cardinal; j: PtrInt; begin result := false; if (self=nil) or (aTable=nil) or (length(aIDs)=0) then exit; i := Rest.Model.GetTableIndex(aTable); if i>=cardinal(Length(fCache)) then exit; if Rest.CacheWorthItForTable(i) then for j := 0 to high(aIDs) do fCache[i].SetCache(aIDs[j]); result := True; end; function TSQLRestCache.SetCache(aRecord: TSQLRecord): boolean; begin if (self=nil) or (aRecord=nil) or (aRecord.fID<=0) then result := false else result := SetCache(PSQLRecordClass(aRecord)^,aRecord.fID); end; procedure TSQLRestCache.Clear; var i: PtrInt; begin if self<>nil then for i := 0 to length(fCache)-1 do fCache[i].Clear; end; function TSQLRestCache.FillFromQuery(aTable: TSQLRecordClass; const FormatSQLWhere: RawUTF8; const BoundsSQLWhere: array of const): integer; var rec: TSQLRecord; cache: ^TSQLRestCacheEntry; begin result := 0; if self=nil then exit; cache := @fCache[fRest.Model.GetTableIndexExisting(aTable)]; if not cache^.CacheEnable then exit; rec := aTable.CreateAndFillPrepare(fRest,FormatSQLWhere,BoundsSQLWhere); try while rec.FillOne do begin cache^.SetJSON(rec); inc(result); end; finally rec.Free; end; end; procedure TSQLRestCache.Flush; var i: PtrInt; begin if self<>nil then for i := 0 to length(fCache)-1 do fCache[i].FlushCacheAllEntries; // include *CriticalSection(Mutex) end; procedure TSQLRestCache.Flush(aTable: TSQLRecordClass); begin if self<>nil then // includes *CriticalSection(Mutex): fCache[fRest.Model.GetTableIndexExisting(aTable)].FlushCacheAllEntries; end; procedure TSQLRestCache.Flush(aTable: TSQLRecordClass; aID: TID); begin if self<>nil then with fCache[fRest.Model.GetTableIndexExisting(aTable)] do if CacheEnable then begin Mutex.Lock; try FlushCacheEntry(Value.Find(aID)); finally Mutex.UnLock; end; end; end; procedure TSQLRestCache.Flush(aTable: TSQLRecordClass; const aIDs: array of TID); var i: PtrInt; begin if (self<>nil) and (length(aIDs)>0) then with fCache[fRest.Model.GetTableIndexExisting(aTable)] do if CacheEnable then begin Mutex.Lock; try for i := 0 to high(aIDs) do FlushCacheEntry(Value.Find(aIDs[i])); finally Mutex.UnLock; end; end; end; procedure TSQLRestCache.Notify(aTable: TSQLRecordClass; aID: TID; const aJSON: RawUTF8; aAction: TSQLOccasion); begin if (self<>nil) and (aTable<>nil) and (aID>0) then Notify(fRest.Model.GetTableIndex(aTable),aID,aJSON,aAction); end; procedure TSQLRestCache.Notify(aRecord: TSQLRecord; aAction: TSQLOccasion); var aTableIndex: cardinal; begin if (self=nil) or (aRecord=nil) or (aRecord.fID<=0) or not (aAction in [soInsert,soUpdate]) then exit; aTableIndex := fRest.Model.GetTableIndex(PSQLRecordClass(aRecord)^); if aTableIndexnil) and (aID>0) and (aAction in [soSelect,soInsert,soUpdate]) and (aJSON<>'') and (Cardinal(aTableIndex)nil) and (aID>0) and (Cardinal(aTableIndex)nil) and (high(aIDs)>=0) and (Cardinal(aTableIndex)nil) and (aTable<>nil) and (aID>0) then NotifyDeletion(fRest.Model.GetTableIndex(aTable),aID); end; function TSQLRestCache.Retrieve(aID: TID; aValue: TSQLRecord): boolean; var TableIndex: cardinal; begin result := false; if (self=nil) or (aValue=nil) or (aID<=0) then exit; TableIndex := fRest.Model.GetTableIndexExisting(PSQLRecordClass(aValue)^); if TableIndexnil) and (aID>0) and (Cardinal(aTableIndex)=endtix); end; end; destructor TSQLRestThread.Destroy; begin if fExecuting then begin Terminate; // will notify Execute that the process is finished WaitForNotExecuting; end; inherited Destroy; if fOwnRest and (fRest<>nil) then begin {$ifdef WITHLOG} if GetCurrentThreadId=ThreadID then begin fRest.fLogFamily := nil; // no log after fRest.EndCurrentThread(self) fRest.fLogClass := nil; end; {$endif} FreeAndNil(fRest); end; fSafe.Done; fEvent.Free; end; function TSQLRestThread.SleepOrTerminated(MS: integer): boolean; var endtix: Int64; begin result := true; // notify Terminated if (self = nil) or Terminated then exit; endtix := SynCommons.GetTickCount64+MS; repeat FixedWaitFor(fEvent,MS); if Terminated then exit; until (MS<32) or (SynCommons.GetTickCount64>=endtix); result := false; // normal delay expiration end; procedure TSQLRestThread.Execute; begin {$ifdef WITHLOG} fLog := fRest.LogClass.Add; {$endif} SetCurrentThreadName('%',[fThreadName]); fRest.BeginCurrentThread(self); try fExecuting := true; try InternalExecute; except on E: Exception do {$ifdef WITHLOG} fLog.Add.Log(sllError,'Unhandled % in %.Execute -> abort',[E,ClassType],self); {$endif} end; finally fRest.EndCurrentThread(self); fLog := nil; // no log after EndCurrentThread fExecuting := false; end; end; {$ifndef HASTTHREADSTART} procedure TSQLRestThread.Start; begin Resume; end; {$endif} {$ifndef HASTTHREADTERMINATESET} procedure TSQLRestThread.Terminate; begin inherited Terminate; // FTerminated := True TerminatedSet; end; {$endif} procedure TSQLRestThread.TerminatedSet; begin fEvent.SetEvent; end; { TSQLRestURIParams } procedure TSQLRestURIParams.Init; begin OutStatus := 0; OutInternalState := 0; RestAccessRights := nil; LowLevelConnectionID := 0; byte(LowLevelFlags) := 0; end; procedure TSQLRestURIParams.Init(const aURI,aMethod,aInHead,aInBody: RawUTF8); begin Init; Url := aURI; Method := aMethod; InHead := aInHead; InBody := aInBody; end; function TSQLRestURIParams.InBodyType(GuessJSONIfNoneSet: boolean): RawUTF8; begin FindNameValue(InHead,HEADER_CONTENT_TYPE_UPPER,result); if GuessJSONIfNoneSet and (result='') then result := JSON_CONTENT_TYPE_VAR; end; function TSQLRestURIParams.InBodyTypeIsJson(GuessJSONIfNoneSet: boolean): boolean; begin result := IdemPChar(pointer(InBodyType(GuessJSONIfNoneSet)),JSON_CONTENT_TYPE_UPPER); end; function TSQLRestURIParams.OutBodyType(GuessJSONIfNoneSet: boolean): RawUTF8; begin FindNameValue(OutHead,HEADER_CONTENT_TYPE_UPPER,result); if GuessJSONIfNoneSet and (result='') then result := JSON_CONTENT_TYPE_VAR; end; function TSQLRestURIParams.OutBodyTypeIsJson(GuessJSONIfNoneSet: boolean): boolean; begin result := IdemPChar(pointer(OutBodyType(GuessJSONIfNoneSet)),JSON_CONTENT_TYPE_UPPER); end; function TSQLRestURIParams.Header(UpperName: PAnsiChar): RawUTF8; begin FindNameValue(InHead,UpperName,result); end; function TSQLRestURIParams.HeaderOnce(var Store: RawUTF8; UpperName: PAnsiChar): RawUTF8; begin if (Store='') and (@self<>nil) then begin FindNameValue(InHead,UpperName,result); if result='' then Store := NULL_STR_VAR else // ensure header is parsed only once Store := result; end else if pointer(Store)=pointer(NULL_STR_VAR) then result := '' else result := Store; end; { TSQLRestClientCallbacks } constructor TSQLRestClientCallbacks.Create(aOwner: TSQLRestClientURI); begin inherited Create; Owner := aOwner; end; function TSQLRestClientCallbacks.FindIndex(aID: integer): integer; begin if self<>nil then for result := 0 to Count-1 do if List[result].ID=aID then exit; result := -1; end; function TSQLRestClientCallbacks.FindEntry(var aItem: TSQLRestClientCallbackItem): boolean; var i: Integer; P: PSQLRestClientCallbackItem; begin result := false; if self=nil then exit; fSafe.Lock; try P := pointer(List); for i := 1 to Count do if P^.ID=aItem.ID then begin if P^.Instance<>nil then begin result := true; aItem := P^; end; exit; end else inc(P); finally Safe.UnLock; end; end; function TSQLRestClientCallbacks.FindAndRelease(aID: integer): boolean; var i: Integer; begin result := false; if self=nil then exit; fSafe.Lock; try i := FindIndex(aID); if i<0 then exit; List[i].ReleasedFromServer := True; finally Safe.UnLock; end; result := true; end; function TSQLRestClientCallbacks.UnRegisterByIndex(index: integer): boolean; begin result := false; if cardinal(index)>=cardinal(Count) then exit; with List[index] do if not ReleasedFromServer then try if Owner.FakeCallbackUnregister(Factory,ID,Instance) then result := true; except // ignore errors at this point, and continue end; dec(Count); if index=Count then SetLength(List,Count+32); with List[Count] do begin ID := aID; Instance := aInstance; Factory := aFactory; end; inc(Count); finally Safe.UnLock; end; end; function TSQLRestClientCallbacks.DoRegister(aInstance: pointer; aFactory: TInterfaceFactory): integer; begin result := InterlockedIncrement(fCurrentID); DoRegister(result,aInstance,aFactory); end; { TSQLRestClientURI } function TSQLRestClientURI.EngineExecute(const SQL: RawUTF8): boolean; begin result := URI(Model.Root,'POST',nil,nil,@SQL).Lo in [HTTP_SUCCESS,HTTP_NOCONTENT]; end; function TSQLRestClientURI.URIGet(Table: TSQLRecordClass; ID: TID; var Resp: RawUTF8; ForUpdate: boolean=false): Int64Rec; const METHOD: array[boolean] of RawUTF8 = ('GET','LOCK'); begin result := URI(Model.getURIID(Table,ID),METHOD[ForUpdate],@Resp,nil,nil); end; function TSQLRestClientURI.UnLock(Table: TSQLRecordClass; aID: TID): boolean; begin if (self=nil) or not Model.UnLock(Table,aID) then result := false else // was not locked by the client result := URI(Model.getURIID(Table,aID),'UNLOCK').Lo in [HTTP_SUCCESS,HTTP_NOCONTENT]; end; function TSQLRestClientURI.ExecuteList(const Tables: array of TSQLRecordClass; const SQL: RawUTF8): TSQLTableJSON; var Resp: RawUTF8; begin if self=nil then result := nil else with URI(Model.Root,'GET',@Resp,nil,@SQL) do if Lo=HTTP_SUCCESS then begin // GET with SQL sent if high(Tables)=0 then result := TSQLTableJSON.CreateFromTables([Tables[0]],SQL,Resp) else result := TSQLTableJSON.CreateFromTables(Tables,SQL,Resp); result.fInternalState := Hi; end else // get data result := nil; end; function TSQLRestClientURI.ServerInternalState: cardinal; begin if (Self=nil) or (Model=nil) then // avoid GPF result := cardinal(-1) else result := URI(Model.Root,'STATE').Hi; end; function TSQLRestClientURI.ServerCacheFlush(aTable: TSQLRecordClass; aID: TID): boolean; var aResp: RawUTF8; begin if (Self=nil) or (Model=nil) then // avoid GPF result := false else result := CallBackGet('CacheFlush',[],aResp,aTable,aID) in [HTTP_SUCCESS,HTTP_NOCONTENT]; end; function TSQLRestClientURI.ServerTimestampSynchronize: boolean; var status: integer; aResp: RawUTF8; begin if self=nil then begin result := false; exit; end; fServerTimestampOffset := 0.0001; // avoid endless recursive call status := CallBackGet('Timestamp',[],aResp); result := (status=HTTP_SUCCESS) and (aResp<>''); if result then SetServerTimestamp(GetInt64(pointer(aResp))) else begin InternalLog('/Timestamp call failed -> Server not available',sllWarning); fLastErrorMessage := 'Server not available - '+Trim(fLastErrorMessage); end; end; function TSQLRestClientURI.InternalRemoteLogSend(const aText: RawUTF8): boolean; begin result := URI(Model.getURICallBack('RemoteLog',nil,0), 'PUT',nil,nil,@aText).Lo in [HTTP_SUCCESS,HTTP_NOCONTENT]; end; {$ifdef MSWINDOWS} type TSQLRestClientURIServiceNotification = class(TServiceMethodExecute) protected fOwner: TSQLRestClientURI; fInstance: pointer; // weak IInvokable reference fPar: RawUTF8; end; procedure TSQLRestClientURI.ServiceNotificationMethodViaMessages(hWnd: HWND; Msg: UINT); begin if Msg=0 then hWnd := 0; // avoid half defined parameters fServiceNotificationMethodViaMessages.Wnd := hWnd; fServiceNotificationMethodViaMessages.Msg := Msg; end; class procedure TSQLRestClientURI.ServiceNotificationMethodExecute(var Msg : TMessage); var exec: TSQLRestClientURIServiceNotification; begin exec := pointer(Msg.LParam); if exec<>nil then try try if exec.InheritsFrom(TSQLRestClientURIServiceNotification) and (HWND(Msg.WParam)=exec.fOwner.fServiceNotificationMethodViaMessages.Wnd) then // run asynchronous notification callback in the main UI thread context exec.ExecuteJson([exec.fInstance],pointer(exec.fPar),nil); finally exec.Free; // always release notification resources end; except ; // ignore any exception for this asynchronous callback execution end; end; {$endif MSWINDOWS} type TServiceInternalMethod = (imFree, imContract, imSignature, imInstance); const SERVICE_PSEUDO_METHOD: array[TServiceInternalMethod] of RawUTF8 = ( '_free_','_contract_','_signature_','_instance_'); SERVICE_PSEUDO_METHOD_COUNT = Length(SERVICE_PSEUDO_METHOD); procedure TSQLRestClientURI.InternalNotificationMethodExecute( var Ctxt: TSQLRestURIParams); var url,root,interfmethod,interf,id,method,frames: RawUTF8; callback: TSQLRestClientCallbackItem; methodIndex: integer; WR: TTextWriter; temp: TTextWriterStackBuffer; ok: Boolean; procedure Call(methodIndex: Integer; const par: RawUTF8; res: TTextWriter); var method: PServiceMethod; exec: TServiceMethodExecute; begin method := @callback.Factory.Methods[methodIndex]; {$ifdef MSWINDOWS} if (fServiceNotificationMethodViaMessages.Wnd<>0) and (method^.ArgsOutputValuesCount=0) then begin // expects no output -> asynchronous non blocking notification in UI thread Ctxt.OutStatus := 0; exec := TSQLRestClientURIServiceNotification.Create(method); TSQLRestClientURIServiceNotification(exec).fOwner := self; TSQLRestClientURIServiceNotification(exec).fInstance := callback.Instance; TSQLRestClientURIServiceNotification(exec).fPar := par; with fServiceNotificationMethodViaMessages do ok := PostMessage(Wnd,Msg,Wnd,LPARAM(exec)); if ok then exit; end else // if PostMessage() failed (e.g. invalid Wnd/Msg) -> blocking exec {$endif} exec := TServiceMethodExecute.Create(method); try ok := exec.ExecuteJson([callback.Instance],pointer(par),res); Ctxt.OutHead := exec.ServiceCustomAnswerHead; Ctxt.OutStatus := exec.ServiceCustomAnswerStatus; finally exec.Free; end; end; begin Ctxt.OutStatus := HTTP_BADREQUEST; url := Ctxt.Url; if (url='') or (isDestroying in fInternalState) then exit; if url[1]='/' then system.delete(url,1,1); Split(Split(url,'/',root),'/',interfmethod,id); // 'root/BidirCallback.AsynchEvent/1' if not IdemPropNameU(root,Model.Root) then exit; callback.ID := GetInteger(pointer(id)); if callback.ID<=0 then exit; if interfmethod=SERVICE_PSEUDO_METHOD[imFree] then begin if fFakeCallbacks.FindAndRelease(callback.ID) then Ctxt.OutStatus := HTTP_SUCCESS; exit; end; if not fFakeCallbacks.FindEntry(callback) then exit; if (Ctxt.InHead<>'') and (callback.Factory.MethodIndexCurrentFrameCallback>=0) then begin FindNameValue(Ctxt.InHead,'SEC-WEBSOCKET-FRAME: ',frames); end; Split(interfmethod,'.',interf,method); methodIndex := callback.Factory.FindMethodIndex(method); if methodIndex<0 then exit; if IdemPropNameU(interfmethod,callback.Factory.Methods[methodIndex].InterfaceDotMethodName) then try WR := TJSONSerializer.CreateOwnedStream(temp); try WR.AddShort('{"result":['); if frames='[0]' then // call before the first method of the jumbo frame Call(callback.Factory.MethodIndexCurrentFrameCallback,frames,nil); Call(methodIndex,Ctxt.InBody,WR); if ok then begin if Ctxt.OutHead='' then begin // <>'' if set via TServiceCustomAnswer WR.Add(']','}'); Ctxt.OutStatus := HTTP_SUCCESS; end; Ctxt.OutBody := WR.Text; end else Ctxt.OutStatus := HTTP_SERVERERROR; if frames='[1]' then // call after the last method of the jumbo frame Call(callback.Factory.MethodIndexCurrentFrameCallback,frames,nil); finally WR.Free; end; except on E: Exception do begin Ctxt.OutHead := ''; Ctxt.OutBody := ObjectToJSONDebug(E); Ctxt.OutStatus := HTTP_SERVERERROR; end; end; end; {$ifdef LVCL} // SyncObjs.TEvent not available in LVCL yet function TSQLRestClientURI.ServerRemoteLog(Sender: TTextWriter; Level: TSynLogInfo; const Text: RawUTF8): boolean; begin result := InternalRemoteLogSend(Text); end; {$else} type TRemoteLogThread = class(TSQLRestThread) protected fClient: TSQLRestClientURI; fPendingRows: RawUTF8; procedure InternalExecute; override; public constructor Create(aClient: TSQLRestClientURI); reintroduce; destructor Destroy; override; procedure AddRow(const aText: RawUTF8); end; constructor TRemoteLogThread.Create(aClient: TSQLRestClientURI); begin fClient := aClient; inherited Create(aClient,false,false); end; destructor TRemoteLogThread.Destroy; var i: integer; begin if fPendingRows<>'' then begin fEvent.SetEvent; for i := 1 to 200 do begin SleepHiRes(10); if fPendingRows='' then break; end; end; inherited Destroy; end; procedure TRemoteLogThread.AddRow(const aText: RawUTF8); begin fSafe.Lock; try AddToCSV(aText,fPendingRows,#13#10); finally fSafe.UnLock; end; fEvent.SetEvent; end; procedure TRemoteLogThread.InternalExecute; var aText: RawUTF8; begin while not Terminated do if FixedWaitFor(fEvent,INFINITE)=wrSignaled then begin if Terminated then break; fSafe.Lock; try aText := fPendingRows; fPendingRows := ''; finally fSafe.UnLock; end; if (aText<>'') and not Terminated then try while not fClient.InternalRemoteLogSend(aText) do if SleepOrTerminated(2000) then // retry after 2 seconds delay exit; except on E: Exception do if (fClient<>nil) and not Terminated then fClient.InternalLog('%.Execute fatal error: %'+ 'some events were not transmitted',[ClassType,E],sllWarning); end; end; end; function TSQLRestClientURI.ServerRemoteLog(Sender: TTextWriter; Level: TSynLogInfo; const Text: RawUTF8): boolean; begin if fRemoteLogThread=nil then result := InternalRemoteLogSend(Text) else begin TRemoteLogThread(fRemoteLogThread).AddRow(Text); result := true; end; end; {$endif LVCL} function TSQLRestClientURI.ServerRemoteLog(Level: TSynLogInfo; FormatMsg: PUTF8Char; const Args: array of const): boolean; begin result := ServerRemoteLog(nil,Level, FormatUTF8('%00% %',[NowToString(false),LOG_LEVEL_TEXT[Level], FormatUTF8(FormatMsg,Args)])); end; procedure TSQLRestClientURI.ServerRemoteLogStart(aLogClass: TSynLogClass; aClientOwnedByFamily: boolean); begin if (fRemoteLogClass<>nil) or (aLogClass=nil) then exit; {$ifdef WITHLOG} SetLogClass(TSynLog.Void); // this client won't log anything {$endif} if not ServerRemoteLog(sllClient,'Remote Client % Connected',[self]) then // first test server without threading raise ECommunicationException.CreateUTF8( 'Connection to RemoteLog server impossible'#13#10'%',[LastErrorMessage]); {$ifndef LVCL} if fRemoteLogThread<>nil then raise ECommunicationException.CreateUTF8('%.ServerRemoteLogStart twice',[self]); fRemoteLogThread := TRemoteLogThread.Create(self); {$endif} fRemoteLogClass := aLogClass.Add; aLogClass.Family.EchoRemoteStart(self,ServerRemoteLog,aClientOwnedByFamily); fRemoteLogOwnedByFamily := aClientOwnedByFamily; end; procedure TSQLRestClientURI.ServerRemoteLogStop; begin if fRemoteLogClass=nil then exit; if not fRemoteLogOwnedByFamily then begin fRemoteLogClass.Log(sllTrace,'End Echoing to remote server'); fRemoteLogClass.Family.EchoRemoteStop; end; fRemoteLogClass := nil; end; function TSQLRestClientURI.UpdateFromServer(const Data: array of TObject; out Refreshed: boolean; PCurrentRow: PInteger): boolean; // notes about refresh mechanism: // - if server doesn't implement InternalState, its value is 0 -> always refresh // - if any TSQLTableJSON or TSQLRecord belongs to a TSQLRestStorage, // the Server stated fInternalState=cardinal(-1) for them -> always refresh var i: integer; State: cardinal; Resp: RawUTF8; T: TSQLTableJSON; TRefreshed: boolean; // to check for each Table refresh const TState: array[boolean] of TOnTableUpdateState = (tusNoChange,tusChanged); begin result := self<>nil; Refreshed := false; if not result then exit; // avoid GPF State := ServerInternalState; // get revision state from server for i := 0 to high(Data) do if Data[i]<>nil then if TObject(Data[i]).InheritsFrom(TSQLTableJSON) then begin T := TSQLTableJSON((Data[i])); if (T.QuerySQL<>'') and (T.InternalState<>State) then begin // refresh needed? with URI(Model.Root,'GET',@Resp,nil,@T.QuerySQL) do if Lo=HTTP_SUCCESS then begin // GET with SQL sent if Assigned(OnTableUpdate) then OnTableUpdate(T,tusPrepare); TRefreshed := false; if not T.UpdateFrom(Resp,TRefreshed,PCurrentRow) then result := false else // mark error retrieving new content T.fInternalState := Hi; if TRefreshed then Refreshed := true; if Assigned(OnTableUpdate) then OnTableUpdate(T,TState[TRefreshed]); end else result := false; // mark error retrieving new content end; end else if TObject(Data[i]).InheritsFrom(TSQLRecord) then with TSQLRecord(Data[i]) do if (fID<>0) and (InternalState<>State) then begin // refresh needed? if not Refresh(fID,TSQLRecord(Data[i]),Refreshed) then result := false; // mark error retrieving new content end; end; function TSQLRestClientURI.List(const Tables: array of TSQLRecordClass; const SQLSelect: RawUTF8; const SQLWhere: RawUTF8): TSQLTableJSON; var Resp, SQL: RawUTF8; U: RawUTF8; InternalState: cardinal; begin result := nil; if high(Tables)<0 then exit; // GET Collection SQL := Model.SQLFromSelectWhere(Tables,SQLSelect,SQLWhere); if high(Tables)=0 then begin // one Table -> use REST protocol (SQL as parameters) if not IsRowID(pointer(SQLSelect)) then // ID selected by default U := '?select='+UrlEncode(SQLSelect) else U := ''; if SQLWhere<>'' then begin if U<>'' then U := U+'&where=' else U := U+'?where='; U := U+UrlEncode(SQLWhere); end; with URI(Model.URI[TSQLRecordClass(Tables[0])]+U,'GET',@Resp) do if Lo<>HTTP_SUCCESS then exit else InternalState := Hi; result := TSQLTableJSON.CreateFromTables([Tables[0]],SQL,Resp); // get data end else begin // multiple tables -> send SQL statement as HTTP body with URI(Model.Root,'GET',@Resp,nil,@SQL) do if Lo<>HTTP_SUCCESS then exit else InternalState := Hi; result := TSQLTableJSON.CreateFromTables(Tables,SQL,Resp); // get data end; result.fInternalState := InternalState; end; procedure TSQLRestClientURI.SessionRenewEvent(Sender: TSynBackgroundTimer; Event: TWaitResult; const Msg: RawUTF8); var resp: RawUTF8; status: integer; begin status := CallBack(mPOST,'CacheFlush/_ping_','',resp); InternalLog('SessionRenewEvent(%) received status=% count=% from % % (timeout=% min)', [Model.Root,status,JSONDecode(resp,'count'), SessionServer,SessionVersion,fSessionServerTimeout],sllUserAuth); end; procedure TSQLRestClientURI.SetSessionHeartbeatSeconds(timeout: integer); begin if (timeout<0) or (timeout=fSessionHeartbeatSeconds) then exit; fSessionHeartbeatSeconds := timeout; TimerEnable(SessionRenewEvent,timeout); end; function TSQLRestClientURI.SessionCreate(aAuth: TSQLRestServerAuthenticationClass; var aUser: TSQLAuthUser; const aSessionKey: RawUTF8): boolean; var period: integer; begin result := false; fSessionID := GetCardinal(pointer(aSessionKey)); if fSessionID=0 then exit; fSessionIDHexa8 := CardinalToHexLower(fSessionID); fSessionPrivateKey := crc32(crc32(0,Pointer(aSessionKey),length(aSessionKey)), pointer(aUser.PasswordHashHexa),length(aUser.PasswordHashHexa)); fSessionUser := aUser; fSessionAuthentication := aAuth; aUser := nil; // now owned by this instance if fSessionServerTimeout>0 then begin // call _ping_ every half timeout period period := fSessionServerTimeout*(60 div 2); if period>25*60 then period := 25*60; // default REST heartbeat at least every 25 minutes SetSessionHeartbeatSeconds(period); end; result := true; end; procedure TSQLRestClientURI.SessionClose; var tmp: RawUTF8; begin if (self<>nil) and (fSessionUser<>nil) and (fSessionID<>CONST_AUTHENTICATION_SESSION_NOT_STARTED) then try TimerDisable(SessionRenewEvent); InternalLog('SessionClose: notify server', sllTrace); CallBackGet('Auth',['UserName',fSessionUser.LogonName,'Session',fSessionID],tmp); finally fSessionID := CONST_AUTHENTICATION_SESSION_NOT_STARTED; fSessionIDHexa8 := ''; fSessionPrivateKey := 0; fSessionAuthentication := nil; fSessionServer := ''; fSessionVersion := ''; FillZero(fSessionData); fSessionData := ''; fSessionServerTimeout := 0; FreeAndNil(fSessionUser); fComputeSignature := TSQLRestServerAuthenticationSignedURI.ComputeSignatureCrc32; end; end; function TSQLRestClientURI.GetCurrentSessionUserID: TID; begin if fSessionUser=nil then result := 0 else result := fSessionUser.IDValue; end; function TSQLRestClientURI.GetSessionVersion: RawUTF8; var resp: RawUTF8; begin if self = nil then result := '' else begin if fSessionVersion = '' then // no session (e.g. API public URI) -> ask if CallBackGet('timestamp/info', [], resp) = HTTP_SUCCESS then fSessionVersion := JSONDecode(resp, 'version'); result := fSessionVersion; end; end; constructor TSQLRestClientURI.Create(aModel: TSQLModel); begin inherited Create(aModel); fMaximumAuthentificationRetry := 1; fComputeSignature := TSQLRestServerAuthenticationSignedURI.ComputeSignatureCrc32; fSessionID := CONST_AUTHENTICATION_NOT_USED; fFakeCallbacks := TSQLRestClientCallbacks.Create(self); {$ifdef USELOCKERDEBUG} fSafe := TAutoLockerDebug.Create(fLogClass,aModel.Root); // more verbose {$else} fSafe := TAutoLocker.Create; {$endif} end; destructor TSQLRestClientURI.Destroy; var t,i: integer; aID: TID; Table: TSQLRecordClass; begin include(fInternalState,isDestroying); {$ifdef MSWINDOWS} fServiceNotificationMethodViaMessages.Wnd := 0; // disable notification {$endif} {$ifdef WITHLOG} if GarbageCollectorFreeing then // may be owned by a TSynLogFamily SetLogClass(nil); {$endif} fBatchCurrent.Free; FreeAndNil(fFakeCallbacks); try // unlock all still locked records by this client if Model<>nil then for t := 0 to high(Model.Locks) do begin Table := Model.Tables[t]; with Model.Locks[t] do for i := 0 to Count-1 do begin aID := IDs[i]; if aID<>0 then // 0 is empty after unlock self.UnLock(Table,aID); end; end; SessionClose; // if not already notified finally // release memory and associated classes if fRemoteLogClass<>nil then begin {$ifndef LVCL} FreeAndNil(fRemoteLogThread); {$endif} ServerRemoteLogStop; end; fSessionUser.Free; try inherited Destroy; // fModel.Free if owned by this TSQLRest instance {$ifndef LVCL} FreeAndNil(fBackgroundThread); // should be done after fServices.Free fOnIdle := nil; {$endif} finally InternalClose; end; end; end; {$ifdef DOMAINAUTH} const SSPI_DEFINITION_USERNAME = '***SSPI***'; {$endif DOMAINAUTH} constructor TSQLRestClientURI.RegisteredClassCreateFrom(aModel: TSQLModel; aDefinition: TSynConnectionDefinition); begin if fModel=nil then // if not already created with a reintroduced constructor Create(aModel); if fModel<>nil then fOnIdle := fModel.OnClientIdle; // allow UI interactivity during SetUser() if aDefinition.User<>'' then begin {$ifdef DOMAINAUTH} if aDefinition.User=SSPI_DEFINITION_USERNAME then SetUser('',aDefinition.PasswordPlain) else {$endif DOMAINAUTH} SetUser(aDefinition.User,aDefinition.PasswordPlain,true); end; end; procedure TSQLRestClientURI.DefinitionTo(Definition: TSynConnectionDefinition); begin if Definition=nil then exit; inherited DefinitionTo(Definition); // save Kind if (fSessionAuthentication<>nil) and (fSessionUser<>nil) then begin {$ifdef DOMAINAUTH} if fSessionAuthentication.InheritsFrom(TSQLRestServerAuthenticationSSPI) then Definition.User := SSPI_DEFINITION_USERNAME else {$endif DOMAINAUTH} Definition.User := fSessionUser.LogonName; Definition.PasswordPlain := fSessionUser.fPasswordHashHexa; end; end; procedure TSQLRestClientURI.Commit(SessionID: cardinal; RaiseException: boolean); begin inherited Commit(CONST_AUTHENTICATION_NOT_USED,RaiseException); // inherited Commit = reset fTransactionActiveSession flag URI(Model.Root,'END'); end; procedure TSQLRestClientURI.RollBack(SessionID: cardinal); begin inherited RollBack(CONST_AUTHENTICATION_NOT_USED); // reset fTransactionActiveSession flag URI(Model.Root,'ABORT'); end; function TSQLRestClientURI.TransactionBegin(aTable: TSQLRecordClass; SessionID: cardinal): boolean; begin result := inherited TransactionBegin(aTable,CONST_AUTHENTICATION_NOT_USED); if result then // fTransactionActiveSession flag was not already set if aTable=nil then result := URI(Model.Root,'BEGIN').Lo in [HTTP_SUCCESS,HTTP_NOCONTENT] else result := URI(Model.URI[aTable],'BEGIN').Lo in [HTTP_SUCCESS,HTTP_NOCONTENT]; end; function TSQLRestClientURI.TransactionBeginRetry(aTable: TSQLRecordClass; Retries: integer): boolean; begin if Retries>50 then Retries := 50; // avoid loop for more than 10 seconds repeat result := TransactionBegin(aTable); if result then exit; dec(Retries); if Retries<=0 then break; SleepHiRes(100); until false; end; const // log up to 2 KB of JSON response, to save space MAX_SIZE_RESPONSE_LOG = 2*1024; function TSQLRestClientURI.CallBackGet(const aMethodName: RawUTF8; const aNameValueParameters: array of const; out aResponse: RawUTF8; aTable: TSQLRecordClass; aID: TID; aResponseHead: PRawUTF8): integer; var url, header: RawUTF8; {$ifdef WITHLOG} log: ISynLog; // for Enter auto-leave to work with FPC {$endif} begin if self=nil then result := HTTP_UNAVAILABLE else begin url := Model.getURICallBack(aMethodName,aTable,aID); if high(aNameValueParameters)>0 then url := url+UrlEncode(aNameValueParameters); {$ifdef WITHLOG} log := fLogClass.Enter('CallBackGet %',[url],self); {$endif} result := URI(url,'GET',@aResponse,@header).Lo; if aResponseHead<>nil then aResponseHead^ := header; {$ifdef WITHLOG} if (log<>nil) and (aResponse<>'') and (sllServiceReturn in fLogFamily.Level) then if IsHTMLContentTypeTextual(pointer(header)) then log.Log(sllServiceReturn,aResponse,self,MAX_SIZE_RESPONSE_LOG) else log.Log(sllServiceReturn,'% bytes [%]',[length(aResponse),header],self); {$endif} end; end; function TSQLRestClientURI.SetUser(const aUserName, aPassword: RawUTF8; aHashedPassword: Boolean): boolean; const HASH: array[boolean] of TSQLRestServerAuthenticationClientSetUserPassword = (passClear, passHashed); begin if self=nil then begin result := false; exit; end; {$ifdef DOMAINAUTH} // try Windows/GSSAPI authentication with the current logged user result := true; if (IsVoid(aUserName) or (PosExChar({$ifdef GSSAPIAUTH}'@'{$else}'\'{$endif},aUserName)>0)) and TSQLRestServerAuthenticationSSPI.ClientSetUser(self,aUserName,aPassword,passKerberosSPN) then exit; {$endif DOMAINAUTH} result := TSQLRestServerAuthenticationDefault. ClientSetUser(self,aUserName,aPassword,HASH[aHashedPassword]); end; procedure TSQLRestClientURI.SetLastException(E: Exception; ErrorCode: integer; Call: PSQLRestURIParams); begin fLastErrorCode := ErrorCode; if E=nil then begin fLastErrorException := nil; if StatusCodeIsSuccess(ErrorCode) then fLastErrorMessage := '' else StatusCodeToErrorMessage(ErrorCode,fLastErrorMessage); end else begin fLastErrorException := PPointer(E)^; fLastErrorMessage := ObjectToJSONDebug(E); end; if Assigned(fOnFailed) then fOnFailed(self,E,Call); end; {$ifndef LVCL} // SyncObjs.TEvent not available in LVCL yet procedure TSQLRestClientURI.OnBackgroundProcess(Sender: TSynBackgroundThreadEvent; ProcessOpaqueParam: pointer); var Call: ^TSQLRestURIParams absolute ProcessOpaqueParam; begin if Call=nil then exit; InternalURI(Call^); if ((Sender=nil) or OnIdleBackgroundThreadActive) and not(isDestroying in fInternalState) then if (Call^.OutStatus=HTTP_NOTIMPLEMENTED) and (isOpened in fInternalState) then begin InternalClose; // force recreate connection Exclude(fInternalState,isOpened); if ((Sender=nil) or OnIdleBackgroundThreadActive) then begin InternalURI(Call^); // try request again if Call^.OutStatus<>HTTP_NOTIMPLEMENTED then Include(fInternalState,isOpened); end; end else Include(fInternalState,isOpened); end; function TSQLRestClientURI.GetOnIdleBackgroundThreadActive: boolean; begin result := (self<>nil) and Assigned(fOnIdle) and fBackgroundThread.OnIdleBackgroundThreadActive; end; {$endif LVCL} function TSQLRestClientURI.FakeCallbackRegister(Sender: TServiceFactoryClient; const Method: TServiceMethod; const ParamInfo: TServiceMethodArgument; ParamValue: Pointer): integer; begin raise EServiceException.CreateUTF8('% does not support interface parameters '+ 'for %.%(%: %): consider using another kind of client', [self,Sender.fInterface.fInterfaceName,Method.URI, ParamInfo.ParamName^,ParamInfo.ArgTypeName^]); end; function TSQLRestClientURI.FakeCallbackUnregister(Factory: TInterfaceFactory; FakeCallbackID: integer; Instance: pointer): boolean; begin raise EServiceException.CreateUTF8( '% does not support % callbacks: consider using another kind of client', [self,Factory.fInterfaceTypeInfo^.Name]); end; function TSQLRestClientURI.URI(const url, method: RawUTF8; Resp: PRawUTF8; Head: PRawUTF8; SendData: PRawUTF8): Int64Rec; var retry: Integer; aUserName, aPassword: string; StatusMsg: RawUTF8; Call: TSQLRestURIParams; aPasswordHashed: Boolean; procedure CallInternalURI; begin Call.Url := url; // reset to allow proper re-sign if fSessionAuthentication<>nil then fSessionAuthentication.ClientSessionSign(Self,Call); Call.Method := method; if SendData<>nil then Call.InBody := SendData^; if Assigned(fOnEncryptBody) then fOnEncryptBody(self,Call.InBody,Call.InHead,Call.Url); {$ifndef LVCL} if Assigned(fOnIdle) then begin if fBackgroundThread=nil then fBackgroundThread := TSynBackgroundThreadEvent.Create(OnBackgroundProcess, OnIdle,FormatUTF8('% % background',[Self,Model.Root])); if not fBackgroundThread.RunAndWait(@Call) then Call.OutStatus := HTTP_UNAVAILABLE; end else {$endif} OnBackgroundProcess({SenderThread=}nil,@Call); if Assigned(fOnDecryptBody) then fOnDecryptBody(self,Call.OutBody,Call.OutHead,Call.Url); result.Lo := Call.OutStatus; result.Hi := Call.OutInternalState; if Head<>nil then Head^ := Call.OutHead; if Resp<>nil then Resp^ := Call.OutBody; fLastErrorCode := Call.OutStatus; end; begin if Self=nil then begin Int64(result) := HTTP_UNAVAILABLE; SetLastException(nil,HTTP_UNAVAILABLE); exit; end; fLastErrorMessage := ''; fLastErrorException := nil; if fServerTimestampOffset=0 then begin if not ServerTimestampSynchronize then begin Int64(result) := HTTP_UNAVAILABLE; exit; // if Timestamp is not available,server is down! end; end; Call.Init; if (Head<>nil) and (Head^<>'') then Call.InHead := Head^; if fSessionHttpHeader<>'' then Call.InHead := Trim(Call.InHead + #13#10 + fSessionHttpHeader); try CallInternalURI; if (Call.OutStatus=HTTP_TIMEOUT) and RetryOnceOnTimeout then begin InternalLog('% % returned "408 Request Timeout" -> RETRY',[method,url],sllError); CallInternalURI; end else if (Call.OutStatus=HTTP_FORBIDDEN) and (MaximumAuthentificationRetry>0) and Assigned(OnAuthentificationFailed) and not(isInAuth in fInternalState) then try Include(fInternalState,isInAuth); retry := 1; while retry<=MaximumAuthentificationRetry do begin // "403 Forbidden" in case of authentication failure -> try relog if OnAuthentificationFailed(retry,aUserName,aPassword,aPasswordHashed) and SetUser(StringToUTF8(aUserName),StringToUTF8(aPassword),aPasswordHashed) then begin CallInternalURI; break; end; Inc(retry); end; finally Exclude(fInternalState,isInAuth); end; if not StatusCodeIsSuccess(Call.OutStatus) then begin StatusCodeToErrorMessage(Call.OutStatus,StatusMsg); if Call.OutBody='' then fLastErrorMessage := StatusMsg else fLastErrorMessage := Call.OutBody; InternalLog('% % returned % (%) with message %', [method,url,Call.OutStatus,StatusMsg,fLastErrorMessage],sllError); if Assigned(fOnFailed) then fOnFailed(Self,nil,@Call); end; except on E: Exception do begin Int64(result) := HTTP_NOTIMPLEMENTED; // 501 SetLastException(E,HTTP_NOTIMPLEMENTED,@Call); exit; end; end; end; function TSQLRestClientURI.CallBackGetResult(const aMethodName: RawUTF8; const aNameValueParameters: array of const; aTable: TSQLRecordClass; aID: TID): RawUTF8; var aResponse: RawUTF8; begin if CallBackGet(aMethodName,aNameValueParameters,aResponse,aTable,aID)=HTTP_SUCCESS then result := JSONDecode(aResponse) else result := ''; end; function TSQLRestClientURI.CallBackPut(const aMethodName, aSentData: RawUTF8; out aResponse: RawUTF8; aTable: TSQLRecordClass; aID: TID; aResponseHead: PRawUTF8): integer; begin result := CallBack(mPUT,aMethodName,aSentData,aResponse,aTable,aID,aResponseHead); end; function TSQLRestClientURI.CallBack(method: TSQLURIMethod; const aMethodName,aSentData: RawUTF8; out aResponse: RawUTF8; aTable: TSQLRecordClass; aID: TID; aResponseHead: PRawUTF8): integer; var u, m: RawUTF8; {$ifdef WITHLOG} log: ISynLog; // for Enter auto-leave to work with FPC {$endif} begin if (self=nil) or (method=mNone) then result := HTTP_UNAVAILABLE else begin u := Model.getURICallBack(aMethodName,aTable,aID); {$ifdef WITHLOG} log := fLogClass.Enter('Callback %',[u],self); {$endif} m := TrimLeftLowerCaseShort(GetEnumName(TypeInfo(TSQLURIMethod),ord(method))); result := URI(u,m,@aResponse,aResponseHead,@aSentData).Lo; InternalLog('% result=% resplen=%',[m,result,length(aResponse)],sllServiceReturn); end; end; procedure TSQLRestClientURI.CallbackNonBlockingSetHeader(out Header: RawUTF8); begin // nothing to do by default (plain REST/HTTP works in blocking mode) end; function TSQLRestClientURI.ServiceRegister(const aInterfaces: array of PTypeInfo; aInstanceCreation: TServiceInstanceImplementation; const aContractExpected: RawUTF8): boolean; begin result := False; if (self=nil) or (high(aInterfaces)<0) then exit; result := (ServiceContainer as TServiceContainerClient).AddInterface( aInterfaces,aInstanceCreation,aContractExpected); end; function TSQLRestClientURI.ServiceRegister(aInterface: PTypeInfo; aInstanceCreation: TServiceInstanceImplementation; const aContractExpected: RawUTF8; aIgnoreAnyException: boolean): TServiceFactory; begin result := nil; if (self=nil) or (aInterface=nil) then begin SetLastException; exit; end; with ServiceContainer as TServiceContainerClient do try result := AddInterface(aInterface,aInstanceCreation,aContractExpected); except on E: Exception do if aIgnoreAnyException then SetLastException(E) else raise; end; end; function TSQLRestClientURI.ServiceRegisterClientDriven(aInterface: PTypeInfo; out Obj; const aContractExpected: RawUTF8): boolean; var Factory: TServiceFactory; begin Factory := ServiceRegister(aInterface,sicClientDriven,aContractExpected); if Factory<>nil then begin result := true; Factory.Get(Obj); end else result := false; end; function TSQLRestClientURI.ServiceDefine(const aInterfaces: array of TGUID; aInstanceCreation: TServiceInstanceImplementation; const aContractExpected: RawUTF8): boolean; begin if self<>nil then result := ServiceRegister(TInterfaceFactory.GUID2TypeInfo(aInterfaces), aInstanceCreation,aContractExpected) else result := false; end; function TSQLRestClientURI.ServiceDefine(const aInterface: TGUID; aInstanceCreation: TServiceInstanceImplementation; const aContractExpected: RawUTF8; aIgnoreAnyException: boolean): TServiceFactoryClient; begin result := TServiceFactoryClient( ServiceRegister(TInterfaceFactory.GUID2TypeInfo(aInterface), aInstanceCreation,aContractExpected,aIgnoreAnyException)); end; function TSQLRestClientURI.ServiceDefineClientDriven(const aInterface: TGUID; out Obj; const aContractExpected: RawUTF8): boolean; begin result := ServiceRegisterClientDriven( TInterfaceFactory.GUID2TypeInfo(aInterface),Obj,aContractExpected); end; function TSQLRestClientURI.ServiceDefineSharedAPI(const aInterface: TGUID; const aContractExpected: RawUTF8; aIgnoreAnyException: boolean): TServiceFactoryClient; begin try result := ServiceDefine(aInterface,sicShared,aContractExpected,aIgnoreAnyException); if result<>nil then begin result.ParamsAsJSONObject := true; // no contract -> explicit parameters result.ResultAsJSONObjectWithoutResult := true; end; except if aIgnoreAnyException then result := nil else raise; end; end; procedure TSQLRestClientURI.ServicePublishOwnInterfaces(OwnServer: TSQLRestServer); begin fServicePublishOwnInterfaces := OwnServer.ServicesPublishedInterfaces; end; function TSQLRestClientURI.ServiceRetrieveAssociated(const aServiceName: RawUTF8; out URI: TSQLRestServerURIDynArray): boolean; var json: RawUTF8; begin result := (CallBackGet('stat',['findservice',aServiceName],json)=HTTP_SUCCESS) and (DynArrayLoadJSON(URI,pointer(json),TypeInfo(TSQLRestServerURIDynArray))<>nil); end; function TSQLRestClientURI.ServiceRetrieveAssociated(const aInterface: TGUID; out URI: TSQLRestServerURIDynArray): boolean; var fact: TInterfaceFactory; begin fact := TInterfaceFactory.Get(aInterface); if fact=nil then result := false else result := ServiceRetrieveAssociated(copy(fact.InterfaceName,2,maxInt),URI); end; function TSQLRestClientURI.EngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; var P: PUTF8Char; url, Head: RawUTF8; begin result := 0; url := Model.URI[Model.Tables[TableModelIndex]]; if URI(url,'POST',nil,@Head,@SentData).Lo<>HTTP_CREATED then exit; // response must be '201 Created' P := pointer(Head); // we need to check the headers if P<>nil then repeat // find ID from 'Location: Member Entry URI' header entry if IdemPChar(P,'LOCATION:') then begin // 'Location: root/People/11012' e.g. inc(P,9); while P^>#13 do inc(P); // go to end of line P^ := #0; // make line asciiz, even if ended with #13 while P[-1] in ['0'..'9'] do dec(P); // get all number chars if P[-1]='-' then dec(P); result := GetInt64(P); // get numerical value at the end of the URI exit; end; while not (P^ in [#0,#13]) do inc(P); if P^=#0 then break else inc(P); if P^=#10 then inc(P); until P^=#0; end; function TSQLRestClientURI.EngineDelete(TableModelIndex: integer; ID: TID): boolean; var url: RawUTF8; begin url := Model.getURIID(Model.Tables[TableModelIndex],ID); result := URI(url,'DELETE').Lo in [HTTP_SUCCESS,HTTP_NOCONTENT]; end; function TSQLRestClientURI.EngineDeleteWhere(TableModelIndex: integer; const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean; var url: RawUTF8; begin // ModelRoot/TableName?where=WhereClause to delete members url := Model.getURI(Model.Tables[TableModelIndex])+'?where='+UrlEncode(SQLWhere); result := URI(url,'DELETE').Lo in [HTTP_SUCCESS,HTTP_NOCONTENT]; end; function TSQLRestClientURI.EngineList(const SQL: RawUTF8; ForceAJAX: Boolean; ReturnedRowCount: PPtrInt): RawUTF8; begin if (self=nil) or (SQL='') or (ReturnedRowCount<>nil) or (URI(Model.Root,'GET',@result,nil,@SQL).Lo<>HTTP_SUCCESS) then result := ''; end; function TSQLRestClientURI.ClientRetrieve(TableModelIndex: integer; ID: TID; ForUpdate: boolean; var InternalState: cardinal; var Resp: RawUTF8): boolean; begin if cardinal(TableModelIndex)<=cardinal(Model.fTablesMax) then with URIGet(Model.Tables[TableModelIndex],ID,Resp,ForUpdate) do if Lo=HTTP_SUCCESS then begin InternalState := Hi; result := true; end else result := false else result := false; end; function TSQLRestClientURI.EngineRetrieveBlob(TableModelIndex: integer; aID: TID; BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean; var url: RawUTF8; begin if (self=nil) or (aID<=0) or (BlobField=nil) then result := false else begin // URI is 'ModelRoot/TableName/TableID/BlobFieldName' with GET method url := Model.getURICallBack(BlobField^.Name,Model.Tables[TableModelIndex],aID); result := URI(url,'GET',@BlobData).Lo=HTTP_SUCCESS; end; end; function TSQLRestClientURI.EngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; var url: RawUTF8; begin url := Model.getURIID(Model.Tables[TableModelIndex],ID); result := URI(url,'PUT',nil,nil,@SentData).Lo in [HTTP_SUCCESS,HTTP_NOCONTENT]; end; function TSQLRestClientURI.EngineUpdateBlob(TableModelIndex: integer; aID: TID; BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean; var url, Head: RawUTF8; begin Head := 'Content-Type: application/octet-stream'; if (self=nil) or (aID<=0) or (BlobField=nil) then result := false else begin // PUT ModelRoot/TableName/TableID/BlobFieldName FormatUTF8('%/%/%',[Model.URI[Model.Tables[TableModelIndex]],aID,BlobField^.Name],url); result := URI(url,'PUT',nil,@Head,@BlobData).Lo in [HTTP_SUCCESS,HTTP_NOCONTENT]; end; end; function TSQLRestClientURI.EngineUpdateField(TableModelIndex: integer; const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; var url: RawUTF8; begin if TableModelIndex<0 then result := false else begin // PUT ModelRoot/TableName?setname=..&set=..&wherename=..&where=.. FormatUTF8('%?setname=%&set=%&wherename=%&where=%', [Model.URI[Model.Tables[TableModelIndex]], SetFieldName,UrlEncode(SetValue),WhereFieldName,UrlEncode(WhereValue)],url); result := URI(url,'PUT').Lo in [HTTP_SUCCESS,HTTP_NOCONTENT]; end; end; function TSQLRestClientURI.EngineBatchSend(Table: TSQLRecordClass; var Data: RawUTF8; var Results: TIDDynArray; ExpectedResultsCount: integer): integer; var Resp: RawUTF8; R: PUTF8Char; i: integer; begin // TSQLRest.BatchSend() ensured that Batch contains some data try // URI is 'ModelRoot/Batch' or 'ModelRoot/Batch/TableName' with PUT method result := URI(Model.getURICallBack('Batch',Table,0),'PUT',@Resp,nil,@Data).Lo; if result<>HTTP_SUCCESS then exit; // returned Resp shall be an array of integers: '[200,200,...]' R := pointer(Resp); if R<>nil then while not (R^ in ['[',#0]) do inc(R); result := HTTP_BADREQUEST; if (R=nil) or (R^<>'[') then // invalid response exit; SetLength(Results,ExpectedResultsCount); if IdemPChar(R,'["OK"]') then begin // to save bandwith if no adding for i := 0 to ExpectedResultsCount-1 do Results[i] := HTTP_SUCCESS; end else begin inc(R); // jump first '[' for i := 0 to ExpectedResultsCount-1 do begin Results[i] := GetJSONInt64Var(R); while R^ in [#1..' '] do inc(R); case R^ of ',': inc(R); ']': break; else exit; end; end; if R^<>']' then exit; end; result := HTTP_SUCCESS; // returns OK finally BatchAbort; end; end; procedure TSQLRestClientURI.BatchAbort; begin if self<>nil then FreeAndNil(fBatchCurrent); end; function TSQLRestClientURI.BatchAdd(Value: TSQLRecord; SendData: boolean; ForceID: boolean=false; const CustomFields: TSQLFieldBits=[]): integer; begin if self=nil then result := -1 else result := fBatchCurrent.Add(Value,SendData,ForceID,CustomFields); end; function TSQLRestClientURI.BatchCount: integer; begin if self=nil then result := 0 else result := fBatchCurrent.Count; end; function TSQLRestClientURI.BatchDelete(ID: TID): integer; begin if self=nil then result := -1 else result := fBatchCurrent.Delete(ID); end; function TSQLRestClientURI.BatchDelete(Table: TSQLRecordClass; ID: TID): integer; begin if self=nil then result := -1 else result := fBatchCurrent.Delete(Table,ID); end; function TSQLRestClientURI.BatchStart(aTable: TSQLRecordClass; AutomaticTransactionPerRow: cardinal; Options: TSQLRestBatchOptions): boolean; begin if (self=nil) or (fBatchCurrent<>nil) then result := false else begin fBatchCurrent := TSQLRestBatch.Create(self,aTable,AutomaticTransactionPerRow,Options); fBatchCurrent.fCalledWithinRest := true; result := true; end; end; function TSQLRestClientURI.BatchStartAny(AutomaticTransactionPerRow: cardinal; Options: TSQLRestBatchOptions): boolean; begin result := BatchStart(nil,AutomaticTransactionPerRow,Options); end; function TSQLRestClientURI.BatchUpdate(Value: TSQLRecord; const CustomFields: TSQLFieldBits; DoNotAutoComputeFields: boolean): integer; begin if (self=nil) or (Value=nil) or (fBatchCurrent=nil) or (Value.fID<=0) or not BeforeUpdateEvent(Value) then result := -1 else result := fBatchCurrent.Update(Value,CustomFields,DoNotAutoComputeFields); end; function TSQLRestClientURI.BatchSend(var Results: TIDDynArray): integer; begin if self<>nil then try result := BatchSend(fBatchCurrent,Results); finally FreeAndNil(fBatchCurrent); end else result := HTTP_BADREQUEST; end; { TSQLRestServer } var GlobalURIRequestServer: TSQLRestServer = nil; function URIRequest(url, method, SendData: PUTF8Char; Resp, Head: PPUTF8Char): Int64Rec; cdecl; function StringToPCharCopy(const s: RawUTF8): PUTF8Char; var L: integer; begin L := length(s); if L=0 then result := nil else begin inc(L); // copy also last #0 from s {$ifdef MSWINDOWS} if not USEFASTMM4ALLOC then result := pointer(GlobalAlloc(GMEM_FIXED,L)) else {$endif} GetMem(result,L); MoveFast(pointer(s)^,result^,L); end; end; var call: TSQLRestURIParams; begin if GlobalURIRequestServer=nil then begin Int64(result) := HTTP_NOTIMPLEMENTED; // 501 exit; end; call.Init; call.Url := url; call.Method := method; call.LowLevelConnectionID := PtrInt(GlobalURIRequestServer); call.LowLevelFlags := [llfSecured]; // in-process communication is safe call.InHead := 'RemoteIP: 127.0.0.1'; if (Head<>nil) and (Head^<>nil) then call.InHead := RawUTF8(Head^)+#13#10+call.InHead; SetString(call.InBody,SendData,StrLen(SendData)); call.RestAccessRights := @SUPERVISOR_ACCESS_RIGHTS; GlobalURIRequestServer.URI(call); result.Lo := call.OutStatus; result.Hi := call.OutInternalState; if Head<>nil then Head^ := StringToPCharCopy(call.OutHead); if Resp<>nil then Resp^ := StringToPCharCopy(call.OutBody); end; procedure AdministrationExecuteGetFiles(const Folder, Mask: TFileName; const Param: RawUTF8; var Answer: TServiceCustomAnswer); var files: TFindFilesDynArray; fn: TFileName; fs: Int64; begin if (Param<>'*') and (PosExChar(':',Param)=0) and (PosExChar(PathDelim,Param)=0) then begin fn := IncludeTrailingPathDelimiter(Folder)+UTF8ToString(Param); fs := FileSize(fn); if (fs>0) and (fs<256 shl 20) then begin // download up to 256 MB Answer.Content := StringFromFile(fn); if Answer.Content<>'' then begin Answer.Header := BINARY_CONTENT_TYPE_HEADER+#13#10'FileName: '+Param; exit; end; end; end; files := FindFiles(Folder,Mask,'',true,false); Answer.Content := DynArraySaveJSON(files,TypeInfo(TFindFilesDynArray)); end; function ReadString(Handle: cardinal): RawUTF8; var L, Read: cardinal; P: PUTF8Char; begin result := ''; if (FileRead(Handle,L,4)=4) and (L<>0) then begin SetLength(result,L); P := pointer(result); repeat Read := FileRead(Handle,P^,L); if Read=0 then begin SleepHiRes(10); // nothing available -> wait a little and retry Read := FileRead(Handle,P^,L); if Read=0 then // server may be down -> abort raise ECommunicationException.Create('ReadString'); end; inc(P,Read); dec(L,Read); until L=0; // loop until received all expected data end; end; procedure WriteString(Handle: cardinal; const Text: RawUTF8); var L: cardinal; begin L := length(Text); if L=0 then // write cardinal 0 if Text='' FileWrite(Handle,L,4) else // write length+content at once {$ifdef FPC} begin FileWrite(Handle,L,4); FileWrite(Handle,pointer(Text)^,L); end; {$else} FileWrite(Handle,pointer(PtrInt(Text)-4)^,L+4); {$endif} end; {$ifdef MSWINDOWS} const ServerPipeNamePrefix: TFileName = '\\.\pipe\mORMot_'; function TSQLRestServer.ExportServerNamedPipe(const ServerApplicationName: TFileName): boolean; var PipeName: TFileName; Pipe: THandle; begin result := false; if fExportServerNamedPipeThread<>nil then exit; // only one ExportServer() by running process if {$ifdef UNICODE}IdemPCharW{$else}IdemPChar{$endif}(pointer(ServerApplicationName),'\\') then PipeName := ServerApplicationName else PipeName := ServerPipeNamePrefix+ServerApplicationName; Pipe := FileOpen(PipeName,fmOpenReadWrite); // is this pipe existing? if Pipe<>Invalid_Handle_Value then begin WriteString(Pipe,''); // send integer=0 -> force server disconnect FileClose(Pipe); exit; // only one pipe server with this name at once end; fExportServerNamedPipeThread := TSQLRestServerNamedPipe.Create(self,PipeName); NoAJAXJSON := true; // use smaller JSON size in this not HTTP use (never AJAX) sleep(10); // allow the background thread to start result := true; // success end; function TSQLRestServer.ExportServerMessage(const ServerWindowName: string): boolean; begin result := false; if (self=nil) or (fServerWindow<>0) then exit; // only one ExportServerMessage() by running process fServerWindow := CreateInternalWindow(ServerWindowName,self); if fServerWindow=0 then exit; // impossible to create window -> fail fServerWindowName := ServerWindowName; result := true; end; const MAGIC_SYN: cardinal = $A5ABA5AB; procedure TSQLRestServer.AnswerToMessage(var Msg: TWMCopyData); var call: TSQLRestURIParams; P: PUTF8Char; input: PCopyDataStruct; Res: packed record Magic: cardinal; Status: cardinal; InternalState: cardinal; end; Data: TCopyDataStruct; Header, ResStr: RawUTF8; begin Msg.Result := HTTP_NOTFOUND; if (self=nil) or (Msg.From=0) then exit; input := PCopyDataStruct(Msg.CopyDataStruct); P := input^.lpData; if (P=nil) or (input^.cbData<=7) then exit; if PCardinal(P)^<>MAGIC_SYN then exit; // invalid layout: a broadcasted WM_COPYDATA message? :( inc(P,4); // #1 is a field delimiter below, since Get*Item() functions return nil for #0 Msg.Result := HTTP_SUCCESS; // Send something back call.Init; GetNextItem(P,#1,call.Url); GetNextItem(P,#1,call.Method); GetNextItem(P,#1,call.InHead); call.LowLevelConnectionID := Msg.From; Header := 'RemoteIP: 127.0.0.1'; AddToCSV(Header,call.InHead,#13#10); SetString(call.InBody,P,PtrInt(input^.cbData)-(P-input^.lpData)); call.RestAccessRights := @SUPERVISOR_ACCESS_RIGHTS; // note: it's up to URI overridden method to implement access rights URI(call); Res.Magic := MAGIC_SYN; Res.Status := call.OutStatus; Res.InternalState := call.OutInternalState; {$ifdef FPC} // alf: to circumvent FPC issues ResStr := ''; SetLength(ResStr,SizeOf(Res)+Length(call.OutHead)+1+Length(call.OutBody)); P := pointer(ResStr); System.Move(Pointer(@Res)^,P^,SizeOf(Res)); Inc(P,SizeOf(Res)); System.Move(pointer(call.OutHead)^,P^,Length(call.OutHead)); Inc(P,Length(call.OutHead)); PByte(P)^ := 1; Inc(P); System.Move(pointer(call.OutBody)^,P^,Length(call.OutBody)); {$else} FastSetString(ResStr,@Res,SizeOf(Res)); ResStr := ResStr+call.OutHead+#1+call.OutBody; {$endif FPC} Data.dwData := fServerWindow; Data.cbData := length(ResStr); Data.lpData := pointer(ResStr); SendMessage(Msg.From,WM_COPYDATA,fServerWindow,PtrInt(@Data)); end; function TSQLRestServer.CloseServerNamedPipe: boolean; begin if fExportServerNamedPipeThread<>nil then begin fExportServerNamedPipeThread.Terminate; SleepHiRes(200); // we have sleep(128) in TSQLRestServerNamedPipe.EngineExecute FreeAndNil(fExportServerNamedPipeThread); result := true; end else result := false; end; function TSQLRestServer.CloseServerMessage: boolean; begin result := ReleaseInternalWindow(fServerWindowName,fServerWindow); end; function TSQLRestServer.ExportedAsMessageOrNamedPipe: Boolean; begin result := (self<>nil) and ((fExportServerNamedPipeThread<>nil) or (fServerWindow<>0)); end; {$endif MSWINDOWS} function TSQLRestServer.ExportServer: boolean; begin {$ifdef MSWINDOWS} if (fServerWindow<>0) or (fExportServerNamedPipeThread<>nil) then result := false else // another server was running {$endif MSWINDOWS} if (GlobalURIRequestServer=nil) or (GlobalURIRequestServer=self) then begin GlobalURIRequestServer := self; result := true; end else result := false; end; procedure TSQLRestServer.ServiceMethodRegister(aMethodName: RawUTF8; const aEvent: TSQLRestServerCallBack; aByPassAuthentication: boolean); begin aMethodName := trim(aMethodName); if aMethodName='' then raise EServiceException.CreateUTF8('%.ServiceMethodRegister('''')',[self]); if Model.GetTableIndex(aMethodName)>=0 then raise EServiceException.CreateUTF8('Published method name %.% '+ 'conflicts with a Table in the Model!',[self,aMethodName]); with PSQLRestServerMethod(fPublishedMethods.AddUniqueName(aMethodName, 'Duplicated published method name %.%',[self,aMethodName]))^ do begin CallBack := aEvent; ByPassAuthentication := aByPassAuthentication; end; end; procedure TSQLRestServer.ServiceMethodRegisterPublishedMethods(const aPrefix: RawUTF8; aInstance: TObject); var i: integer; methods: TPublishedMethodInfoDynArray; begin if aInstance=nil then exit; if PosExChar('/',aPrefix)>0 then raise EServiceException.CreateUTF8('%.ServiceMethodRegisterPublishedMethods'+ '("%"): prefix should not contain "/"',[self,aPrefix]); for i := 0 to GetPublishedMethods(aInstance,methods)-1 do with methods[i] do ServiceMethodRegister(aPrefix+Name,TSQLRestServerCallBack(Method)); end; constructor TSQLRestServer.Create(aModel: TSQLModel; aHandleUserAuthentication: boolean); var t: integer; tmp: RawUTF8; begin if aModel=nil then raise EORMException.CreateUTF8('%.Create(Model=nil)',[self]); // specific server initialization fStatLevels := SERVERDEFAULTMONITORLEVELS; fVirtualTableDirect := true; // faster direct Static call by default fSessions := TSynObjectListLocked.Create; // needed by AuthenticationRegister() below fSQLAuthUserClass := TSQLAuthUser; fSQLAuthGroupClass := TSQLAuthGroup; fModel := aModel; fSQLRecordVersionDeleteTable := TSQLRecordTableDeleted; for t := 0 to high(Model.Tables) do if fModel.Tables[t].RecordProps.RecordVersionField<>nil then begin fSQLRecordVersionDeleteTable := fModel.AddTableInherited(TSQLRecordTableDeleted); break; end; fSessionClass := TAuthSession; if aHandleUserAuthentication then // default mORMot authentication schemes AuthenticationRegister([TSQLRestServerAuthenticationDefault {$ifdef DOMAINAUTH},TSQLRestServerAuthenticationSSPI{$endif}]); fTrackChangesHistoryTableIndexCount := length(Model.Tables); SetLength(fTrackChangesHistory,fTrackChangesHistoryTableIndexCount); if fTrackChangesHistoryTableIndexCount>64 then fTrackChangesHistoryTableIndexCount := 64; // rows are identified as RecordRef SetLength(fTrackChangesHistoryTableIndex,fTrackChangesHistoryTableIndexCount); for t := 0 to fTrackChangesHistoryTableIndexCount-1 do fTrackChangesHistoryTableIndex[t] := -1; fAssociatedServices := TServicesPublishedInterfacesList.Create(0); // abstract REST initalization inherited Create(aModel); fAfterCreation := true; fStats := TSQLRestServerMonitor.Create(self); URIPagingParameters := PAGINGPARAMETERS_YAHOO; TAESPRNG.Main.Fill(@fSessionCounter,SizeOf(fSessionCounter)); if integer(fSessionCounter)<0 then // ensure positive 31-bit integer fSessionCounter := -fSessionCounter; // retrieve published methods fPublishedMethods.InitSpecific(TypeInfo(TSQLRestServerMethods), fPublishedMethod,djRawUTF8,nil,true); ServiceMethodRegisterPublishedMethods('',self); fPublishedMethodAuthIndex := ServiceMethodByPassAuthentication('Auth'); fPublishedMethodTimestampIndex := ServiceMethodByPassAuthentication('Timestamp'); tmp := 'Batch'; fPublishedMethodBatchIndex := fPublishedMethods.FindHashed(tmp); if (fPublishedMethodBatchIndex<0) or (fPublishedMethodTimestampIndex<0) then raise EORMException.CreateUTF8('%.Create: missing method!',[self]); fSafeRootUpper := UpperCase(fModel.Root)+'/_SAFE_/'; end; constructor TSQLRestServer.CreateWithOwnModel(const Tables: array of TSQLRecordClass; aHandleUserAuthentication: boolean; const aRoot: RawUTF8); var Model: TSQLModel; begin Model := TSQLModel.Create(Tables,aRoot); Create(Model,aHandleUserAuthentication); Model.Owner := self; end; class function TSQLRestServer.CreateInMemoryForAllVirtualTables(aModel: TSQLModel; aHandleUserAuthentication: boolean): TSQLRestServer; var restClass: TSQLRestClass; fake: TSynConnectionDefinition; begin fake := TSynConnectionDefinition.Create; try fake.Kind := 'TSQLRestServerDB'; restClass := TSQLRest.ClassFrom(fake); if (restClass=nil) or not restClass.InheritsFrom(TSQLRestServer) then begin // fallback if mORMotSQlite3.pas not linked result := TSQLRestServerFullMemory.Create(aModel,aHandleUserAuthentication); exit; end; fake.ServerName := ':memory:'; // avoid dependency to SynSQLite3.pas result := TSQLRestServerClass(restClass).RegisteredClassCreateFrom( aModel,aHandleUserAuthentication,fake); finally fake.Free; end; end; procedure TSQLRestServer.CreateMissingTables(user_version: cardinal=0; Options: TSQLInitializeTableOptions=[]); begin fCreateMissingTablesOptions := Options; end; procedure TSQLRestServer.InitializeTables(Options: TSQLInitializeTableOptions); var t: integer; begin if (Self<>nil) and (Model<>nil) then for t := 0 to Model.TablesMax do if not TableHasRows(Model.Tables[t]) then Model.Tables[t].InitializeTable(self,'',Options); end; constructor TSQLRestServer.RegisteredClassCreateFrom(aModel: TSQLModel; aServerHandleAuthentication: boolean; aDefinition: TSynConnectionDefinition); begin Create(aModel,aServerHandleAuthentication); end; destructor TSQLRestServer.Destroy; var i: integer; begin Shutdown; if GlobalURIRequestServer=self then begin GlobalURIRequestServer := nil; SleepHiRes(200); // way some time any request is finished in another thread end; // close any running named-pipe or GDI-messages server instance {$ifdef MSWINDOWS} CloseServerNamedPipe; CloseServerMessage; {$endif} AsynchBatchStop(nil); // may use fStaticData[] FreeAndNil(fBackgroundTimer); fRecordVersionSlaveCallbacks := nil; // should be done before fServices.Free for i := 0 to high(fStaticVirtualTable) do if fStaticVirtualTable[i]<>nil then begin // free all virtual TSQLRestStorage objects fStaticVirtualTable[i].Free; if fStaticData<>nil then fStaticData[i] := nil; // free once end; for i := 0 to high(fStaticData) do // free all TSQLRestStorage objects and update file if necessary fStaticData[i].Free; for i := 0 to fPublishedMethods.Count-1 do fPublishedMethod[i].Stats.Free; FreeAndNil(fSessions); FreeAndNil(fAssociatedServices); ObjArrayClear(fSessionAuthentication); inherited Destroy; // calls fServices.Free which will update fStats FreeAndNil(fJWTForUnauthenticatedRequest); FreeAndNil(fStats); end; procedure TSQLRestServer.Shutdown(const aStateFileName: TFileName); var timeout: Int64; {$ifdef WITHLOG} log: ISynLog; // for Enter auto-leave to work with FPC {$endif} begin if fSessions=nil then exit; // avoid GPF e.g. in case of missing sqlite3-64.dll {$ifdef WITHLOG} log := fLogClass.Enter('Shutdown(%) % CurrentRequestCount=%', [aStateFileName,fModel.Root,fStats.AddCurrentRequestCount(0)],self); {$endif} OnNotifyCallback := nil; fSessions.Safe.Lock; try if fShutdownRequested then exit; // Shutdown method already called fShutdownRequested := true; // will be identified by TSQLRestServer.URI() finally fSessions.Safe.UnLock; end; timeout := GetTickCount64+30000; // never wait forever repeat SleepHiRes(5); until (fStats.AddCurrentRequestCount(0)=0) or (GetTickCount64>timeout); if aStateFileName<>'' then SessionsSaveToFile(aStateFileName); end; function TSQLRestServer.SleepOrShutdown(MS: integer): boolean; var timeout: Int64; begin result := true; timeout := GetTickCount64+MS; repeat if fShutdownRequested then exit; if MS<=10 then SleepHiRes(MS) else SleepHiRes(1); if fShutdownRequested then exit; until (MS<=10) or (GetTickCount64>=timeout); result := false; end; function TSQLRestServer.GetStaticDataServer(aClass: TSQLRecordClass): TSQLRest; var i: cardinal; begin if (self<>nil) and (fStaticData<>nil) then begin i := Model.GetTableIndexExisting(aClass); if i=0 then begin if cardinal(aTableIndex)nil) then result := fStaticVirtualTable[aTableIndex]; end; end; function TSQLRestServer.GetStaticTableIndex(aTableIndex: integer; out Kind: TSQLRestServerKind): TSQLRest; begin result := nil; Kind := sMainEngine; if aTableIndex>=0 then begin if cardinal(aTableIndex)nil then begin Kind := sStaticDataTable; exit; end; end; if fVirtualTableDirect and (fStaticVirtualTable<>nil) then begin result := fStaticVirtualTable[aTableIndex]; if result<>nil then Kind := sVirtualTable; end; end; end; function TSQLRestServer.GetRemoteTable(TableIndex: Integer): TSQLRest; begin if (cardinal(TableIndex)>=cardinal(length(fStaticData))) or (fStaticData[TableIndex]=nil) or not fStaticData[TableIndex].InheritsFrom(TSQLRestStorageRemote) then result := nil else result := TSQLRestStorageRemote(fStaticData[TableIndex]).RemoteRest; end; function TSQLRestServer.GetVirtualTable(aClass: TSQLRecordClass): TSQLRest; var i: integer; begin result := nil; if fStaticVirtualTable<>nil then begin i := Model.GetTableIndexExisting(aClass); if (i>=0) and (Model.TableProps[i].Kind in IS_CUSTOM_VIRTUAL) then result := fStaticVirtualTable[i]; end; end; function TSQLRestServer.IsInternalSQLite3Table(aTableIndex: integer): boolean; begin result := ((cardinal(aTableIndex)>=cardinal(length(fStaticData))) or (fStaticData[aTableIndex]=nil)) and ((cardinal(aTableIndex)>=cardinal(length(fStaticVirtualTable))) or (fStaticVirtualTable[aTableIndex]=nil)); end; function TSQLRestServer.StaticDataAdd(aStaticData: TSQLRestStorage): boolean; var i,n,t: cardinal; begin result := false; if (self=nil) or (aStaticData=nil) then exit; i := Model.GetTableIndexExisting(aStaticData.StoredClass); n := length(fStaticData); if (inil) and (fStaticData[i]<>aStaticData) then exit; // TSQLRecord already registered t := length(Model.Tables); if nnil then begin // class already registered -> update file name (result as aServerClass).fFileName := aFileName; end else begin // class not already registered -> register now if aServerClass=nil then aServerClass := TSQLRestStorageInMemory; // default in-memory engine result := aServerClass.Create(aClass,self,aFileName,aBinaryFile); if not StaticDataAdd(result) then raise EORMException.CreateUTF8('Error in %.StaticDataCreate(%)',[self,aClass]); end; end; function TSQLRestServer.RemoteDataCreate(aClass: TSQLRecordClass; aRemoteRest: TSQLRest): TSQLRestStorageRemote; begin if GetStaticDataServer(aClass)<>nil then raise EORMException.CreateUTF8('Duplicate %.RemoteDataCreate(%)',[self,aClass]); result := TSQLRestStorageRemote.Create(aClass,self,aRemoteRest); if not StaticDataAdd(result) then raise EORMException.CreateUTF8('Error in %.RemoteDataCreate(%)',[self,aClass]); end; procedure TSQLRestServer.FlushInternalDBCache; begin // do nothing by default end; function SQLGetOrder(const SQL: RawUTF8): RawUTF8; var P: PUTF8Char; i: integer; begin i := PosI('ORDER BY ',SQL); if i>0 then begin inc(i,9); while SQL[i] in [#1..' '] do inc(i); // trim left result := copy(SQL,i,maxInt); P := PosChar(Pointer(Result),' '); if P=nil then P := PosChar(Pointer(Result),';'); if P<>nil then SetLength(result,P-pointer(Result)); // trim right end; if result='' then // by default, a SQLite3 query is ordered by ID result := 'RowID'; end; function TSQLRestServer.GetNoAJAXJSON: boolean; begin result := (self<>nil) and (rsoNoAJAXJSON in fOptions); end; procedure TSQLRestServer.SetNoAJAXJSON(const Value: boolean); begin if Value then include(fOptions,rsoNoAJAXJSON) else exclude(fOptions,rsoNoAJAXJSON); end; function TSQLRestServer.InternalAdaptSQL(TableIndex: integer; var SQL: RawUTF8): TSQLRest; begin result := nil; if (self<>nil) and (TableIndex>=0) then begin // SQL refers to this unique table if cardinal(TableIndex) we need to // retrieve manualy any static table from the SQL SELECT statement result := fStaticData[TableIndex]; if (result=nil) and fVirtualTableDirect and (fStaticVirtualTable<>nil) then begin result := fStaticVirtualTable[TableIndex]; // virtual table may need adaptation (e.g. RowID -> ID) if result<>nil then if result.InheritsFrom(TSQLRestStorage) and not TSQLRestStorage(result).AdaptSQLForEngineList(SQL) then // complex request will use SQlite3 virtual engine module result := nil; end; end; end; function TSQLRestServer.InternalListRawUTF8(TableIndex: integer; const SQL: RawUTF8): RawUTF8; var aSQL: RawUTF8; Rest: TSQLRest; begin aSQL := SQL; Rest := InternalAdaptSQL(TableIndex,aSQL); if Rest<>nil then // this SQL statement is handled by direct connection, faster adaptation result := Rest.EngineList(aSQL) else // complex TSQLVirtualTableJSON/External queries will rely on virtual table result := MainEngineList(SQL,false,nil); if result='[]'#$A then result := ''; end; const SQLRECORDVERSION_DELETEID_SHIFT = 58; SQLRECORDVERSION_DELETEID_RANGE = Int64(1) shl SQLRECORDVERSION_DELETEID_SHIFT; procedure TSQLRestServer.InternalRecordVersionMaxFromExisting(RetrieveNext: PID); var m: integer; field: TSQLPropInfoRTTIRecordVersion; current,max,mDeleted: Int64; begin fAcquireExecution[execORMWrite].Safe.Lock; try if fRecordVersionMax=0 then begin // check twice to avoid race condition current := 0; for m := 0 to Model.TablesMax do begin field := Model.Tables[m].RecordProps.RecordVersionField; if field<>nil then begin if OneFieldValue(Model.Tables[m],'max('+field.Name+')','',[],[],max) then if max>current then current := max; mDeleted := Int64(m) shl SQLRECORDVERSION_DELETEID_SHIFT; if OneFieldValue(fSQLRecordVersionDeleteTable,'max(ID)','ID>? and IDcurrent then current := max; end; end; end; end else current := fRecordVersionMax; if RetrieveNext<>nil then begin inc(current); RetrieveNext^ := current; end; fRecordVersionMax := current; finally fAcquireExecution[execORMWrite].Safe.UnLock; end; end; function TSQLRestServer.InternalRecordVersionComputeNext: TRecordVersion; begin if fRecordVersionMax=0 then InternalRecordVersionMaxFromExisting(@result) else begin fAcquireExecution[execORMWrite].Safe.Lock; inc(fRecordVersionMax); result := fRecordVersionMax; fAcquireExecution[execORMWrite].Safe.UnLock; end; end; function TSQLRestServer.RecordVersionCompute: TRecordVersion; begin result := InternalRecordVersionComputeNext; if result>=SQLRECORDVERSION_DELETEID_RANGE then raise EORMException.CreateUTF8('%.InternalRecordVersionCompute=% overflow: '+ '%.ID should be < 2^%)',[self,result,fSQLRecordVersionDeleteTable, SQLRECORDVERSION_DELETEID_SHIFT]); end; function TSQLRestServer.RecordVersionCurrent: TRecordVersion; begin if self=nil then result := 0 else begin if fRecordVersionMax=0 then InternalRecordVersionMaxFromExisting(nil); result := fRecordVersionMax; end; end; procedure TSQLRestServer.InternalRecordVersionHandle(Occasion: TSQLOccasion; TableIndex: integer; var Decoder: TJSONObjectDecoder; RecordVersionField: TSQLPropInfoRTTIRecordVersion); begin if RecordVersionField=nil then exit; // no TRecordVersion field to track if Decoder.FindFieldName(RecordVersionField.Name)<0 then // only compute new monotonic TRecordVersion if not already supplied by sender Decoder.AddFieldValue(RecordVersionField.Name,Int64ToUtf8(RecordVersionCompute),ftaNumber); if (fServices<>nil) then (fServices as TServiceContainerServer).RecordVersionNotifyAddUpdate( Occasion,TableIndex,Decoder); end; procedure TSQLRestServer.InternalRecordVersionDelete(TableIndex: integer; ID: TID; Batch: TSQLRestBatch); var deleted: TSQLRecordTableDeleted; revision: TRecordVersion; begin if fRecordVersionDeleteIgnore then exit; deleted := fSQLRecordVersionDeleteTable.Create; try revision := RecordVersionCompute; deleted.IDValue := revision+Int64(TableIndex) shl SQLRECORDVERSION_DELETEID_SHIFT; deleted.Deleted := ID; if Batch<>nil then Batch.Add(deleted,True,True) else Add(deleted,True,True); if (fServices<>nil) then (fServices as TServiceContainerServer).RecordVersionNotifyDelete( TableIndex,ID,Revision); finally deleted.Free; end; end; function TSQLRestServer.RecordVersionSynchronizeSlave(Table: TSQLRecordClass; Master: TSQLRest; ChunkRowLimit: integer; OnWrite: TOnBatchWrite): TRecordVersion; var Writer: TSQLRestBatch; IDs: TIDDynArray; status: integer; {$ifdef WITHLOG} log: ISynLog; // for Enter auto-leave to work with FPC {$endif} begin {$ifdef WITHLOG} log := fLogClass.Enter('RecordVersionSynchronizeSlave %',[Table],self); {$endif} result := -1; // error if fRecordVersionMax=0 then InternalRecordVersionMaxFromExisting(nil); repeat Writer := RecordVersionSynchronizeSlaveToBatch( Table,Master,fRecordVersionMax,ChunkRowLimit,OnWrite); if Writer=nil then exit; // error if Writer.Count=0 then begin // nothing new (e.g. reached last chunk) result := fRecordVersionMax; Writer.Free; break; end else try fAcquireExecution[execORMWrite].Safe.Lock; fRecordVersionDeleteIgnore := true; status := BatchSend(Writer,IDs); if status=HTTP_SUCCESS then begin InternalLog('RecordVersionSynchronize(%) Added=% Updated=% Deleted=% on %', [Table,Writer.AddCount,Writer.UpdateCount,Writer.DeleteCount,Master],sllDebug); if ChunkRowLimit=0 then begin result := fRecordVersionMax; break; end; end else begin InternalLog('RecordVersionSynchronize(%) BatchSend=%',[Table,status],sllError); fRecordVersionMax := 0; // force recompute the maximum from DB break; end; finally fRecordVersionDeleteIgnore := false; fAcquireExecution[execORMWrite].Safe.UnLock; Writer.Free; end; until false; end; function TSQLRestServer.RecordVersionSynchronizeSlaveToBatch(Table: TSQLRecordClass; Master: TSQLRest; var RecordVersion: TRecordVersion; MaxRowLimit: integer; OnWrite: TOnBatchWrite): TSQLRestBatch; var TableIndex,SourceTableIndex,UpdatedRow,DeletedRow: integer; Props: TSQLRecordProperties; Where: RawUTF8; UpdatedVersion,DeletedVersion: TRecordVersion; ListUpdated,ListDeleted: TSQLTableJSON; Rec: TSQLRecord; DeletedMinID: TID; Deleted: TSQLRecordTableDeleted; {$ifdef WITHLOG} log: ISynLog; // for Enter auto-leave to work with FPC {$endif} begin {$ifdef WITHLOG} log := fLogClass.Enter('RecordVersionSynchronizeSlaveToBatch %',[Table],self); {$endif} result := nil; if Master=nil then raise EORMException.CreateUTF8('%.RecordVersionSynchronizeSlaveToBatch(Master=nil)',[self]); TableIndex := Model.GetTableIndexExisting(Table); SourceTableIndex := Master.Model.GetTableIndexExisting(Table); // <>TableIndex? Props := Model.TableProps[TableIndex].Props; if Props.RecordVersionField=nil then raise EORMException.CreateUTF8( '%.RecordVersionSynchronizeSlaveToBatch(%) with no TRecordVersion field',[self,Table]); fAcquireExecution[execORMWrite].Safe.Lock; try Where := '%>? order by %'; if MaxRowLimit>0 then Where := FormatUTF8('% limit %',[Where,MaxRowLimit]); ListUpdated := Master.MultiFieldValues(Table,'*',Where, [Props.RecordVersionField.Name,Props.RecordVersionField.Name],[RecordVersion]); if ListUpdated=nil then exit; // DB error ListDeleted := nil; try DeletedMinID := Int64(SourceTableIndex) shl SQLRECORDVERSION_DELETEID_SHIFT; Where := 'ID>? and ID0 then Where := FormatUTF8('% limit %',[Where,MaxRowLimit]); ListDeleted := Master.MultiFieldValues(fSQLRecordVersionDeleteTable, 'ID,Deleted',Where,[DeletedMinID+RecordVersion, DeletedMinID+SQLRECORDVERSION_DELETEID_RANGE]); if ListDeleted=nil then exit; // DB error result := TSQLRestBatch.Create(self,nil,10000); result.OnWrite := OnWrite; if (ListUpdated.fRowCount=0) and (ListDeleted.fRowCount=0) then exit; // nothing new -> returns void TSQLRestBach with Count=0 Rec := Table.Create; Deleted := fSQLRecordVersionDeleteTable.Create; try Rec.FillPrepare(ListUpdated); Deleted.FillPrepare(ListDeleted); UpdatedRow := 1; DeletedRow := 1; UpdatedVersion := 0; DeletedVersion := 0; repeat // compute all changes in increasing version order if UpdatedVersion=0 then if UpdatedRow<=ListUpdated.fRowCount then begin Rec.FillRow(UpdatedRow); UpdatedVersion := Props.RecordVersionField.PropInfo.GetInt64Prop(Rec); inc(UpdatedRow); end; if DeletedVersion=0 then if DeletedRow<=ListDeleted.fRowCount then begin Deleted.FillRow(DeletedRow); DeletedVersion := Deleted.IDValue and pred(SQLRECORDVERSION_DELETEID_RANGE); inc(DeletedRow); end; if (UpdatedVersion=0) and (DeletedVersion=0) then break; // no more update available if (UpdatedVersion>0) and ((DeletedVersion=0) or (UpdatedVersion0 then begin result.Delete(Table,Deleted.Deleted); Deleted.IDValue := DeletedVersion+ // local ID follows current Model Int64(TableIndex) shl SQLRECORDVERSION_DELETEID_SHIFT; result.Add(Deleted,true,true,[],true); RecordVersion := DeletedVersion; DeletedVersion := 0; end; until false; finally Deleted.Free; Rec.Free; end; finally ListUpdated.Free; ListDeleted.Free; end; finally fAcquireExecution[execORMWrite].Safe.UnLock; end; end; function TSQLRestServer.ServiceContainer: TServiceContainer; begin if fServices=nil then fServices := TServiceContainerServer.Create(self); result := fServices; end; function TSQLRestServer.RecordVersionSynchronizeSubscribeMaster(Table: TSQLRecordClass; RecordVersion: TRecordVersion; const SlaveCallback: IServiceRecordVersionCallback): boolean; begin if self=nil then result := false else result := (ServiceContainer as TServiceContainerServer). RecordVersionSynchronizeSubscribeMaster(Model.GetTableIndexExisting(Table), RecordVersion,SlaveCallback); end; function TSQLRestServer.RecordVersionSynchronizeMasterStart( ByPassAuthentication: boolean): boolean; var factory: TServiceFactoryServer; begin if Services<>nil then begin factory := Services.Info(TypeInfo(IServiceRecordVersion)) as TServiceFactoryServer; if factory<>nil then begin result := factory.ByPassAuthentication=ByPassAuthentication; exit; // already registered with the same authentication parameter end; end; factory := ServiceRegister(TServiceRecordVersion,[TypeInfo(IServiceRecordVersion)],sicShared); if factory<>nil then begin if ByPassAuthentication then factory.ByPassAuthentication := ByPassAuthentication; result := true; end else result := false; end; function TSQLRestServer.RecordVersionSynchronizeSlaveStart(Table: TSQLRecordClass; MasterRemoteAccess: TSQLRestClientURI; OnNotify: TOnBatchWrite): boolean; var current,previous: TRecordVersion; tableIndex: integer; tableName: RawUTF8; service: IServiceRecordVersion; callback: IServiceRecordVersionCallback; retry: integer; {$ifdef WITHLOG} log: ISynLog; // for Enter auto-leave to work with FPC {$endif} begin {$ifdef WITHLOG} log := fLogClass.Enter('RecordVersionSynchronizeSlaveStart % over %', [Table, MasterRemoteAccess],self); {$endif} callback := nil; // weird fix for FPC/ARM result := false; if (self=nil) or (MasterRemoteAccess=nil) then exit; tableIndex := Model.GetTableIndexExisting(Table); if (fRecordVersionSlaveCallbacks<>nil) and (fRecordVersionSlaveCallbacks[tableIndex]<>nil) then begin InternalLog('RecordVersionSynchronizeSlaveStart(%): already running', [Table],sllWarning); exit; end; tableName := Model.TableProps[tableIndex].Props.SQLTableName; if MasterRemoteAccess.Services.Info(IServiceRecordVersion)=nil then if not MasterRemoteAccess.ServiceRegister([TypeInfo(IServiceRecordVersion)],sicShared) then exit; if not MasterRemoteAccess.Services.Resolve(IServiceRecordVersion,service) then exit; current := 0; retry := 0; repeat repeat // retrieve all pending versions (may retry up to 5 times) previous := current; current := RecordVersionSynchronizeSlave(Table,MasterRemoteAccess,10000,OnNotify); if current<0 then begin InternalLog('RecordVersionSynchronizeSlaveStart(%): REST failure', [Table],sllError); exit; end; until current=previous; // subscribe for any further modification if callback=nil then callback := TServiceRecordVersionCallback.Create(self,MasterRemoteAccess,Table,OnNotify); InternalLog('RecordVersionSynchronizeSlaveStart(%) current=% Subscribe(%)', [Table,current,pointer(callback)],sllDebug); if service.Subscribe(tableName,current,callback) then begin // push notifications if fRecordVersionSlaveCallbacks=nil then SetLength(fRecordVersionSlaveCallbacks,Model.TablesMax+1); fRecordVersionSlaveCallbacks[tableIndex] := callback; InternalLog('RecordVersionSynchronizeSlaveStart(%): started from revision %', [Table,current],sllDebug); result := true; exit; end; // some modifications since version (i.e. last RecordVersionSynchronizeSlave) inc(retry); until retry=5; // avoid endless loop (most of the time, not needed) InternalLog('RecordVersionSynchronizeSlaveStart(%): retry failure',[Table],sllError); end; function TSQLRestServer.RecordVersionSynchronizeSlaveStop(Table: TSQLRecordClass): boolean; var tableIndex: integer; begin result := false; if self=nil then exit; tableIndex := Model.GetTableIndexExisting(Table); if (fRecordVersionSlaveCallbacks=nil) or (fRecordVersionSlaveCallbacks[tableIndex]=nil) then begin InternalLog('RecordVersionSynchronizeSlaveStop(%): not running',[Table],sllWarning); exit; end; fRecordVersionSlaveCallbacks[tableIndex] := nil; // will notify the server result := true; end; function TSQLRestServer.UnLock(Table: TSQLRecordClass; aID: TID): boolean; begin result := Model.UnLock(Table,aID); end; procedure TSQLRestServer.Commit(SessionID: cardinal; RaiseException: boolean); var i: integer; begin inherited Commit(SessionID,RaiseException); if self<>nil then for i := 0 to high(fStaticVirtualTable) do if fStaticVirtualTable[i]<>nil then with TSQLRestStorageInMemory(fStaticVirtualTable[i]) do if InheritsFrom(TSQLRestStorageInMemory) and not CommitShouldNotUpdateFile then UpdateFile; // will do nothing if not Modified end; function TSQLRestServer.Delete(Table: TSQLRecordClass; ID: TID): boolean; begin result := inherited Delete(Table,ID); // call EngineDelete if result then // force relational database coherency (i.e. our FOREIGN KEY implementation) AfterDeleteForceCoherency(Model.GetTableIndex(Table),ID); end; function TSQLRestServer.Delete(Table: TSQLRecordClass; const SQLWhere: RawUTF8): boolean; var IDs: TIDDynArray; TableIndex,i: integer; begin result := InternalDeleteNotifyAndGetIDs(Table,SQLWhere,IDs); if (IDs=nil) or not result then exit; // nothing to delete TableIndex := Model.GetTableIndexExisting(Table); fAcquireExecution[execORMWrite].fSafe.Lock; try // may be within a batch in another thread result := EngineDeleteWhere(TableIndex,SQLWhere,IDs); finally fAcquireExecution[execORMWrite].fSafe.Unlock; end; if result then // force relational database coherency (i.e. our FOREIGN KEY implementation) for i := 0 to high(IDs) do AfterDeleteForceCoherency(TableIndex,IDs[i]); end; function TSQLRestServer.TableRowCount(Table: TSQLRecordClass): Int64; var Rest: TSQLRest; begin Rest := GetStaticTable(Table); if Rest<>nil then // faster direct call result := Rest.TableRowCount(Table) else result := inherited TableRowCount(Table); end; function TSQLRestServer.TableHasRows(Table: TSQLRecordClass): boolean; var Rest: TSQLRest; begin Rest := GetStaticTable(Table); if Rest<>nil then // faster direct call result := Rest.TableHasRows(Table) else result := inherited TableHasRows(Table); end; function TSQLRestServer.MemberExists(Table: TSQLRecordClass; ID: TID): boolean; var Rest: TSQLRest; begin Rest := GetStaticTable(Table); if Rest<>nil then // faster direct call result := Rest.MemberExists(Table,ID) else result := inherited MemberExists(Table,ID); end; function TSQLRestServer.UpdateBlobFields(Value: TSQLRecord): boolean; var Rest: TSQLRest; begin // overridden method to update all BLOB fields at once if (Value=nil) or (Value.fID<=0) then result := false else begin Rest := GetStaticTable(PSQLRecordClass(Value)^); if Rest<>nil then // faster direct call result := Rest.UpdateBlobFields(Value) else result := inherited UpdateBlobFields(Value); end; end; function TSQLRestServer.RetrieveBlobFields(Value: TSQLRecord): boolean; var Rest: TSQLRest; begin // overridden method to update all BLOB fields at once if Value=nil then result := false else begin Rest := GetStaticTable(PSQLRecordClass(Value)^); if Rest<>nil then // faster direct call result := Rest.RetrieveBlobFields(Value) else result := inherited RetrieveBlobFields(Value); end; end; function TSQLRestServer.AfterDeleteForceCoherency(aTableIndex: integer; aID: TID): boolean; procedure PerformCascade(const Where: Int64; Ref: PSQLModelRecordReference); var W: RawUTF8; cascadeOK: boolean; Rest: TSQLRest; begin // set Field=0 or delete row where Field references aID if Where=0 then exit; Int64ToUTF8(Where,W); if Ref^.CascadeDelete then cascadeOK := Delete(Model.Tables[Ref^.TableIndex], Ref^.FieldType.Name+'=:('+W+'):') else begin Rest := GetStaticTableIndex(Ref^.TableIndex); if Rest<>nil then // fast direct call cascadeOK := Rest.EngineUpdateField(Ref^.TableIndex, Ref^.FieldType.Name,'0',Ref^.FieldType.Name,W) else cascadeOK := MainEngineUpdateField(Ref^.TableIndex, Ref^.FieldType.Name,'0',Ref^.FieldType.Name,W); end; if not cascadeOK then InternalLog('AfterDeleteForceCoherency() failed to handle field %.%', [Model.Tables[Ref^.TableIndex],Ref^.FieldType.Name],sllWarning); end; var i: integer; Ref: PSQLModelRecordReference; begin Ref := @Model.fRecordReferences[0]; if Ref<>nil then begin for i := 1 to length(Model.fRecordReferences) do begin if Ref^.FieldTableIndex=-2 then // lazy initialization Ref^.FieldTableIndex := Model.GetTableIndexSafe(Ref^.FieldTable,false); case Ref^.FieldType.SQLFieldType of sftRecord: // TRecordReference published field PerformCascade(RecordReference(aTableIndex,aID),Ref); sftID: // TSQLRecord published field if Ref^.FieldTableIndex=aTableIndex then PerformCascade(aID,Ref); sftTID: // TTableID = type TID published field if Ref^.FieldTableIndex=aTableIndex then PerformCascade(aID,Ref); end; inc(Ref); end; end; result := true; // success even if no match found, or some cascade warnings end; function TSQLRestServer.CreateSQLMultiIndex(Table: TSQLRecordClass; const FieldNames: array of RawUTF8; Unique: boolean; IndexName: RawUTF8=''): boolean; var SQL: RawUTF8; i, TableIndex: integer; Props: TSQLRecordProperties; Rest: TSQLRest; begin result := false; if high(FieldNames)<0 then exit; // avoid endless loop for TSQLRestStorage with no overridden method TableIndex := Model.GetTableIndexExisting(Table); Rest := nil; if TableIndex>=0 then begin // bypass fVirtualTableDirect if cardinal(TableIndex)nil) then Rest := fStaticVirtualTable[TableIndex]; end; if Rest<>nil then begin if Rest.InheritsFrom(TSQLRestStorage) then // will try to create an index on the static table (e.g. for external DB) result := TSQLRestStorage(Rest). CreateSQLMultiIndex(Table,FieldNames,Unique,IndexName); exit; end; if (high(FieldNames)=0) and IsRowID(pointer(FieldNames[0])) then begin result := true; // SQLite3 has always its ID/RowID primary key indexed exit; end; Props := Model.TableProps[TableIndex].Props; for i := 0 to high(FieldNames) do if not IsRowID(pointer(FieldNames[i])) then if (Props.Fields.IndexByName(FieldNames[i])<0) then exit; // wrong field name if Unique then SQL := 'UNIQUE ' else SQL := ''; if IndexName='' then begin IndexName := RawUTF8ArrayToCSV(FieldNames,''); if length(IndexName)+length(Props.SQLTableName)>64 then // avoid reaching potential identifier name size limit IndexName := crc32cUTF8ToHex(Props.SQLTableName)+ crc32cUTF8ToHex(IndexName); end; SQL := FormatUTF8('CREATE %INDEX IF NOT EXISTS Index%% ON %(%);', [SQL,Props.SQLTableName,IndexName,Props.SQLTableName,RawUTF8ArrayToCSV(FieldNames,',')]); result := EngineExecute(SQL); end; function TSQLRestServer.CreateSQLIndex(Table: TSQLRecordClass; const FieldName: RawUTF8; Unique: boolean; const IndexName: RawUTF8=''): boolean; begin result := CreateSQLMultiIndex(Table,[FieldName],Unique,IndexName); end; function TSQLRestServer.CreateSQLIndex(Table: TSQLRecordClass; const FieldNames: array of RawUTF8; Unique: boolean): boolean; var i: integer; begin result := true; for i := 0 to high(FieldNames) do if not CreateSQLMultiIndex(Table,[FieldNames[i]],Unique) then result := false; end; function TSQLRestServer.GetAuthenticationSchemesCount: integer; begin result := length(fSessionAuthentication); end; function TSQLRestServer.GetCurrentSessionUserID: TID; begin with PServiceRunningContext(@ServiceContext)^ do if (Request<>nil) and (Request.Session>CONST_AUTHENTICATION_NOT_USED) then result := Request.SessionUser else result := 0; end; function TSQLRestServer.AuthenticationRegister( aMethod: TSQLRestServerAuthenticationClass): TSQLRestServerAuthentication; var i: integer; begin result := nil; if self=nil then exit; fSessions.Safe.Lock; try for i := 0 to high(fSessionAuthentication) do if fSessionAuthentication[i].ClassType=aMethod then begin result := fSessionAuthentication[i]; exit; // method already there -> return existing instance end; // create and initialize new authentication instance result := aMethod.Create(self); ObjArrayAdd(fSessionAuthentication,result); // will be owned by this array fHandleAuthentication := true; // we need both AuthUser+AuthGroup tables for authentication -> create now fSQLAuthGroupClass := Model.AddTableInherited(TSQLAuthGroup); fSQLAuthUserClass := Model.AddTableInherited(TSQLAuthUser); if fAfterCreation and (not TableHasRows(fSQLAuthUserClass) or not TableHasRows(fSQLAuthGroupClass)) then CreateMissingTables(0,fCreateMissingTablesOptions); finally fSessions.Safe.UnLock; end; end; procedure TSQLRestServer.AuthenticationRegister( const aMethods: array of TSQLRestServerAuthenticationClass); var i: integer; begin for i := 0 to high(aMethods) do AuthenticationRegister(aMethods[i]); end; procedure TSQLRestServer.AuthenticationUnregister(aMethod: TSQLRestServerAuthenticationClass); var i: integer; begin if (self=nil) or (fSessionAuthentication=nil) then exit; fSessions.Safe.Lock; try for i := 0 to high(fSessionAuthentication) do if fSessionAuthentication[i].ClassType=aMethod then begin ObjArrayDelete(fSessionAuthentication,i); fHandleAuthentication := (fSessionAuthentication<>nil); break; end; finally fSessions.Safe.UnLock; end; end; procedure TSQLRestServer.AuthenticationUnregister( const aMethods: array of TSQLRestServerAuthenticationClass); var i: integer; begin for i := 0 to high(aMethods) do AuthenticationUnregister(aMethods[i]); end; procedure TSQLRestServer.AuthenticationUnregisterAll; begin if (self=nil) or (fSessionAuthentication=nil) then exit; fSessions.Safe.Lock; try ObjArrayClear(fSessionAuthentication); fHandleAuthentication := false; finally fSessions.Safe.UnLock; end; end; function TSQLRestServer.ServiceMethodByPassAuthentication(const aMethodName: RawUTF8): integer; var i: integer; begin result := -1; if self=nil then exit; if aMethodName='' then for i := 0 to fPublishedMethods.Count-1 do fPublishedMethod[i].ByPassAuthentication := true else begin result := fPublishedMethods.FindHashed(aMethodName); if result>=0 then fPublishedMethod[result].ByPassAuthentication := true; end; end; function TSQLRestServer.BanIP(const aIP: RawUTF8; aRemoveBan: boolean): boolean; begin result := false; if fIPBan=nil then begin if aRemoveBan then exit; fIPBan := TIPBan.Create; fPrivateGarbageCollector.Add(fIPBan); end; if aRemoveBan then result := fIPBan.Delete(aIP) else result := fIPBan.Add(aIP); InternalLog('BanIP(%,%)=%',[aIP,BOOL_STR[aRemoveBan],BOOL_STR[result]]); end; function TSQLRestServer.JWTForUnauthenticatedRequestWhiteIP(const aIP: RawUTF8; aRemoveWhite: boolean): boolean; begin result := false; if fJWTForUnauthenticatedRequest=nil then exit; if fIPWhiteJWT=nil then begin if aRemoveWhite then exit; fIPWhiteJWT := TIPBan.Create; fPrivateGarbageCollector.Add(fIPWhiteJWT); end; if aRemoveWhite then result := fIPWhiteJWT.Delete(aIP) else result := fIPWhiteJWT.Add(aIP); InternalLog('WhiteIP(%,%)=%',[aIP,BOOL_STR[aRemoveWhite],BOOL_STR[result]]); end; function TSQLRestServer.GetServiceMethodStat(const aMethod: RawUTF8): TSynMonitorInputOutput; var i: Integer; begin if self=nil then i := -1 else i := fPublishedMethods.FindHashed(aMethod); if i>=0 then result := fPublishedMethod[i].Stats else result := nil; end; procedure TSQLRestServer.SetPublicURI(const Address,Port: RawUTF8); begin fPublicURI.Address := Address; fPublicURI.Port := Port; fPublicURI.Root := Model.Root; end; const // text definition registered in unit's initialization block below _TSQLRestServerURI = 'Address,Port,Root RawUTF8'; _TServicesPublishedInterfaces = 'PublicURI{Address,Port,Root RawUTF8} Names array of RawUTF8'; function TSQLRestServer.ServicesPublishedInterfaces: RawUTF8; var nfo: TServicesPublishedInterfaces; begin if (self=nil) or (Services=nil) then result := '' else begin nfo.PublicURI := fPublicURI; Services.SetInterfaceNames(nfo.Names); result := RecordSaveJSON(nfo,TypeInfo(TServicesPublishedInterfaces)); end; end; { Low-level background execution functions } type TInterfacedObjectHooked = class(TInterfacedObject) public procedure InternalRelease; end; TBackgroundLauncherAction = ( doCallMethod, doInstanceRelease, doThreadMethod); PBackgroundLauncher = ^TBackgroundLauncher; TBackgroundLauncher = record Context: PServiceRunningContext; case Action: TBackgroundLauncherAction of doCallMethod: (CallMethodArgs: pointer); // PCallMethodArgs doInstanceRelease: (Instance: TInterfacedObjectHooked); doThreadMethod: (ThreadMethod: TThreadMethod) end; procedure TInterfacedObjectHooked.InternalRelease; begin if self<>nil then IInterface(self)._Release; // call the release interface end; procedure BackgroundExecuteProc(Call: pointer); forward; {$ifdef DELPHI6OROLDER} {$ifndef LVCL} type TThreadHook = class(TThread); // Delphi 5-6 tweak to access private fields {$endif} {$endif} procedure BackGroundExecute(var synch: TBackgroundLauncher; backgroundThread: TSynBackgroundThreadMethod); var event: TThreadMethod; {$ifdef DELPHI6OROLDER} {$ifndef LVCL} tempThread: TThread; {$endif} {$endif} begin synch.Context := @ServiceContext; TMethod(event).Code := @BackgroundExecuteProc; TMethod(event).Data := @synch; if backgroundThread=nil then if GetCurrentThreadID=MainThreadID then event else {$ifdef LVCL} raise EServiceException.Create('BackGroundExecute(thread=nil)') {$else} {$ifdef DELPHI6OROLDER} if synch.Context^.RunningThread=nil then begin // circumvent Delphi 6 limitation by using a temporary TThread tempThread := TThread.Create(true); try TThreadHook(tempThread).Synchronize(event) finally tempThread.Free; // slightly slower, but working end; end else TThreadHook(synch.Context^.RunningThread).Synchronize(event) {$else} TThread.Synchronize(synch.Context^.RunningThread,event) {$endif DELPHI6OROLDER} {$endif LVCL} else backgroundThread.RunAndWait(event); end; procedure BackgroundExecuteCallMethod(args: pointer; backgroundThread: TSynBackgroundThreadMethod); var synch: TBackgroundLauncher; begin synch.Action := doCallMethod; synch.CallMethodArgs := args; BackGroundExecute(synch,backgroundThread); end; procedure BackgroundExecuteInstanceRelease(instance: TObject; backgroundThread: TSynBackgroundThreadMethod); var synch: TBackgroundLauncher; begin synch.Action := doInstanceRelease; if not instance.InheritsFrom(TInterfacedObject) then raise EServiceException.CreateUTF8('BackgroundExecuteInstanceRelease(%)',[instance]); synch.Instance := TInterfacedObjectHooked(instance); BackGroundExecute(synch,backgroundThread); end; procedure BackgroundExecuteThreadMethod(const method: TThreadMethod; backgroundThread: TSynBackgroundThreadMethod); var synch: TBackgroundLauncher; begin synch.Action := doThreadMethod; synch.ThreadMethod := method; BackGroundExecute(synch,backgroundThread); end; { TSQLRestServerURIContext } constructor TSQLRestServerURIContext.Create(aServer: TSQLRestServer; const aCall: TSQLRestURIParams); begin inherited Create; Server := aServer; Call := @aCall; Method := ToMethod(aCall.Method);; fThreadServer := @ServiceContext; fThreadServer^.Request := self; end; destructor TSQLRestServerURIContext.Destroy; begin if fThreadServer<>nil then fThreadServer^.Request := nil; inherited Destroy; end; procedure TSQLRestServerURIContext.InternalSetTableFromTableName(TableName: PUTF8Char); begin TableEngine := Server; InternalSetTableFromTableIndex(Server.Model.GetTableIndexPtr(TableName)); if TableIndex<0 then exit; Static := Server.GetStaticTableIndex(TableIndex,StaticKind); if Static<>nil then TableEngine := Static; end; procedure TSQLRestServerURIContext.InternalSetTableFromTableIndex(Index: integer); begin TableIndex := Index; if TableIndex>=0 then with Server.Model do begin self.Table := Tables[TableIndex]; self.TableRecordProps := TableProps[TableIndex]; end; end; function TSQLRestServerURIContext.URIDecodeREST: boolean; var i,j,slash: integer; Par: PUTF8Char; begin // expects 'ModelRoot[/TableName[/TableID][/URIBlobFieldName]][?param=...]' format // check root URI and Parameters i := 0; if (Call^.url<>'') and (Call^.url[1]='/') then inc(i); // URL may be '/path' j := length(Server.Model.Root); if (i+j>length(Call^.Url)) or (not(Call^.Url[i+j+1] in [#0,'/','?'])) or (StrCompIL(pointer(PtrInt(Call^.url)+i),pointer(Server.Model.Root),j,0)<>0) then begin result := False; exit; // bad ModelRoot -> caller can try another TSQLRestServer end; ParametersPos := PosExChar('?',Call^.url); if ParametersPos>0 then // '?select=...&where=...' or '?where=...' Parameters := @Call^.url[ParametersPos+1]; if Method=mPost then begin fInputPostContentType := Call^.InBodyType(false); if (Parameters=nil) and IdemPChar(pointer(fInputPostContentType),'APPLICATION/X-WWW-FORM-URLENCODED') then Parameters := pointer(Call^.InBody); end; // compute URI without any root nor parameter inc(i,j+2); if ParametersPos=0 then URI := copy(Call^.url,i,maxInt) else URI := copy(Call^.url,i,ParametersPos-i); URIAfterRoot := PUTF8Char(pointer(Call^.url))+i-1; // compute Table, TableID and URIBlobFieldName slash := PosExChar('/',URI); if slash>0 then begin URI[slash] := #0; Par := pointer(URI); InternalSetTableFromTableName(Par); inc(Par,slash); if (Table<>nil) and (Par^ in ['0'..'9']) then // "ModelRoot/TableName/TableID/URIBlobFieldName" TableID := GetNextItemInt64(Par,'/') else TableID := -1; // URI like "ModelRoot/TableName/MethodName" URIBlobFieldName := Par; if Table<>nil then begin j := PosExChar('/',URIBlobFieldName); if j>0 then begin // handle "ModelRoot/TableName/URIBlobFieldName/ID" TableID := GetCardinalDef(pointer(PtrInt(URIBlobFieldName)+j),cardinal(-1)); SetLength(URIBlobFieldName,j-1); end; end; SetLength(URI,slash-1); end else InternalSetTableFromTableName(pointer(URI)); // "ModelRoot/TableName" // compute URISessionSignaturePos and URIWithoutSignature if ParametersPos>0 then if IdemPChar(Parameters,'SESSION_SIGNATURE=') then URISessionSignaturePos := ParametersPos else URISessionSignaturePos := PosEx('&session_signature=',Call^.url,ParametersPos+1); if URISessionSignaturePos=0 then URIWithoutSignature := Call^.Url else URIWithoutSignature := Copy(Call^.Url,1,URISessionSignaturePos-1); result := True; end; procedure TSQLRestServerURIContext.URIDecodeSOAByMethod; begin if Table=nil then // check URI as 'ModelRoot/MethodName' MethodIndex := Server.fPublishedMethods.FindHashed(URI) else if URIBlobFieldName<>'' then // check URI as 'ModelRoot/TableName[/TableID]/MethodName' MethodIndex := Server.fPublishedMethods.FindHashed(URIBlobFieldName) else MethodIndex := -1; end; var // as set by TSQLRestServer.AdministrationExecute() BYPASS_ACCESS_RIGHTS: TSQLAccessRights; function TSQLRestServerURIContext.Authenticate: boolean; var aSession: TAuthSession; i: integer; begin if Server.HandleAuthentication and not IsRemoteAdministrationExecute then begin Session := CONST_AUTHENTICATION_SESSION_NOT_STARTED; result := false; Server.fSessions.Safe.Lock; try if Server.fSessionAuthentication<>nil then for i := 0 to length(Server.fSessionAuthentication)-1 do begin aSession := Server.fSessionAuthentication[i].RetrieveSession(self); if aSession<>nil then begin {$ifdef WITHLOG} if (aSession.RemoteIP<>'') and (aSession.RemoteIP<>'127.0.0.1') then Log.Log(sllUserAuth,'%/% %',[aSession.User.LogonName,aSession.ID, aSession.RemoteIP],self); {$endif} result := true; exit; end; end; finally Server.fSessions.Safe.UnLock; end; // if we reached here, no session was found if Service<>nil then // you can allow a service to be called directly result := Service.ByPassAuthentication else if MethodIndex>=0 then // /auth + /timestamp are e.g. allowed methods without signature result := Server.fPublishedMethod[MethodIndex].ByPassAuthentication else if (Table<>nil) and (Method in Server.fBypassORMAuthentication) then // allow by-pass for a set of HTTP verbs (e.g. mGET) result := true; end else begin // default unique session if authentication is not enabled Session := CONST_AUTHENTICATION_NOT_USED; result := true; end; end; procedure TSQLRestServerURIContext.AuthenticationFailed( Reason: TNotifyAuthenticationFailedReason); var txt: PShortString; begin txt := ToText(Reason); {$ifdef WITHLOG} Log.Log(sllUserAuth,'AuthenticationFailed(%) for % (session=%)', [txt^,Call^.Url,Session],self); {$endif} // 401 Unauthorized response MUST include a WWW-Authenticate header, // which is not what we used, so here we won't send 401 error code but 403 Call.OutStatus := HTTP_FORBIDDEN; FormatUTF8('Authentication Failed: % (%)', [UnCamelCase(TrimLeftLowerCaseShort(txt)),ord(Reason)],CustomErrorMsg); // call the notification event if Assigned(Server.OnAuthenticationFailed) then Server.OnAuthenticationFailed(Server,Reason,nil,self); end; destructor TSQLRestAcquireExecution.Destroy; begin inherited Destroy; Thread.Free; end; procedure TSQLRestServerURIContext.ExecuteCommand; procedure TimeOut; begin {$ifdef WITHLOG} Log.Log(sllServer,'TimeOut %.Execute(%) after % ms',[self,ToText(Command)^, Server.fAcquireExecution[Command].LockedTimeOut],self); {$endif} if Call<>nil then Call^.OutStatus := HTTP_TIMEOUT; // 408 Request Time-out end; var Method: TThreadMethod; Start64: Int64; begin with Server.fAcquireExecution[Command] do begin case Command of execSOAByMethod: Method := ExecuteSOAByMethod; execSOAByInterface: Method := ExecuteSOAByInterface; execORMGet: Method := ExecuteORMGet; execORMWrite: begin // special behavior to handle transactions at writing Method := ExecuteORMWrite; Start64 := GetTickCount64; repeat if Safe.TryLock then try if (Server.fTransactionActiveSession=0) or // avoid transaction mixups (Server.fTransactionActiveSession=Session) then begin if Mode=amLocked then begin ExecuteORMWrite; // process within the obtained write mutex exit; end; break; // will handle Mode<>amLocked below end; finally Safe.UnLock; end; if (LockedTimeOut<>0) and (GetTickCount64>Start64+LockedTimeOut) then begin TimeOut; // wait up to 5 second by default exit; end; SleepHiRes(1); // retry every 1 ms until Server.fShutdownRequested; end; else raise EORMException.CreateUTF8('Unexpected Command=% in %.Execute', [ord(Command),self]); end; if Mode=amBackgroundORMSharedThread then if (Command=execORMWrite) and (Server.fAcquireExecution[execORMGet].Mode=amBackgroundORMSharedThread) then Command := execORMGet; // for share same thread for ORM read/write end; with Server.fAcquireExecution[Command] do case Mode of amUnlocked: Method; amLocked: if LockedTimeOut=0 then begin Safe.Lock; try Method; finally Safe.UnLock; end; end else begin Start64 := GetTickCount64; repeat if Safe.TryLock then try Method; finally Safe.UnLock; end; if GetTickCount64>Start64+LockedTimeOut then break; // wait up to 2 second by default SleepHiRes(1); // retry every 1 ms until Server.fShutdownRequested; TimeOut; end; {$ifndef LVCL} amMainThread: BackgroundExecuteThreadMethod(Method,nil); {$endif} amBackgroundThread,amBackgroundORMSharedThread: begin if Thread=nil then Thread := Server.NewBackgroundThreadMethod('% % %', [self,Server.Model.Root,ToText(Command)^]); BackgroundExecuteThreadMethod(Method,Thread); end; end; end; procedure TSQLRestServerURIContext.ConfigurationRestMethod(SettingsStorage: TObject); var value: TDocVariantData; valid: boolean; config: variant; begin URIBlobFieldName := StringReplaceChars(URIBlobFieldName,'/','.'); if InputExists['value'] then begin if URIBlobFieldName='' then exit; value.InitObjectFromPath(URIBlobFieldName,Input['value']); JsonToObject(SettingsStorage,pointer(value.ToJSON),valid,nil,JSONTOOBJECT_TOLERANTOPTIONS); if not valid then begin Error('Invalid input [%] - expected %',[variant(value), ClassFieldNamesAllPropsAsText(SettingsStorage.ClassType,true)]); exit; end; end; ObjectToVariant(SettingsStorage,config,[woDontStoreDefault]); if URIBlobFieldName<>'' then config := TDocVariantData(config).GetValueByPath(URIBlobFieldName); ReturnsJson(config,HTTP_SUCCESS,true,twJsonEscape,true); end; procedure StatsAddSizeForCall(Stats: TSynMonitorInputOutput; const Call: TSQLRestURIParams); begin Stats.AddSize( // rough estimation length(Call.Url)+length(Call.Method)+length(Call.InHead)+length(Call.InBody)+12, length(Call.OutHead)+length(Call.OutBody)+16); end; procedure TSQLRestServerURIContext.StatsFromContext(Stats: TSynMonitorInputOutput; var Diff: Int64; DiffIsMicroSecs: boolean); begin StatsAddSizeForCall(Stats,Call^); if not StatusCodeIsSuccess(Call.OutStatus) then Stats.ProcessErrorNumber(Call.OutStatus); if DiffIsMicroSecs then // avoid a division Stats.FromExternalMicroSeconds(Diff) else Diff := Stats.FromExternalQueryPerformanceCounters(Diff); // converted to us end; procedure TSQLRestServerURIContext.ExecuteSOAByMethod; var timeStart,timeEnd: Int64; sessionstat: TSynMonitorInputOutput; begin with Server.fPublishedMethod[MethodIndex] do begin if mlMethods in Server.StatLevels then begin {$ifdef LINUX}QueryPerformanceMicroSeconds{$else}QueryPerformanceCounter{$endif}(timeStart); if Stats=nil then begin Server.fStats.Lock; // use this global lock try if Stats=nil then Stats := TSynMonitorInputOutput.Create(Name); finally Server.fStats.UnLock; end; end; Stats.Processing := true; end; if Parameters<>'' then Server.InternalLog('% %',[Name,Parameters],sllServiceCall); CallBack(self); if Stats<>nil then begin {$ifdef LINUX}QueryPerformanceMicroSeconds{$else}QueryPerformanceCounter{$endif}(timeEnd); dec(timeEnd,timeStart); StatsFromContext(Stats,timeEnd,false); if Server.StatUsage<>nil then Server.StatUsage.Modified(Stats,[]); if (mlSessions in Server.StatLevels) and (fSession<>nil) then begin if fSession.fMethods=nil then begin Server.fStats.Lock; try if fSession.fMethods=nil then SetLength(fSession.fMethods,length(Server.fPublishedMethod)); finally Server.fStats.UnLock; end; end; sessionstat := fSession.fMethods[MethodIndex]; if sessionstat=nil then begin Server.fStats.Lock; try sessionstat := fSession.fMethods[MethodIndex]; if sessionstat=nil then begin sessionstat := TSynMonitorInputOutput.Create(Name); fSession.fMethods[MethodIndex] := sessionstat; end; finally Server.fStats.UnLock; end; end; StatsFromContext(sessionstat,timeEnd,true); // mlSessions stats are not yet tracked per Client end; end; end; with Server.fStats do begin fSafe^.Lock; // try...finally not mandatory (slow inc(fServiceMethod); // TSQLRestServerMonitor.Changed method is void fSafe^.UnLock; end; end; procedure TSQLRestServerURIContext.ServiceResultStart(WR: TTextWriter); const JSONSTART: array[boolean] of RawUTF8 = ('{"result":[','{"result":{'); begin // InternalExecuteSOAByInterface has set ForceServiceResultAsJSONObject if ForceServiceResultAsJSONObjectWithoutResult then WR.Add('{') else WR.AddString(JSONSTART[ForceServiceResultAsJSONObject]); end; procedure TSQLRestServerURIContext.ServiceResultEnd(WR: TTextWriter; ID: TID); const JSONSEND_WITHID: array[boolean] of RawUTF8 = ('],"id":','},"id":'); JSONSEND_NOID: array[boolean] of AnsiChar = (']','}'); begin // InternalExecuteSOAByInterface has set ForceServiceResultAsJSONObject if ID=0 then WR.Add(JSONSEND_NOID[ForceServiceResultAsJSONObject]) else begin if ForceServiceResultAsJSONObjectWithoutResult then raise EServiceException.CreateUTF8( '%.ServiceResultEnd(ID=%) with ForceServiceResultAsJSONObjectWithoutResult', [self,ID]); WR.AddString(JSONSEND_WITHID[ForceServiceResultAsJSONObject]); WR.Add(ID); // only used in sicClientDriven mode end; if not ForceServiceResultAsJSONObjectWithoutResult then WR.Add('}'); end; procedure TSQLRestServerURIContext.InternalExecuteSOAByInterface; procedure ComputeResult; procedure ServiceResult(const Name,JSONValue: RawUTF8); var WR: TTextWriter; temp: TTextWriterStackBuffer; begin WR := TJSONSerializer.CreateOwnedStream(temp); try ServiceResultStart(WR); if ForceServiceResultAsJSONObject then WR.AddFieldName(Name); WR.AddString(JSONValue); ServiceResultEnd(WR,0); Returns(WR.Text); finally WR.Free; end; end; begin ForceServiceResultAsXMLObject := ForceServiceResultAsXMLObject or Service.ResultAsXMLObject; ForceServiceResultAsJSONObject := ForceServiceResultAsJSONObject or Service.ResultAsJSONObject or Service.ResultAsJSONObjectWithoutResult or ForceServiceResultAsXMLObject; // XML needs a full JSON object as input ForceServiceResultAsJSONObjectWithoutResult := ForceServiceResultAsJSONObject and (Service.InstanceCreation in SERVICE_IMPLEMENTATION_NOID) and Service.ResultAsJSONObjectWithoutResult; if ForceServiceResultAsXMLObjectNameSpace='' then ForceServiceResultAsXMLObjectNameSpace := Service.ResultAsXMLObjectNameSpace; with Server.fStats do begin fSafe^.Lock; inc(fServiceInterface); Changed; fSafe^.UnLock; end; case ServiceMethodIndex of ord(imFree): if not (Service.InstanceCreation in [sicClientDriven..sicPerThread]) then begin Error('_free_ is not compatible with %',[ToText(Service.InstanceCreation)^]); exit; end; // {"method":"_free_", "params":[], "id":1234} ord(imContract): begin // "method":"_contract_" to retrieve the implementation contract if (Call^.InBody<>'') and (Call^.InBody<>'[]') then Server.AssociatedServices.RegisterFromClientJSON(Call^.InBody); ServiceResult('contract',Service.ContractExpected); exit; // "id":0 for this method -> no instance was created end; ord(imSignature): begin // "method":"_signature_" to retrieve the implementation signature if TServiceContainerServer(Server.Services).PublishSignature then ServiceResult('signature',Service.Contract) else // "id":0 for this method -> no instance was created Error('Not allowed to publish signature'); exit; end; ord(imInstance): // "method":"_instance_" from TServiceFactoryClient.CreateFakeInstance if not (Service.InstanceCreation in [sicClientDriven]) then begin Error('_instance_ is not compatible with %',[ToText(Service.InstanceCreation)^]); exit; end else if ServiceInstanceID<>0 then begin Error('_instance_ with ServiceInstanceID=%',[ServiceInstanceID]); exit; end; else // TServiceFactoryServer.ExecuteMethod() will use ServiceMethod(Index) if ServiceMethod=nil then raise EServiceException.CreateUTF8('%.InternalExecuteSOAByInterface: '+ 'ServiceMethodIndex=% and ServiceMethod=nil',[self,ServiceMethodIndex]); end; if (Session>CONST_AUTHENTICATION_NOT_USED) and (ServiceExecution<>nil) and (SessionGroup-1 in ServiceExecution.Denied) then begin Error('Unauthorized method',HTTP_NOTALLOWED); exit; end; // if we reached here, we have to run the service method Service.ExecuteMethod(self); end; var xml: RawUTF8; m: integer; begin // expects Service, ServiceParameters, ServiceMethod(Index) to be set m := ServiceMethodIndex-SERVICE_PSEUDO_METHOD_COUNT; if m>=0 then begin if ServiceMethod=nil then ServiceMethod := @Service.fInterface.fMethods[m]; ServiceExecution := @Service.fExecution[m]; ServiceExecutionOptions := ServiceExecution.Options; with PServiceMethod(ServiceMethod)^ do begin // log from Ctxt.ServiceExecutionOptions if [smdConst,smdVar]*HasSPIParams<>[] then include(ServiceExecutionOptions,optNoLogInput); if [smdVar,smdOut,smdResult]*HasSPIParams<>[] then include(ServiceExecutionOptions,optNoLogOutput); end; {$ifdef WITHLOG} // load method call and parameter values (if worth it) if (sllServiceCall in Log.GenericFamily.Level) and (ServiceParameters<>nil) and (PWord(ServiceParameters)^<>ord('[')+ord(']') shl 8) then if optNoLogInput in ServiceExecutionOptions then Log.Log(sllServiceCall,'%{}', [PServiceMethod(ServiceMethod)^.InterfaceDotMethodName],Server) else Log.Log(sllServiceCall,'%%',[Service.InterfaceFactory.GetFullMethodName( ServiceMethodIndex),ServiceParameters],Server); {$endif} if Assigned(Service.OnMethodExecute) then if not Service.OnMethodExecute(self,PServiceMethod(ServiceMethod)^) then exit; // execution aborted by OnMethodExecute() callback event end; if Service.ResultAsXMLObjectIfAcceptOnlyXML then begin FindNameValue(Call^.InHead,'ACCEPT:',xml); if (xml='application/xml') or (xml='text/xml') then ForceServiceResultAsXMLObject := true; end; try ComputeResult; finally ServiceParameters := nil; // ensure no GPF later if points to some local data end; if ForceServiceResultAsXMLObject and (Call.OutBody<>'') and (Call.OutHead<>'') and CompareMemFixed(pointer(Call.OutHead),pointer(JSON_CONTENT_TYPE_HEADER_VAR),45) then begin delete(Call.OutHead,15,31); insert(XML_CONTENT_TYPE,Call.OutHead,15); JSONBufferToXML(pointer(Call.OutBody),XMLUTF8_HEADER, ForceServiceResultAsXMLObjectNameSpace,xml); Call.OutBody := xml; end; end; procedure TSQLRestServerURIContext.ExecuteORMGet; procedure ConvertOutBodyAsPlainJSON(const FieldsCSV: RawUTF8; Options: TJSONSerializerSQLRecordOptions); var rec: TSQLRecord; W: TJSONSerializer; bits: TSQLFieldBits; withid: boolean; begin // force plain standard JSON output for AJAX clients if (FieldsCSV='') or // handle ID single field only if ID_str is needed (IsRowID(pointer(FieldsCSV)) and not (jwoID_str in Options)) or // we won't handle min()/max() functions not TableRecordProps.Props.FieldBitsFromCSV(FieldsCSV,bits,withid) then exit; rec := Table.CreateAndFillPrepare(Call.OutBody); try W := TableRecordProps.Props.CreateJSONWriter( TRawByteStringStream.Create,true,FieldsCSV,{knownrows=}0); try include(W.fCustomOptions,twoForceJSONStandard); // force regular JSON W.SQLRecordOptions := Options; // will do the magic rec.AppendFillAsJsonValues(W); W.SetText(Call.OutBody); finally W.Stream.Free; // associated TRawByteStringStream instance W.Free; end; finally rec.Free; end; end; var SQLSelect, SQLWhere, SQLWhereCount, SQLSort, SQLDir, SQL: RawUTF8; SQLStartIndex, SQLResults, SQLTotalRowsCount: integer; NonStandardSQLSelectParameter, NonStandardSQLWhereParameter: boolean; SQLisSelect: boolean; ResultList: TSQLTableJSON; TableIndexes: TIntegerDynArray; rec: TSQLRecord; opt: TJSONSerializerSQLRecordOptions; P: PUTF8Char; i,j,L: integer; Blob: PPropInfo; begin {$ifdef KYLIX3} TableIndexes := nil; // make Kylix happy {$endif} case Method of mLOCK,mGET: begin if Table=nil then begin if (Method<>mLOCK) then begin if (Call.InBody='') and (Parameters<>nil) and (reUrlEncodedSQL in Call.RestAccessRights^.AllowRemoteExecute) then begin // GET with a SQL statement sent in URI, as sql=.... while not UrlDecodeValue(Parameters,'SQL=',SQL,@Parameters) do if Parameters=nil then break; end else // GET with a SQL statement sent as UTF-8 body (not 100% HTTP compatible) SQL := Call.InBody; if SQL<>'' then begin SQLisSelect := isSelect(pointer(SQL),@SQLSelect); if SQLisSelect or (reSQL in Call.RestAccessRights^.AllowRemoteExecute) then begin Static := nil; if SQLisSelect then begin TableIndexes := Server.Model.GetTableIndexesFromSQLSelect(SQL); if TableIndexes=nil then begin // check for SELECT without any known table if not (reSQLSelectWithoutTable in Call.RestAccessRights^.AllowRemoteExecute) then begin Call.OutStatus := HTTP_NOTALLOWED; exit; end; end else begin // check for SELECT with one (or several JOINed) tables for i := 0 to high(TableIndexes) do if not (TableIndexes[i] in Call.RestAccessRights^.GET) then begin Call.OutStatus := HTTP_NOTALLOWED; exit; end; // use the first static table (poorman's JOIN) Static := Server.InternalAdaptSQL(TableIndexes[0],SQL); end; end; if Static<>nil then begin TableEngine := Static; Call.OutBody := TableEngine.EngineList(SQL); end else Call.OutBody := Server.MainEngineList(SQL,false,nil); // security note: only first statement is run by EngineList() if Call.OutBody<>'' then begin // got JSON list '[{...}]' ? if (SQLSelect<>'') and (length(TableIndexes)=1) then begin InternalSetTableFromTableIndex(TableIndexes[0]); opt := ClientSQLRecordOptions; if opt<>[] then ConvertOutBodyAsPlainJSON(SQLSelect,opt); end; Call.OutStatus := HTTP_SUCCESS; // 200 OK if not SQLisSelect then // accurate fStats.NotifyORM(Method) below Method := TSQLURIMethod(IdemPCharArray(SQLBegin(pointer(SQL)), ['INSERT','UPDATE','DELETE'])+2); // -1+2 -> mGET=1 end; end; end; end; end else // here, Table<>nil and TableIndex in [0..MAX_SQLTABLES-1] if not (TableIndex in Call.RestAccessRights^.GET) then // check User Access Call.OutStatus := HTTP_NOTALLOWED else begin if TableID>0 then begin // GET ModelRoot/TableName/TableID[/BlobFieldName] to retrieve one member, // with or w/out locking, or a specified BLOB field content if Method=mLOCK then // Safe.Lock is to be followed by PUT -> check user if not (TableIndex in Call.RestAccessRights^.PUT) then Call.OutStatus := HTTP_NOTALLOWED else if Server.Model.Lock(TableIndex,TableID) then Method := mGET; // mark successfully locked if Method<>mLOCK then if URIBlobFieldName<>'' then begin // GET ModelRoot/TableName/TableID/BlobFieldName: retrieve BLOB field content Blob := Table.RecordProps.BlobFieldPropFromRawUTF8(URIBlobFieldName); if Blob<>nil then begin if TableEngine.EngineRetrieveBlob(TableIndex, TableID,Blob,TSQLRawBlob(Call.OutBody)) then begin Call.OutHead := GetMimeContentTypeHeader(Call.OutBody); Call.OutStatus := HTTP_SUCCESS; // 200 OK end else Call.OutStatus := HTTP_NOTFOUND; end; end else begin // GET ModelRoot/TableName/TableID: retrieve a member content, JSON encoded Call.OutBody := Server.fCache.Retrieve(TableIndex,TableID); if Call.OutBody='' then begin // get JSON object '{...}' if Static<>nil then Call.OutBody := Static.EngineRetrieve(TableIndex,TableID) else Call.OutBody := Server.MainEngineRetrieve(TableIndex,TableID); // cache if expected if Call.OutBody='' then Server.fCache.NotifyDeletion(TableIndex,TableID) else Server.fCache.Notify(TableIndex,TableID,Call.OutBody,soSelect); end; if Call.OutBody<>'' then begin // if something was found opt := ClientSQLRecordOptions; if opt<>[] then begin rec := Table.CreateFrom(Call.OutBody); // cached? -> make private try Call.OutBody := rec.GetJSONValues(true,true,soSelect,nil,opt); finally rec.Free; end; end; Call.OutStatus := HTTP_SUCCESS; end else // 200 OK Call.OutStatus := HTTP_NOTFOUND; end; end else // ModelRoot/TableName with 'select=..&where=' or YUI paging if Method<>mLOCK then begin // Safe.Lock not available here SQLSelect := 'RowID'; // if no select is specified (i.e. ModelRoot/TableName) // all IDs of this table are returned to the client SQLTotalRowsCount := 0; if Parameters<>nil then begin // '?select=...&where=...' or '?where=...' SQLStartIndex := 0; SQLResults := 0; if Parameters^<>#0 then with Server.URIPagingParameters do begin NonStandardSQLSelectParameter := Select<>PAGINGPARAMETERS_YAHOO.Select; NonStandardSQLWhereParameter := Where<>PAGINGPARAMETERS_YAHOO.Where; repeat UrlDecodeValue(Parameters,Sort,SQLSort); UrlDecodeValue(Parameters,Dir,SQLDir); UrlDecodeInteger(Parameters,StartIndex,SQLStartIndex); UrlDecodeInteger(Parameters,Results,SQLResults); UrlDecodeValue(Parameters,Select,SQLSelect); if NonStandardSQLSelectParameter and (SQLSelect='') then UrlDecodeValue(Parameters,PAGINGPARAMETERS_YAHOO.Select,SQLSelect); if NonStandardSQLWhereParameter and (SQLWhere='') then UrlDecodeValue(Parameters,PAGINGPARAMETERS_YAHOO.Where,SQLWhere); UrlDecodeValue(Parameters,Server.URIPagingParameters.Where,SQLWhere,@Parameters); until Parameters=nil; end; // let SQLite3 do the sort and the paging (will be ignored by Static) SQLWhereCount := SQLWhere; // "select count(*)" won't expect any ORDER if (SQLSort<>'') and (StrPosI('ORDER BY ',pointer(SQLWhere))=nil) then begin if SameTextU(SQLDir,'DESC') then SQLSort := SQLSort+' DESC'; // allow DESC, default is ASC SQLWhere := SQLWhere+' ORDER BY '+SQLSort; end; SQLWhere := trim(SQLWhere); if (SQLResults<>0) and (StrPosI('LIMIT ',pointer(SQLWhere))=nil) then begin if (Server.URIPagingParameters.SendTotalRowsCountFmt<>'') then begin if SQLWhere=SQLWhereCount then begin i := PosEx('ORDER BY ',UpperCase(SQLWhereCount)); if i>0 then // if ORDER BY already in the SQLWhere clause SetLength(SQLWhereCount,i-1); end; ResultList := Server.ExecuteList([Table], Server.Model.TableProps[TableIndex].SQLFromSelectWhere('Count(*)',SQLWhereCount)); if ResultList<>nil then try SQLTotalRowsCount := ResultList.GetAsInteger(1,0); finally ResultList.Free; end; end; SQLWhere := FormatUTF8('% LIMIT % OFFSET %',[SQLWhere,SQLResults,SQLStartIndex]); end; end; SQL := Server.Model.TableProps[TableIndex]. SQLFromSelectWhere(SQLSelect,trim(SQLWhere)); Call.OutBody := Server.InternalListRawUTF8(TableIndex,SQL); if Call.OutBody<>'' then begin // got JSON list '[{...}]' ? opt := ClientSQLRecordOptions; if opt<>[] then ConvertOutBodyAsPlainJSON(SQLSelect,opt); Call.OutStatus := HTTP_SUCCESS; // 200 OK if Server.URIPagingParameters.SendTotalRowsCountFmt<>'' then // insert "totalRows":% optional value to the JSON output if Server.NoAJAXJSON or (ClientKind=ckFramework) then begin P := pointer(Call.OutBody); L := length(Call.OutBody); P := NotExpandedBufferRowCountPos(P,P+L); j := 0; if P<>nil then j := P-pointer(Call.OutBody)-11 else for i := 1 to 10 do if Call.OutBody[L]='}' then begin j := L; break; end else dec(L); if j>0 then Insert(FormatUTF8(Server.URIPagingParameters.SendTotalRowsCountFmt, [SQLTotalRowsCount]),Call.OutBody,j); end else begin // expanded format -> as {"values":[...],"total":n} if SQLTotalRowsCount=0 then // avoid sending fields array Call.OutBody := '[]' else Call.OutBody := trim(Call.OutBody); Call.OutBody := '{"values":'+Call.OutBody+FormatUTF8(Server. URIPagingParameters.SendTotalRowsCountFmt,[SQLTotalRowsCount])+'}'; end; end else Call.OutStatus := HTTP_NOTFOUND; end; end; if Call.OutStatus=HTTP_SUCCESS then Server.fStats.NotifyORM(Method); end; mUNLOCK: begin // ModelRoot/TableName/TableID to unlock a member if not (TableIndex in Call.RestAccessRights^.PUT) then Call.OutStatus := HTTP_NOTALLOWED else if (Table<>nil) and (TableID>0) and Server.Model.UnLock(Table,TableID) then Call.OutStatus := HTTP_SUCCESS; // 200 OK end; mSTATE: begin // STATE method for TSQLRestClientServerInternalState // this method is called with Root (-> Table=nil -> Static=nil) // we need a specialized method in order to avoid fStats.Invalid increase Call.OutStatus := HTTP_SUCCESS; for i := 0 to high(Server.fStaticData) do if (Server.fStaticData[i]<>nil) and Server.fStaticData[i].InheritsFrom(TSQLRestStorage) then if TSQLRestStorage(Server.fStaticData[i]).RefreshedAndModified then begin inc(Server.InternalState); // force refresh break; end; end else raise EORMException.CreateUTF8('%.ExecuteORMGet(method=%)',[self,ord(Method)]); end; end; procedure TSQLRestServerURIContext.ExecuteORMWrite; procedure ComputeInBodyFields(Occasion: TSQLEvent); var Rec: TSQLRecord; bits: TSQLFieldBits; begin Rec := Table.Create; try Rec.FillFrom(pointer(Call.InBody),@bits); Rec.ComputeFieldsBeforeWrite(Server,Occasion); with TableRecordProps.Props do if Occasion=seAdd then bits := bits+ComputeBeforeAddFieldsBits else bits := bits+ComputeBeforeUpdateFieldsBits; Call.Inbody := Rec.GetJSONValues(true,Rec.IDValue<>0,bits); finally Rec.Free; end; end; var OK: boolean; Blob: PPropInfo; SQLSelect, SQLWhere, SQLSort, SQLDir: RawUTF8; begin if MethodIndex=Server.fPublishedMethodBatchIndex then begin ExecuteSOAByMethod; // run the BATCH process in execORMWrite context exit; end; if not Call.RestAccessRights^.CanExecuteORMWrite( Method,Table,TableIndex,TableID,self) then begin Call.OutStatus := HTTP_FORBIDDEN; exit; end; case Method of mPOST: // POST=ADD=INSERT if Table=nil then begin // ModelRoot with free SQL statement sent as UTF-8 (only for Admin group) // see e.g. TSQLRestClientURI.EngineExecute if reSQL in Call.RestAccessRights^.AllowRemoteExecute then if (Call.InBody<>'') and not (GotoNextNotSpace(Pointer(Call.InBody))^ in [#0,'[','{']) and Server.EngineExecute(Call.InBody) then begin Call.OutStatus := HTTP_SUCCESS; // 200 OK end else Call.OutStatus := HTTP_FORBIDDEN; end else begin // ModelRoot/TableName with possible JSON SentData: create a new member // here, Table<>nil, TableID<0 and TableIndex in [0..MAX_SQLTABLES-1] if rsoComputeFieldsBeforeWriteOnServerSide in Server.Options then ComputeInBodyFields(seAdd); TableID := TableEngine.EngineAdd(TableIndex,Call.InBody); if TableID<>0 then begin Call.OutStatus := HTTP_CREATED; // 201 Created FormatUTF8('Location: %/%',[URI,TableID],Call.OutHead); if rsoAddUpdateReturnsContent in Server.Options then begin Server.fCache.NotifyDeletion(TableIndex,TableID); Call.OutBody := TableEngine.EngineRetrieve(TableIndex,TableID); Server.fCache.Notify(TableIndex,TableID,Call.OutBody,soInsert); end else Server.fCache.Notify(TableIndex,TableID,Call.InBody,soInsert); end; end; mPUT: // PUT=UPDATE if TableID>0 then begin // PUT ModelRoot/TableName/TableID[/BlobFieldName] to update member/BLOB content if Server.RecordCanBeUpdated(Table,TableID,seUpdate,@CustomErrorMsg) then begin OK := false; if URIBlobFieldName<>'' then begin // PUT ModelRoot/TableName/TableID/BlobFieldName: update BLOB field content Blob := Table.RecordProps.BlobFieldPropFromRawUTF8(URIBlobFieldName); if Blob<>nil then OK := TableEngine.EngineUpdateBlob(TableIndex,TableID,Blob,Call.InBody); end else begin // ModelRoot/TableName/TableID with JSON SentData: update a member if rsoComputeFieldsBeforeWriteOnServerSide in Server.Options then ComputeInBodyFields(seUpdate); OK := TableEngine.EngineUpdate(TableIndex,TableID,Call.InBody); if OK then begin // flush (no CreateTime in JSON) Server.fCache.NotifyDeletion(TableIndex,TableID); if rsoAddUpdateReturnsContent in Server.Options then Call.OutBody := TableEngine.EngineRetrieve(TableIndex,TableID); end; end; if OK then Call.OutStatus := HTTP_SUCCESS; // 200 OK end else Call.OutStatus := HTTP_FORBIDDEN; end else if Parameters<>nil then begin // e.g. from TSQLRestClient.EngineUpdateField // PUT ModelRoot/TableName?setname=..&set=..&wherename=..&where=.. repeat UrlDecodeValue(Parameters,'SETNAME=',SQLSelect); UrlDecodeValue(Parameters,'SET=',SQLDir); UrlDecodeValue(Parameters,'WHERENAME=',SQLSort); UrlDecodeValue(Parameters,'WHERE=',SQLWhere,@Parameters); until Parameters=nil; if (SQLSelect<>'') and (SQLDir<>'') and (SQLSort<>'') and (SQLWhere<>'') then if TableEngine.EngineUpdateField(TableIndex,SQLSelect,SQLDir,SQLSort,SQLWhere) then begin if rsoAddUpdateReturnsContent in Server.Options then Call.OutBody := TableEngine.EngineRetrieve(TableIndex,TableID); Call.OutStatus := HTTP_SUCCESS; // 200 OK end; end; mDELETE: if TableID>0 then // ModelRoot/TableName/TableID to delete a member if not Server.RecordCanBeUpdated(Table,TableID,seDelete,@CustomErrorMsg) then Call.OutStatus := HTTP_FORBIDDEN else begin if TableEngine.EngineDelete(TableIndex,TableID) and Server.AfterDeleteForceCoherency(TableIndex,TableID) then begin Call.OutStatus := HTTP_SUCCESS; // 200 OK Server.fCache.NotifyDeletion(TableIndex,TableID); end; end else if Parameters<>nil then begin // ModelRoot/TableName?where=WhereClause to delete members repeat if UrlDecodeValue(Parameters,'WHERE=',SQLWhere,@Parameters) then begin SQLWhere := trim(SQLWhere); if SQLWhere<>'' then begin if Server.Delete(Table,SQLWhere) then Call.OutStatus := HTTP_SUCCESS; // 200 OK end; break; end; until Parameters=nil; end; mBEGIN: begin // BEGIN TRANSACTION // TSQLVirtualTableJSON/External will rely on SQLite3 module // and also TSQLRestStorageInMemory, since COMMIT/ROLLBACK have Static=nil // mBEGIN logic is just the opposite of mEND/mABORT: Safe.Lock main, then static if Server.TransactionBegin(Table,Session) then begin if (Static<>nil) and (StaticKind=sVirtualTable) then Static.TransactionBegin(Table,Session) else if (Static=nil) and (Server.fTransactionTable<>nil) then begin Static := Server.StaticVirtualTable[Server.fTransactionTable]; if Static<>nil then Static.TransactionBegin(Table,Session); end; Call.OutStatus := HTTP_SUCCESS; // 200 OK end; end; mEND: begin // END=COMMIT // this method is called with Root (-> Table=nil -> Static=nil) // mEND logic is just the opposite of mBEGIN: release static, then main if (Static<>nil) and (StaticKind=sVirtualTable) then Static.Commit(Session,false) else if (Static=nil) and (Server.fTransactionTable<>nil) then begin Static := Server.StaticVirtualTable[Server.fTransactionTable]; if Static<>nil then Static.Commit(Session,false); end; Server.Commit(Session,false); Call.OutStatus := HTTP_SUCCESS; // 200 OK end; mABORT: begin // ABORT=ROLLBACK // this method is called with Root (-> Table=nil -> Static=nil) // mABORT logic is just the opposite of mBEGIN: release static, then main if (Static<>nil) and (StaticKind=sVirtualTable) then Static.RollBack(Session) else if (Static=nil) and (Server.fTransactionTable<>nil) then begin Static := Server.StaticVirtualTable[Server.fTransactionTable]; if Static<>nil then Static.RollBack(Session); end; Server.RollBack(Session); Call.OutStatus := HTTP_SUCCESS; // 200 OK end; end; if StatusCodeIsSuccess(Call.OutStatus) then Server.fStats.NotifyORM(Method); end; procedure TSQLRestServerURIContext.FillInput(const LogInputIdent: RawUTF8); var n,max: integer; P: PUTF8Char; begin if (fInput<>nil) or (Parameters=nil) then exit; // only do it once P := Parameters; n := 0; max := 0; repeat if n>=max then begin if n>=256 then // avoid DOS - see MAX_METHOD_ARGS for TInterfacedObjectFake raise EParsingException.CreateUTF8( 'Security Policy: Accept up to 128 parameters for %.FillInput',[self]); inc(max,16); SetLength(fInput,max); end; P := UrlDecodeNextNameValue(P,fInput[n],fInput[n+1]); if P=nil then break; inc(n,2); until P^=#0; SetLength(fInput,n); {$ifdef WITHLOG} if LogInputIdent<>'' then Log.Add.Log(sllDebug,LogInputIdent,TypeInfo(TRawUTF8DynArray),fInput,self); {$endif} end; function TSQLRestServerURIContext.GetInputInt(const ParamName: RawUTF8): Int64; var err: integer; v: RawUTF8; begin GetInputByName(ParamName,'Int',v); result := GetInt64(pointer(v),err); if err<>0 then raise EParsingException.CreateUTF8('%.InputInt[%]: ''%'' is not an integer', [self,ParamName,v]); end; function TSQLRestServerURIContext.GetInputDouble(const ParamName: RawUTF8): double; var err: integer; v: RawUTF8; begin GetInputByName(ParamName,'Double',v); result := GetExtended(pointer(v),err); if err<>0 then raise EParsingException.CreateUTF8('%.InputDouble[%]: ''%'' is not a float', [self,ParamName,v]); end; function TSQLRestServerURIContext.GetInputIntOrVoid(const ParamName: RawUTF8): Int64; begin result := GetInt64(pointer(GetInputUTF8OrVoid(ParamName))); end; function TSQLRestServerURIContext.GetInputHexaOrVoid(const ParamName: RawUTF8): cardinal; var value: RawUTF8; begin value := GetInputUTF8OrVoid(ParamName); if (length(value)<>8) or not HexDisplayToCardinal(Pointer(value),result) then result := 0; end; function TSQLRestServerURIContext.GetInputDoubleOrVoid(const ParamName: RawUTF8): double; begin result := GetExtended(pointer(GetInputUTF8OrVoid(ParamName))); end; function TSQLRestServerURIContext.GetInputNameIndex(const ParamName: RawUTF8): PtrInt; begin // fInput[0]='Param1',fInput[1]='Value1',fInput[2]='Param2'... if (fInput=nil) and (Parameters<>nil) then FillInput; for result := 0 to (length(fInput)shr 1)-1 do if IdemPropNameU(ParamName,fInput[result*2]) then exit; result := -1; end; procedure TSQLRestServerURIContext.GetInputByName(const ParamName,InputName: RawUTF8; var result: RawUTF8); var i: PtrInt; begin i := GetInputNameIndex(ParamName); if i<0 then raise EParsingException.CreateUTF8('%: missing Input%[%]',[self,InputName,ParamName]); result := fInput[i*2+1]; end; function TSQLRestServerURIContext.GetInputUTF8(const ParamName: RawUTF8): RawUTF8; begin GetInputByName(ParamName,'UTF8',result); end; function TSQLRestServerURIContext.GetInputUTF8OrVoid(const ParamName: RawUTF8): RawUTF8; var i: PtrInt; begin i := GetInputNameIndex(ParamName); if i<0 then result := '' else result := fInput[i*2+1]; end; function TSQLRestServerURIContext.InputUTF8OrDefault( const ParamName, DefaultValue: RawUTF8): RawUTF8; var i: PtrInt; begin i := GetInputNameIndex(ParamName); if i<0 then result := DefaultValue else result := fInput[i*2+1]; end; function TSQLRestServerURIContext.InputUTF8OrError(const ParamName: RawUTF8; out Value: RawUTF8; const ErrorMessageForMissingParameter: string): boolean; var i: PtrInt; begin i := GetInputNameIndex(ParamName); if i<0 then begin if ErrorMessageForMissingParameter='' then Error('%: missing ''%'' parameter',[self,ParamName]) else Error('%',[ErrorMessageForMissingParameter]); result := false; end else begin Value := fInput[i*2+1]; result := true; end; end; function TSQLRestServerURIContext.InputEnum(const ParamName: RawUTF8; EnumType: PTypeInfo; out ValueEnum; DefaultEnumOrd: integer): boolean; var value: RawUTF8; int,err: Integer; begin result := false; if (EnumType=nil) or (EnumType^.Kind<>tkEnumeration) then exit; value := GetInputUTF8OrVoid(ParamName); if value<>'' then begin int := GetInteger(Pointer(value),err); if err=0 then result := true else begin int := EnumType^.EnumBaseType^.GetEnumNameValue(pointer(value),length(value)); if int>=0 then result := true else int := DefaultEnumOrd; end; end else int := DefaultEnumOrd; EnumType^.EnumBaseType^.SetEnumFromOrdinal(ValueEnum,int); end; function TSQLRestServerURIContext.GetInputString(const ParamName: RawUTF8): string; var i: PtrInt; begin i := GetInputNameIndex(ParamName); if i<0 then raise EParsingException.CreateUTF8('%: missing InputString[%]',[self,ParamName]); result := UTF8ToString(fInput[i*2+1]); end; function TSQLRestServerURIContext.GetInputStringOrVoid(const ParamName: RawUTF8): string; var i: PtrInt; begin i := GetInputNameIndex(ParamName); if i<0 then result := '' else result := UTF8ToString(fInput[i*2+1]); end; function TSQLRestServerURIContext.GetInputExists(const ParamName: RawUTF8): Boolean; begin result := GetInputNameIndex(ParamName)>=0; end; {$ifndef NOVARIANTS} function TSQLRestServerURIContext.GetInput(const ParamName: RawUTF8): variant; var v: RawUTF8; begin GetInputByName(ParamName,'',v); GetVariantFromJSON(pointer(v),false,Result); end; function TSQLRestServerURIContext.GetInputOrVoid(const ParamName: RawUTF8): variant; begin GetVariantFromJSON(pointer(GetInputUTF8OrVoid(ParamName)),false,Result); end; function TSQLRestServerURIContext.InputOrError(const ParamName: RawUTF8; out Value: variant; const ErrorMessageForMissingParameter: string): boolean; var v: RawUTF8; begin result := InputUTF8OrError(ParamName,v,ErrorMessageForMissingParameter); if result then GetVariantFromJSON(pointer(v),false,Value); end; function TSQLRestServerURIContext.GetInputAsTDocVariant(const Options: TDocVariantOptions; ServiceMethod: pointer): variant; var ndx, a: PtrInt; forcestring: boolean; v: variant; MultiPart: TMultiPartDynArray; name: RawUTF8; met: PServiceMethod absolute ServiceMethod; res: TDocVariantData absolute result; begin VarClear(result); FillInput; if fInput<>nil then begin res.Init(Options,dvObject); for ndx := 0 to (length(fInput) shr 1)-1 do begin name := fInput[ndx*2]; if met<>nil then begin a := met.ArgIndex(pointer(name),length(name),{input=}true); forcestring := (a>=0) and (vIsString in met.Args[a].ValueKindAsm); end else forcestring := false; GetVariantFromJSON(pointer(fInput[ndx*2+1]),forcestring,v,@Options); res.AddValue(name,v); end; end else if InputAsMultiPart(MultiPart) then begin res.Init(Options,dvObject); for ndx := 0 to high(MultiPart) do with MultiPart[ndx] do if ContentType=TEXT_CONTENT_TYPE then begin // append as regular "Name":"TextValue" field RawUTF8ToVariant(Content,v); res.AddValue(Name,v); end else // append binary file as an object, with Base64-encoded data res.AddValue(Name,_ObjFast(['data',BinToBase64(Content), 'filename',FileName,'contenttype',ContentType])); end; end; {$endif NOVARIANTS} function TSQLRestServerURIContext.InputAsMultiPart(var MultiPart: TMultiPartDynArray): Boolean; begin result := (Method=mPOST) and IdemPChar(pointer(fInputPostContentType),'MULTIPART/FORM-DATA') and MultiPartFormDataDecode(fInputPostContentType,Call^.InBody,MultiPart); end; function TSQLRestServerURIContext.GetInHeader(const HeaderName: RawUTF8): RawUTF8; var up: array[byte] of AnsiChar; begin if self=nil then result := '' else if fInHeaderLastName=HeaderName then result := fInHeaderLastValue else begin PWord(UpperCopy255(up,HeaderName))^ := ord(':'); FindNameValue(Call.InHead,up,result); if result<>'' then begin fInHeaderLastName := HeaderName; fInHeaderLastValue := result; end; end; end; const COOKIE_MAXCOUNT_DOSATTACK = 512; procedure TSQLRestServerURIContext.RetrieveCookies; var n: integer; P: PUTF8Char; cookie,cn,cv: RawUTF8; begin fInputCookiesRetrieved := true; FindNameValue(Call.InHead,'COOKIE:',cookie); P := pointer(cookie); n := 0; while P<>nil do begin GetNextItemTrimed(P,'=',cn); GetNextItemTrimed(P,';',cv); if (cn='') and (cv='') then break; SetLength(fInputCookies,n+1); fInputCookies[n].Name := cn; fInputCookies[n].Value := cv; inc(n); if n>COOKIE_MAXCOUNT_DOSATTACK then raise EParsingException.CreateUTF8('%.RetrieveCookies overflow: DOS?',[self]); end; end; procedure TSQLRestServerURIContext.SetInCookie(CookieName, CookieValue: RawUTF8); var i,n: integer; begin CookieName := trim(CookieName); if (self=nil) or (CookieName='') then exit; if not fInputCookiesRetrieved then RetrieveCookies; n := length(fInputCookies); for i := 0 to n-1 do if fInputCookies[i].Name=CookieName then begin // cookies are case-sensitive fInputCookies[i].Value := CookieValue; // in-place update exit; end; SetLength(fInputCookies,n+1); fInputCookies[n].Name := CookieName; fInputCookies[n].Value := CookieValue; end; function TSQLRestServerURIContext.GetInCookie(CookieName: RawUTF8): RawUTF8; var i: integer; begin result := ''; CookieName := trim(CookieName); if (self=nil) or (CookieName='') then exit; if not fInputCookiesRetrieved then RetrieveCookies; for i := 0 to length(fInputCookies)-1 do if fInputCookies[i].Name=CookieName then begin // cookies are case-sensitive result := fInputCookies[i].Value; exit; end; end; procedure TSQLRestServerURIContext.SetOutSetCookie(aOutSetCookie: RawUTF8); const HTTPONLY: array[boolean] of RawUTF8 = ('; HttpOnly',''); begin if self=nil then exit; aOutSetCookie := Trim(aOutSetCookie); if not IsValidUTF8WithoutControlChars(aOutSetCookie) then raise EBusinessLayerException.CreateUTF8('Unsafe %.SetOutSetCookie',[self]); if PosExChar('=',aOutSetCookie)<2 then raise EBusinessLayerException.CreateUTF8( '"name=value" expected for %.SetOutSetCookie("%")',[self,aOutSetCookie]); if StrPosI('; PATH=',pointer(aOutSetCookie))=nil then FormatUTF8('%; Path=/%%',[aOutSetCookie,Server.Model.Root, HTTPONLY[rsoCookieHttpOnlyFlagDisable in Server.fOptions]],fOutSetCookie) else fOutSetCookie := aOutSetCookie; end; function TSQLRestServerURIContext.GetUserAgent: RawUTF8; begin result := Call^.HeaderOnce(fUserAgent,'USER-AGENT: '); end; function TSQLRestServerURIContext.GetRemoteIP: RawUTF8; begin result := Call^.HeaderOnce(fRemoteIP,HEADER_REMOTEIP_UPPER); end; function TSQLRestServerURIContext.GetRemoteIPNotLocal: RawUTF8; begin result := Call^.HeaderOnce(fRemoteIP,HEADER_REMOTEIP_UPPER); if result='127.0.0.1' then result := ''; end; function TSQLRestServerURIContext.GetRemoteIPIsLocalHost: boolean; begin result := (GetRemoteIP='') or (fRemoteIP='127.0.0.1'); end; function TSQLRestServerURIContext.AuthenticationBearerToken: RawUTF8; begin result := Call^.HeaderOnce(fAuthenticationBearerToken,HEADER_BEARER_UPPER); if (result='') and not(rsoAuthenticationURIDisable in Server.Options) then begin result := GetInputUTF8OrVoid('authenticationbearer'); if result<>'' then fAuthenticationBearerToken := result; end; end; function TSQLRestServerURIContext.AuthenticationCheck(jwt: TJWTAbstract): boolean; begin if jwt=nil then JWTContent.result := jwtNoToken else jwt.Verify(AuthenticationBearerToken,JWTContent); result := JWTContent.result=jwtValid; if not result then Error('Invalid Bearer [%]',[ToText(JWTContent.result)^],HTTP_FORBIDDEN) else if (Server.fIPWhiteJWT<>nil) and not Server.fIPWhiteJWT.Exists(RemoteIP) and (fRemoteIP<>'') and (fRemoteIP<>'127.0.0.1') then begin Error('Invalid IP [%]',[fRemoteIP],HTTP_FORBIDDEN); result := false; end; end; function TSQLRestServerURIContext.ClientKind: TSQLRestServerURIContextClientKind; var agent: RawUTF8; begin if fClientKind=ckUnknown then if Call.InHead='' then // e.g. for WebSockets remote access fClientKind := ckAjax else begin agent := GetUserAgent; if (agent='') or (PosEx('mORMot',agent)>0) then // 'mORMot' set e.g. from XPOWEREDPROGRAM value in SynCrtSock fClientKind := ckFramework else fClientKind := ckAjax; end; result := fClientKind; end; function TSQLRestServerURIContext.IsRemoteAdministrationExecute: boolean; begin result := (self<>nil) and (call.RestAccessRights=@BYPASS_ACCESS_RIGHTS); end; function TSQLRestServerURIContext.ClientSQLRecordOptions: TJSONSerializerSQLRecordOptions; begin result := []; if (TableRecordProps=nil) or (ClientKind<>ckAjax) then exit; if rsoGetID_str in Server.Options then include(result,jwoID_str); if ([sftObject,sftBlobDynArray{$ifndef NOVARIANTS},sftVariant{$endif}]* TableRecordProps.Props.HasTypeFields<>[]) and (rsoGetAsJsonNotAsString in Server.Options) then include(result,jwoAsJsonNotAsString); end; function TSQLRestServerURIContext.GetResourceFileName: TFileName; begin if (URIBlobFieldName='') or (PosEx('..',URIBlobFieldName)>0) then result := '' else // for security, disallow .. in the supplied file path result := UTF8ToString(StringReplaceAll(URIBlobFieldName,'/',PathDelim)); end; procedure TSQLRestServerURIContext.Returns(const Result: RawUTF8; Status: integer; const CustomHeader: RawUTF8; Handle304NotModified,HandleErrorAsRegularResult: boolean; CacheControlMaxAge: integer; ServerHash: RawUTF8); var clientHash: RawUTF8; begin if HandleErrorAsRegularResult or StatusCodeIsSuccess(Status) then begin Call.OutStatus := Status; Call.OutBody := Result; if CustomHeader<>'' then Call.OutHead := CustomHeader else if Call.OutHead='' then Call.OutHead := JSON_CONTENT_TYPE_HEADER_VAR; if CacheControlMaxAge>0 then Call.OutHead := Call.OutHead+#13#10'Cache-Control: max-age='+UInt32ToUtf8(CacheControlMaxAge); if Handle304NotModified and (Status=HTTP_SUCCESS) and (Length(Result)>64) then begin FindNameValue(Call.InHead,'IF-NONE-MATCH: ',clientHash); if ServerHash='' then ServerHash := '"'+crc32cUTF8ToHex(Result)+'"'; ServerHash := '"'+ServerHash+'"'; if clientHash<>ServerHash then Call.OutHead := Call.OutHead+#13#10'ETag: '+ServerHash else begin Call.OutBody := ''; // save bandwidth for "304 Not Modified" Call.OutStatus := HTTP_NOTMODIFIED; end; end; end else Error(Result,Status); end; procedure TSQLRestServerURIContext.Returns(Value: TObject; Status: integer; Handle304NotModified: boolean; SQLRecordOptions: TJSONSerializerSQLRecordOptions; const CustomHeader: RawUTF8); var json: RawUTF8; begin if Value.InheritsFrom(TSQLRecord) then json := TSQLRecord(Value).GetJSONValues(true,true,soSelect,nil,SQLRecordOptions) else json := ObjectToJSON(Value); Returns(json,Status,CustomHeader,Handle304NotModified); end; procedure TSQLRestServerURIContext.ReturnsJson(const Value: Variant; Status: integer; Handle304NotModified: boolean; Escape: TTextWriterKind; MakeHumanReadable: boolean; const CustomHeader: RawUTF8); var json: RawUTF8; tmp: TSynTempBuffer; begin VariantSaveJSON(Value,Escape,json); if MakeHumanReadable and (json<>'') and (json[1] in ['{','[']) then begin tmp.Init(json); try JSONBufferReformat(tmp.buf,json); finally tmp.Done; end; end; Returns(json,Status,CustomHeader,Handle304NotModified); end; procedure TSQLRestServerURIContext.ReturnBlob(const Blob: RawByteString; Status: integer; Handle304NotModified: boolean; const FileName: TFileName; CacheControlMaxAge: integer); begin if not ExistsIniName(pointer(Call.OutHead),HEADER_CONTENT_TYPE_UPPER) then AddToCSV(GetMimeContentTypeHeader(Blob,FileName),Call.OutHead,#13#10); Returns(Blob,Status,Call.OutHead,Handle304NotModified,false,CacheControlMaxAge); end; procedure TSQLRestServerURIContext.ReturnFile(const FileName: TFileName; Handle304NotModified: boolean; const ContentType,AttachmentFileName, Error404Redirect: RawUTF8; CacheControlMaxAge: integer); var FileTime: TDateTime; clientHash, serverHash: RawUTF8; begin if FileName='' then FileTime := 0 else FileTime := FileAgeToDateTime(FileName); if FileTime=0 then if Error404Redirect<>'' then Redirect(Error404Redirect) else Error('',HTTP_NOTFOUND,CacheControlMaxAge) else begin if not ExistsIniName(pointer(Call.OutHead),HEADER_CONTENT_TYPE_UPPER) then begin if Call.OutHead<>'' then Call.OutHead := Call.OutHead+#13#10; if ContentType<>'' then Call.OutHead := Call.OutHead+HEADER_CONTENT_TYPE+ContentType else Call.OutHead := Call.OutHead+GetMimeContentTypeHeader('',FileName); end; if CacheControlMaxAge>0 then Call.OutHead := Call.OutHead+#13#10'Cache-Control: max-age='+UInt32ToUtf8(CacheControlMaxAge); Call.OutStatus := HTTP_SUCCESS; if Handle304NotModified then begin FindNameValue(Call.InHead,'IF-NONE-MATCH:',clientHash); serverHash := '"'+DateTimeToIso8601(FileTime,false,'T',true)+'"'; Call.OutHead := Call.OutHead+#13#10'ETag: '+serverHash; if clientHash=serverHash then begin Call.OutStatus := HTTP_NOTMODIFIED; exit; end; end; // Content-Type: appears twice: 1st to notify static file, 2nd for mime type Call.OutHead := STATICFILE_CONTENT_TYPE_HEADER+#13#10+Call.OutHead; StringToUTF8(FileName,Call.OutBody); // body=filename for STATICFILE_CONTENT if AttachmentFileName<>'' then Call.OutHead := Call.OutHead+ #13#10'Content-Disposition: attachment; filename="'+AttachmentFileName+'"'; end; end; procedure TSQLRestServerURIContext.ReturnFileFromFolder(const FolderName: TFileName; Handle304NotModified: boolean; const DefaultFileName: TFileName; const Error404Redirect: RawUTF8; CacheControlMaxAge: integer); var fileName: TFileName; begin if URIBlobFieldName='' then fileName := DefaultFileName else if PosEx('..',URIBlobFieldName)>0 then fileName := '' else fileName := UTF8ToString(StringReplaceChars(URIBlobFieldName,'/',PathDelim)); if fileName<>'' then fileName := IncludeTrailingPathDelimiter(FolderName)+fileName; ReturnFile(fileName,Handle304NotModified,'','',Error404Redirect,CacheControlMaxAge); end; procedure TSQLRestServerURIContext.Redirect(const NewLocation: RawUTF8; PermanentChange: boolean); begin if PermanentChange then Call.OutStatus := HTTP_MOVEDPERMANENTLY else Call.OutStatus := HTTP_TEMPORARYREDIRECT; Call.OutHead := 'Location: '+NewLocation; end; procedure TSQLRestServerURIContext.Returns(const NameValuePairs: array of const; Status: integer; Handle304NotModified,HandleErrorAsRegularResult: boolean; const CustomHeader: RawUTF8); begin Returns(JSONEncode(NameValuePairs),Status,CustomHeader,Handle304NotModified, HandleErrorAsRegularResult); end; procedure TSQLRestServerURIContext.Results(const Values: array of const; Status: integer; Handle304NotModified: boolean; CacheControlMaxAge: integer); var i,h: integer; result: RawUTF8; temp: TTextWriterStackBuffer; begin h := high(Values); if h<0 then result := '{"result":null}' else with TJSONSerializer.CreateOwnedStream(temp) do try AddShort('{"result":'); if h=0 then // result is one value AddJSONEscape(Values[0]) else begin // result is one array of values Add('['); i := 0; repeat AddJSONEscape(Values[i]); if i=h then break; Add(','); inc(i); until false; Add(']'); end; Add('}'); SetText(result); finally Free; end; Returns(result,Status,'',Handle304NotModified,false,CacheControlMaxAge); end; procedure TSQLRestServerURIContext.Success(Status: integer); begin if StatusCodeIsSuccess(Status) then Call.OutStatus := Status else Error('',Status); end; procedure TSQLRestServerURIContext.Error(const Format: RawUTF8; const Args: array of const; Status, CacheControlMaxAge: integer); var msg: RawUTF8; begin FormatUTF8(Format,Args,msg); Error(msg,Status,CacheControlMaxAge); end; procedure TSQLRestServerURIContext.Error(E: Exception; const Format: RawUTF8; const Args: array of const; Status: integer); var msg,exc: RawUTF8; begin FormatUTF8(Format,Args,msg); if E=nil then Error(msg,Status) else begin exc := ObjectToJSONDebug(E); if msg='' then Error('{"%":%}',[E,exc],Status) else Error(FormatUTF8('{"msg":?,"%":%}',[E,exc],[msg],true),Status); end; end; procedure TSQLRestServerURIContext.Error(const ErrorMessage: RawUTF8; Status, CacheControlMaxAge: integer); var ErrorMsg: RawUTF8; temp: TTextWriterStackBuffer; begin Call.OutStatus := Status; if StatusCodeIsSuccess(Status) then begin // not an error Call.OutBody := ErrorMessage; if CacheControlMaxAge<>0 then // Cache-Control is ignored for errors Call.OutHead := 'Cache-Control: max-age='+UInt32ToUtf8(CacheControlMaxAge); exit; end; if ErrorMessage='' then StatusCodeToErrorMessage(Status,ErrorMsg) else ErrorMsg := ErrorMessage; with TTextWriter.CreateOwnedStream(temp) do try AddShort('{'#13#10'"errorCode":'); Add(call.OutStatus); if (ErrorMsg<>'') and (ErrorMsg[1]='{') and (ErrorMsg[length(ErrorMsg)]='}') then begin AddShort(','#13#10'"error":'#13#10); AddNoJSONEscape(pointer(ErrorMsg),length(ErrorMsg)); AddShort(#13#10'}'); end else begin AddShort(','#13#10'"errorText":"'); AddJSONEscape(pointer(ErrorMsg)); AddShort('"'#13#10'}'); end; SetText(Call.OutBody); finally Free; end; Server.InternalLog('%.Error: %',[ClassType,Call.OutBody],sllDebug); end; { TSQLRestRoutingREST } procedure TSQLRestRoutingREST.URIDecodeSOAByInterface; var i: integer; method,clientdrivenid: RawUTF8; begin if (Table=nil) and (MethodIndex<0) and (URI<>'') and (Server.Services<>nil) then begin // check URI as '/Model/Interface.Method[/ClientDrivenID]' i := Server.Services.fInterfaceMethods.FindHashed(URI); if i>=0 then // no specific message: it may be a valid request with Server.Services.fInterfaceMethod[i] do begin Service := TServiceFactoryServer(InterfaceService); ServiceMethodIndex := InterfaceMethodIndex; fServiceListInterfaceMethodIndex := i; i := ServiceMethodIndex-SERVICE_PSEUDO_METHOD_COUNT; if i>=0 then ServiceMethod := @Service.fInterface.fMethods[i]; ServiceInstanceID := GetInteger(pointer(URIBlobFieldName)); end else if URIBlobFieldName<>'' then begin // check URI as '/Model/Interface/Method[/ClientDrivenID]'' i := Server.Services.fInterfaces.FindHashed(URI); if i>=0 then begin // identified as a valid JSON-RPC service Service := TServiceFactoryServer(Server.Services.fInterface[i].Service); Split(URIBlobFieldName,'/',method,clientdrivenid); ServiceMethodIndex := Service.InterfaceFactory.FindMethodIndex(method); if ServiceMethodIndex<0 then Service := nil else begin ServiceMethod := @Service.fInterface.fMethods[ServiceMethodIndex]; inc(ServiceMethodIndex,SERVICE_PSEUDO_METHOD_COUNT); fServiceListInterfaceMethodIndex := -1; ServiceInstanceID := GetInteger(pointer(clientdrivenid)); end; end; end; end; end; procedure TSQLRestRoutingREST.ExecuteSOAByInterface; var JSON: RawUTF8; procedure DecodeUriParametersIntoJSON(const input: TRawUTF8DynArray); var a,i,iLow: Integer; WR: TTextWriter; argDone: boolean; temp: TTextWriterStackBuffer; begin WR := TJSONSerializer.CreateOwnedStream(temp); try // convert URI parameters into the expected ordered JSON array WR.Add('['); with PServiceMethod(ServiceMethod)^ do begin iLow := 0; for a := ArgsInFirst to ArgsInLast do with Args[a] do if ValueDirection<>smdOut then begin argDone := false; for i := iLow to high(input) shr 1 do // search argument in URI if IdemPropNameU(input[i*2],@ParamName^[1],ord(ParamName^[0])) then begin AddValueJSON(WR,input[i*2+1]); // will add "" if needed if i=iLow then inc(iLow); // optimistic in-order search, but allow any order argDone := true; break; end; if not argDone then AddDefaultJSON(WR); // allow missing argument (and add ',') end; end; WR.CancelLastComma; WR.Add(']'); WR.SetText(JSON); finally WR.Free; end; end; var Par: PUTF8Char; begin // here Ctxt.Service and ServiceMethod(Index) are set if (Server.Services=nil) or (Service=nil) then raise EServiceException.CreateUTF8('%.ExecuteSOAByInterface invalid call',[self]); // URI as '/Model/Interface.Method[/ClientDrivenID]' if Call.InBody<>'' then // parameters sent as JSON array/object (the Delphi/AJAX way) or single blob if (ServiceMethod<>nil) and PServiceMethod(ServiceMethod)^.ArgsInputIsOctetStream and not Call.InBodyTypeIsJson then begin JSON := BinToBase64(Call.InBody,'["','"]',false); ServiceParameters := pointer(JSON); // as expected by InternalExecuteSOAByInterface end else ServiceParameters := pointer(Call.InBody) else begin // no body -> try URI-encoded parameters (the HTML way) Par := Parameters; if Par<>nil then begin while Par^='+' do inc(Par); // ignore trailing spaces if (Par^='[') or IdemPChar(Par,'%5B') then // as JSON array (input is e.g. '+%5B...' for ' [...') JSON := UrlDecode(Parameters) else begin // or as a list of parameters (input is 'Param1=Value1&Param2=Value2...') FillInput; // fInput[0]='Param1',fInput[1]='Value1',fInput[2]='Param2'... if (fInput<>nil) and (ServiceMethod<>nil) then DecodeUriParametersIntoJSON(fInput); end; end; ServiceParameters := pointer(JSON); end; // now Service, ServiceParameters, ServiceMethod(Index) are set InternalExecuteSOAByInterface; end; class procedure TSQLRestRoutingREST.ClientSideInvoke(var uri: RawUTF8; ctxt: TSQLRestServerURIContextClientInvoke; const method, params, clientDrivenID: RawUTF8; out sent,head: RawUTF8); begin if clientDrivenID<>'' then uri := uri+'.'+method+'/'+clientDrivenID else uri := uri+'.'+method; if (csiAsOctetStream in ctxt) and (length(params)>2) and (params[1]='"') then begin sent := Base64ToBin(@params[2],length(params)-2); if sent<>'' then begin head := BINARY_CONTENT_TYPE_HEADER; exit; end; end; sent := '['+params+']'; // we may also encode them within the URI end; { TSQLRestRoutingJSON_RPC } procedure TSQLRestRoutingJSON_RPC.URIDecodeSOAByInterface; var i: integer; begin if (Table=nil) and (MethodIndex<0) and (URI<>'') and (Server.Services<>nil) then begin // URI as '/Model/Interface' i := Server.Services.fInterfaces.FindHashed(URI); if i>=0 then // identified as a valid JSON-RPC service Service := TServiceFactoryServer(Server.Services.fInterface[i].Service); end; // ServiceMethodIndex will be retrieved from "method": in body end; procedure TSQLRestRoutingJSON_RPC.ExecuteSOAByInterface; var method: RawUTF8; Values: array[0..2] of TValuePUTF8Char; internal: TServiceInternalMethod; tmp: TSynTempBuffer; begin // here Ctxt.Service is set (not ServiceMethodIndex yet) if (Server.Services=nil) or (Service=nil) then raise EServiceException.CreateUTF8('%.ExecuteSOAByInterface invalid call',[self]); tmp.Init(call.Inbody); try JSONDecode(tmp.buf,['method','params','id'],@Values,true); if Values[0].Value=nil then // Method name required exit; Values[0].ToUTF8(method); ServiceParameters := Values[1].Value; ServiceInstanceID := Values[2].ToCardinal; // retrieve "id":ClientDrivenID ServiceMethodIndex := Service.fInterface.FindMethodIndex(method); if ServiceMethodIndex>=0 then inc(ServiceMethodIndex,SERVICE_PSEUDO_METHOD_COUNT) else begin for internal := low(TServiceInternalMethod) to high(TServiceInternalMethod) do if IdemPropNameU(method,SERVICE_PSEUDO_METHOD[internal]) then begin ServiceMethodIndex := ord(internal); break; end; if ServiceMethodIndex<0 then begin Error('Unknown method'); exit; end; end; // now Service, ServiceParameters, ServiceMethod(Index) are set InternalExecuteSOAByInterface; ServiceParameters := nil; finally tmp.Done; // release temp storage for Values[] = Service* fields end; end; class procedure TSQLRestRoutingJSON_RPC.ClientSideInvoke(var uri: RawUTF8; ctxt: TSQLRestServerURIContextClientInvoke; const method, params, clientDrivenID: RawUTF8; out sent,head: RawUTF8); begin sent := '{"method":"'+method+'","params":['+params; if clientDrivenID='' then sent := sent+']}' else sent := sent+'],"id":'+clientDrivenID+'}'; end; function TSQLRestServer.ServiceRegister( aImplementationClass: TInterfacedClass; const aInterfaces: array of PTypeInfo; aInstanceCreation: TServiceInstanceImplementation; const aContractExpected: RawUTF8): TServiceFactoryServer; begin if (aImplementationClass=nil) or (high(aInterfaces)<0) then result := nil else result := (ServiceContainer as TServiceContainerServer). AddImplementation(aImplementationClass,aInterfaces,aInstanceCreation,nil,aContractExpected); end; function TSQLRestServer.ServiceRegister(aSharedImplementation: TInterfacedObject; const aInterfaces: array of PTypeInfo; const aContractExpected: RawUTF8): TServiceFactoryServer; begin if (self=nil) or (aSharedImplementation=nil) or (high(aInterfaces)<0) then result := nil else result := (ServiceContainer as TServiceContainerServer). AddImplementation(TInterfacedClass(aSharedImplementation.ClassType), aInterfaces,sicShared,aSharedImplementation,aContractExpected); end; function TSQLRestServer.ServiceRegister(aClient: TSQLRest; const aInterfaces: array of PTypeInfo; aInstanceCreation: TServiceInstanceImplementation; const aContractExpected: RawUTF8): boolean; begin result := False; if (self=nil) or (high(aInterfaces)<0) or (aClient=nil) then exit; result := (ServiceContainer as TServiceContainerServer).AddInterface( aInterfaces,aInstanceCreation,aContractExpected); end; function TSQLRestServer.ServiceDefine(aImplementationClass: TInterfacedClass; const aInterfaces: array of TGUID; aInstanceCreation: TServiceInstanceImplementation; const aContractExpected: RawUTF8): TServiceFactoryServer; var ti: PTypeInfoDynArray; begin ti := TInterfaceFactory.GUID2TypeInfo(aInterfaces); result := ServiceRegister(aImplementationClass,ti,aInstanceCreation,aContractExpected); end; function TSQLRestServer.ServiceDefine(aSharedImplementation: TInterfacedObject; const aInterfaces: array of TGUID; const aContractExpected: RawUTF8): TServiceFactoryServer; var ti: PTypeInfoDynArray; begin ti := TInterfaceFactory.GUID2TypeInfo(aInterfaces); result := ServiceRegister(aSharedImplementation,ti,aContractExpected); end; function TSQLRestServer.ServiceDefine(aClient: TSQLRest; const aInterfaces: array of TGUID; aInstanceCreation: TServiceInstanceImplementation; const aContractExpected: RawUTF8): boolean; var ti: PTypeInfoDynArray; begin ti := TInterfaceFactory.GUID2TypeInfo(aInterfaces); result := ServiceRegister(aClient,ti,aInstanceCreation,aContractExpected); end; procedure TSQLRestServer.URI(var Call: TSQLRestURIParams); const COMMANDTEXT: array[TSQLRestServerURIContextCommand] of string[9] = ('?','Method','Interface','Read','Write'); var Ctxt: TSQLRestServerURIContext; timeStart,timeEnd: Int64; elapsed, len: cardinal; outcomingfile: boolean; safeid: integer; P: PUTF8Char; {$ifdef WITHLOG} log: ISynLog; // for Enter auto-leave to work with FPC {$endif} begin {$ifdef WITHLOG} log := fLogClass.Enter('URI % % in=%',[Call.Method,Call.Url,KB(Call.InBody)],self); {$endif} {$ifdef LINUX}QueryPerformanceMicroSeconds{$else}QueryPerformanceCounter{$endif}(timeStart); fStats.AddCurrentRequestCount(1); if Assigned(fOnDecryptBody) then fOnDecryptBody(self,Call.InBody,Call.InHead,Call.Url); Call.OutStatus := HTTP_BADREQUEST; // default error code is 400 BAD REQUEST safeid := 0; if (fSafeProtocol<>nil) and IdemPropNameU(Call.Method,'POST') then begin P := pointer(Call.Url); if P^='/' then inc(P); // may be /modelroot/_safe_ if IdemPChar(P,pointer(fSafeRootUpper)) then begin // 'ROOT/_SAFE_/' InternalSafeProtocol(Call,safeid); if safeid=0 then exit; // low-level error end; end; Ctxt := ServicesRouting.Create(self,Call); try {$ifdef WITHLOG} if log<>nil then Ctxt.Log := log.Instance; {$endif} Ctxt.SafeProtocolID := safeID; if fShutdownRequested then Ctxt.Error('Server is shutting down',HTTP_UNAVAILABLE) else if Ctxt.Method=mNone then Ctxt.Error('Unknown Verb %',[Call.Method]) else if (fIPBan<>nil) and fIPBan.Exists(Ctxt.RemoteIP) then Ctxt.Error('Banned IP %',[Ctxt.fRemoteIP]) else // 1. decode URI if not Ctxt.URIDecodeREST then Ctxt.Error('Invalid Root',HTTP_NOTFOUND) else if (RootRedirectGet<>'') and (Ctxt.Method=mGet) and (Call.Url=Model.Root) and (Call.InBody='') then Ctxt.Redirect(RootRedirectGet) else begin Ctxt.URIDecodeSOAByMethod; if (Ctxt.MethodIndex<0) and (Ctxt.URI<>'') then Ctxt.URIDecodeSOAByInterface; // 2. handle security if (rsoSecureConnectionRequired in fOptions) and (Ctxt.MethodIndex<>fPublishedMethodTimestampIndex) and not (llfSecured in Call.LowLevelFlags) then Ctxt.AuthenticationFailed(afSecureConnectionRequired) else if not Ctxt.Authenticate then Ctxt.AuthenticationFailed(afInvalidSignature) else if (Ctxt.Service<>nil) and not (reService in Call.RestAccessRights^.AllowRemoteExecute) then if (rsoRedirectForbiddenToAuth in Options) and (Ctxt.ClientKind=ckAjax) then Ctxt.Redirect(Model.Root+'/auth') else Ctxt.AuthenticationFailed(afRemoteServiceExecutionNotAllowed) else if (Ctxt.Session<>CONST_AUTHENTICATION_NOT_USED) or (fJWTForUnauthenticatedRequest=nil) or (Ctxt.MethodIndex=fPublishedMethodTimestampIndex) or ((llfSecured in Call.LowLevelFlags) and not (llfHttps in Call.LowLevelFlags)) or // HTTPS does not authenticate Ctxt.AuthenticationCheck(fJWTForUnauthenticatedRequest) then // 3. call appropriate ORM / SOA commands in fAcquireExecution[] context try if Ctxt.MethodIndex>=0 then if Ctxt.MethodIndex=fPublishedMethodBatchIndex then Ctxt.Command := execORMWrite else Ctxt.Command := execSOAByMethod else if Ctxt.Service<>nil then Ctxt.Command := execSOAByInterface else if Ctxt.Method in [mLOCK,mGET,mUNLOCK,mSTATE] then // handle read methods Ctxt.Command := execORMGet else // write methods (mPOST, mPUT, mDELETE...) Ctxt.Command := execORMWrite; if not Assigned(OnBeforeURI) or OnBeforeURI(Ctxt) then Ctxt.ExecuteCommand; except on E: Exception do if not Assigned(OnErrorURI) or OnErrorURI(Ctxt,E) then if E.ClassType=EInterfaceFactoryException then Ctxt.Error(E,'',[],HTTP_NOTACCEPTABLE) else Ctxt.Error(E,'',[],HTTP_SERVERERROR); end; end; // 4. return expected result to the client and update Server statistics if StatusCodeIsSuccess(Call.OutStatus) then begin outcomingfile := false; if Call.OutBody<>'' then begin len := length(Call.OutHead); outcomingfile := (len>=25) and (Call.OutHead[15]='!') and IdemPChar(pointer(Call.OutHead),STATICFILE_CONTENT_TYPE_HEADER_UPPPER); end else // Call.OutBody='' if (Call.OutStatus=HTTP_SUCCESS) and (rsoHttp200WithNoBodyReturns204 in fOptions) then Call.OutStatus := HTTP_NOCONTENT; fStats.ProcessSuccess(outcomingfile); end else begin fStats.ProcessErrorNumber(Call.OutStatus); if Call.OutBody='' then // if no custom error message, compute it now as JSON Ctxt.Error(Ctxt.CustomErrorMsg,Call.OutStatus); end; StatsAddSizeForCall(fStats,Call); if (rsoNoInternalState in fOptions) and (Ctxt.Method<>mSTATE) then Call.OutInternalState := 0 // reduce headers verbosity else if (Ctxt.Static<>nil) and Ctxt.Static.InheritsFrom(TSQLRestStorage) and TSQLRestStorage(Ctxt.Static).fOutInternalStateForcedRefresh then // force always refresh for Static table which demands it Call.OutInternalState := cardinal(-1) else // database state may have changed above Call.OutInternalState := InternalState; if Ctxt.OutSetCookie<>'' then begin Call.OutHead := Trim(Call.OutHead+#13#10'Set-Cookie: '+Ctxt.OutSetCookie); if rsoCookieIncludeRootPath in fOptions then Call.OutHead := Call.OutHead+'; Path=/'; // case-sensitive Path=/ModelRoot end; if not (rsoHttpHeaderCheckDisable in fOptions) and IsInvalidHttpHeader(pointer(Call.OutHead), length(Call.OutHead)) then Ctxt.Error('Unsafe HTTP header rejected [%]', [EscapeToShort(Call.OutHead)], HTTP_SERVERERROR); finally {$ifdef LINUX}QueryPerformanceMicroSeconds{$else}QueryPerformanceCounter{$endif}(timeEnd); Ctxt.MicroSecondsElapsed := fStats.FromExternalQueryPerformanceCounters(timeEnd-timeStart); {$ifdef WITHLOG} InternalLog('% % % %/% %=% out=% in %', [Ctxt.SessionUserName,Ctxt.RemoteIPNotLocal,Call.Method,Model.Root,Ctxt.URI, COMMANDTEXT[Ctxt.Command],Call.OutStatus,KB(Call.OutBody), MicroSecToString(Ctxt.MicroSecondsElapsed)],sllServer); if (Call.OutBody<>'') and (sllServiceReturn in fLogFamily.Level) then if not(optNoLogOutput in Ctxt.ServiceExecutionOptions) then if IsHTMLContentTypeTextual(pointer(Call.OutHead)) then fLogFamily.SynLog.Log(sllServiceReturn,Call.OutBody,self,MAX_SIZE_RESPONSE_LOG); {$endif} if mlTables in StatLevels then case Ctxt.Command of execORMGet: fStats.NotifyORMTable(Ctxt.TableIndex,length(Call.OutBody),false,Ctxt.MicroSecondsElapsed); execORMWrite: fStats.NotifyORMTable(Ctxt.TableIndex,length(Call.InBody),true,Ctxt.MicroSecondsElapsed); end; fStats.AddCurrentRequestCount(-1); if fStatUsage<>nil then fStatUsage.Modified(fStats,[]); if Assigned(OnAfterURI) then try OnAfterURI(Ctxt); except end; if Assigned(fOnEncryptBody) then fOnEncryptBody(self,Call.OutBody,Call.OutHead,Call.Url); Ctxt.Free; end; if safeid<>0 then InternalSafeProtocol(Call,safeid); // encrypt Call.OutBody+OutHead if Assigned(OnIdle) then begin elapsed := GetTickCount64 shr 7; // trigger every 128 ms if elapsed<>fOnIdleLastTix then begin OnIdle(self); fOnIdleLastTix := elapsed; end; end; end; function TSQLRestServer.StatsAsJson(Flags: TSQLRestServerAddStats): RawUTF8; var W: TTextWriter; temp: TTextWriterStackBuffer; begin // emulates root/stat?withall=1 method call W := TJSONSerializer.CreateOwnedStream(temp); try AddStat(Flags,W); W.SetText(result); finally W.Free; end; end; function TSQLRestServer.StatsAsDocVariant(Flags: TSQLRestServerAddStats): variant; begin _Json(StatsAsJson(Flags),result,JSON_OPTIONS_FAST); end; {$ifndef NOVARIANTS} procedure TSQLRestServer.InternalInfo(var info: TDocVariantData); var cpu,mem,free: RawUTF8; now: TTimeLogBits; m: TSynMonitorMemory; begin // called by root/Timestamp/info REST method now.Value := ServerTimestamp; cpu := TSystemUse.Current(false).HistoryText(0,15,@mem); m := TSynMonitorMemory.Create({nospace=}true); try FormatUTF8('%/%',[m.PhysicalMemoryFree.Text,m.PhysicalMemoryTotal.Text],free); info.AddNameValuesToObject(['nowutc',now.Text(true,' ') , 'timestamp',now.Value, 'exe',ExeVersion.ProgramName, 'version',ExeVersion.Version.DetailedOrVoid, 'host',ExeVersion.Host, 'cpu',cpu, {$ifdef MSWINDOWS}'mem',mem,{$endif} 'memused',KB(m.AllocatedUsed.Bytes), 'memfree',free, 'diskfree',GetDiskPartitionsText({nocache=}false,{withfree=}true,{nospace=}true), 'exception',GetLastExceptions(10)]); finally m.Free; end; Stats.Lock; try info.AddNameValuesToObject([ 'started',Stats.StartDate, 'clients',Stats.ClientsCurrent, 'methods',Stats.ServiceMethod, 'interfaces',Stats.ServiceInterface, 'total',Stats.TaskCount, 'time',Stats.TotalTime.Text]); finally Stats.Unlock; end; if Assigned(OnInternalInfo) then OnInternalInfo(self,info); end; {$endif} procedure TSQLRestServer.InternalStat(Ctxt: TSQLRestServerURIContext; W: TTextWriter); var flags: TSQLRestServerAddStats; begin if Ctxt.InputExists['withall'] then flags := [low(TSQLRestServerAddStat)..high(TSQLRestServerAddStat)] else begin flags := []; if Ctxt.InputExists['withtables'] then include(flags, withtables); if Ctxt.InputExists['withmethods'] then include(flags, withmethods); if Ctxt.InputExists['withinterfaces'] then include(flags, withinterfaces); if Ctxt.InputExists['withsessions'] then include(flags, withsessions); end; AddStat(flags,W); end; procedure TSQLRestServer.AddStat(Flags: TSQLRestServerAddStats; W: TTextWriter); const READWRITE: array[boolean] of string[9] = ('{"read":','{"write":'); var s,i: integer; rw: boolean; begin Stats.ComputeDetailsTo(W); W.CancelLastChar('}'); if fCache<>nil then begin W.AddShort(',"cachedMemoryBytes":'); W.AddU(fCache.CachedMemory); // will also flush outdated JSON W.Add(','); end; if (fBackgroundTimer<>nil) and (fBackgroundTimer.Stats<>nil) then begin W.CancelLastComma; W.AddShort(',"backgroundTimer":'); fBackgroundTimer.Stats.ComputeDetailsTo(W); W.Add(','); end; if withtables in flags then begin W.CancelLastComma; W.AddShort(',"tables":['); Stats.Lock; // thread-safe Stats.fPerTable[] access try for i := 0 to fModel.TablesMax do begin W.Add('{"%":[',[fModel.TableProps[i].Props.SQLTableName]); for rw := False to True do if (inil) and (Stats.fPerTable[rw,i].TaskCount<>0) then begin W.AddShort(READWRITE[rw]); Stats.fPerTable[rw,i].ComputeDetailsTo(W); W.Add('}',','); end; W.CancelLastComma; W.AddShort(']},'); end; finally Stats.UnLock; end; W.CancelLastComma; W.Add(']',','); end; if withmethods in flags then begin W.CancelLastComma; W.AddShort(',"methods":['); for i := 0 to high(fPublishedMethod) do with fPublishedMethod[i] do if (Stats<>nil) and (Stats.TaskCount<>0) then begin W.Add('{"%":',[Name]); Stats.ComputeDetailsTo(W); W.Add('}',','); end; W.CancelLastComma; W.Add(']',','); end; if withinterfaces in flags then begin W.CancelLastComma; W.AddShort(',"interfaces":['); for s := 0 to fServices.Count-1 do with fServices.Index(s) as TServiceFactoryServer do for i := 0 to fInterface.MethodsCount-1 do if fStats[i]<>nil then begin W.Add('{"%":',[fInterface.fMethods[i].InterfaceDotMethodName]); fStats[i].ComputeDetailsTo(W); W.Add('}',','); end; W.CancelLastComma; W.Add(']',','); end; if (withsessions in flags) and (fSessions<>nil) then begin W.CancelLastComma; W.AddShort(',"sessions":['); fSessions.Safe.Lock; try for s := 0 to fSessions.Count-1 do begin W.WriteObject(fSessions.List[s]); W.CancelLastChar('}'); with TAuthSession(fSessions.List[s]) do begin W.AddShort(',"methods":['); for i := 0 to high(fMethods) do if fMethods[i]<>nil then begin W.Add('{"%":',[fPublishedMethod[i].Name]); fMethods[i].ComputeDetailsTo(W); W.Add('}',','); end; W.CancelLastComma; W.AddShort('],"interfaces":['); for i := 0 to high(fInterfaces) do if fInterfaces[i]<>nil then begin W.Add('{"%":',[Services.fInterfaceMethod[i].InterfaceDotMethodName]); fInterfaces[i].ComputeDetailsTo(W); W.Add('}',','); end; W.CancelLastComma; W.AddShort(']},'); end; end; finally fSessions.Safe.UnLock; end; W.CancelLastComma; W.Add(']',','); end; W.CancelLastComma; W.Add('}'); end; procedure TSQLRestServer.Stat(Ctxt: TSQLRestServerURIContext); var W: TTextWriter; json,xml,name: RawUTF8; temp: TTextWriterStackBuffer; begin W := TJSONSerializer.CreateOwnedStream(temp); try name := Ctxt.InputUTF8OrVoid['findservice']; if name='' then begin InternalStat(Ctxt,W); name := 'Stats'; end else AssociatedServices.FindServiceAll(name,W); W.SetText(json); if Ctxt.InputExists['format'] or IdemPropNameU(Ctxt.URIBlobFieldName,'json') then json := JSONReformat(json) else if IdemPropNameU(Ctxt.URIBlobFieldName,'xml') then begin JSONBufferToXML(pointer(json),XMLUTF8_HEADER,'<'+name+'>',xml); Ctxt.Returns(xml,200,XML_CONTENT_TYPE_HEADER); exit; end; Ctxt.Returns(json); finally W.Free; end; end; procedure TSQLRestServer.SetStatUsage(usage: TSynMonitorUsage); begin if fStatUsage=usage then exit; if usage=nil then begin // e.g. from TTestServiceOrientedArchitecture.ClientSideRESTSessionsStats FreeAndNil(fStatUsage); exit; end; if fStatUsage<>nil then raise EModelException.CreateUTF8('%.StatUsage should be set once', [self]); fStatUsage := usage; fStatUsage.Track(fStats,'rest'); end; procedure TSQLRestServer.AdministrationExecute(const DatabaseName,SQL: RawUTF8; var result: TServiceCustomAnswer); var isAjax: boolean; name,interf,method: RawUTF8; obj: TObject; call: TSQLRestURIParams; info: TDocVariantData; P: PUTF8Char; procedure PrepareCall; begin call.Init; call.LowLevelFlags := [llfSecured]; // admin access considered as safe BYPASS_ACCESS_RIGHTS := SUPERVISOR_ACCESS_RIGHTS; call.RestAccessRights := @BYPASS_ACCESS_RIGHTS; call.Url := Model.Root; end; begin isAjax := not NoAjaxJson; if isAjax then NoAjaxJson := true; // reduce memory use from a Delphi (ToolsAdmin) tool try if (SQL<>'') and (SQL[1]='#') then begin P := @SQL[2]; case IdemPCharArray(P,['INTERFACES','STATS(','STATS','SERVICES','SESSIONS', 'GET','POST','WRAPPER','HELP','INFO']) of 0: result.Content := ServicesPublishedInterfaces; 1: begin name := copy(SQL,8,length(SQL)-8); obj := ServiceMethodStat[name]; if obj=nil then begin Split(name,'.',interf,method); obj := Services[interf]; if obj<>nil then obj := (obj as TServiceFactoryServer).Stat[method] else obj := nil; end; if obj<>nil then result.Content := ObjectToJSON(obj); end; 2: result.Content := StatsAsJson; 3: result.Content := Services.AsJson; 4: result.Content := SessionsAsJson; 5,6: begin PrepareCall; GetNextItem(P,' ',call.Method); // GET or POST if P<>nil then call.Url := call.Url+'/'+RawUTF8(P); URI(call); result.Content := call.OutBody; end; 7: begin PrepareCall; call.Method := 'GET'; call.Url := call.Url+'/wrapper/context'; URI(call); result.Content := call.OutBody; end; 8: begin inherited; result.Content[length(result.Content)] := '|'; result.Content := result.Content+'#interfaces|#wrapper|#info|'+ '#stats|#stats(method)|#stats(interface.method)|#services|#sessions|'+ '#get url|#post url"'; end; 9: begin info.InitJSONInPlace(pointer(result.Content)); // from DatabaseExecute() InternalInfo(info); result.Content := info.ToJSON; end; else inherited AdministrationExecute(DatabaseName,SQL,result); end; end else inherited; // will execute the SQL finally NoAjaxJson := not isAjax; end; end; procedure TSQLRestServer.Timestamp(Ctxt: TSQLRestServerURIContext); {$ifndef NOVARIANTS} procedure DoInfo; var info: TDocVariantData; begin info.InitFast; InternalInfo(info); Ctxt.Returns(info.ToJSON('','',jsonHumanReadable)); end; {$endif} begin {$ifndef NOVARIANTS} if IdemPropNameU(Ctxt.URIBlobFieldName,'info') and not (rsoTimestampInfoURIDisable in fOptions) then DoInfo else {$endif} Ctxt.Returns(Int64ToUtf8(ServerTimestamp),HTTP_SUCCESS,TEXT_CONTENT_TYPE_HEADER); end; procedure TSQLRestServer.CacheFlush(Ctxt: TSQLRestServerURIContext); var i,count: integer; begin case Ctxt.Method of mGET: begin if Ctxt.Table=nil then Cache.Flush else if Ctxt.TableID=0 then Cache.Flush(Ctxt.Table) else Cache.SetCache(Ctxt.Table,Ctxt.TableID); Ctxt.Success; end; mPOST: if Ctxt.URIBlobFieldName='_callback_' then // as called from TSQLHttpClientWebsockets.FakeCallbackUnregister (Services as TServiceContainerServer).FakeCallbackRelease(Ctxt) else if Ctxt.URIBlobFieldName='_ping_' then begin count := 0; if Ctxt.Session>CONST_AUTHENTICATION_NOT_USED then for i := 0 to Services.Count-1 do inc(count,TServiceFactoryServer(Services.fInterface[i].Service). RenewSession(Ctxt.Session)); InternalLog('Renew % authenticated session % from %: count=%', [Model.Root,Ctxt.Session,Ctxt.RemoteIPNotLocal,count],sllUserAuth); Ctxt.Returns(['count',count]); end; end; end; procedure TSQLRestServer.Batch(Ctxt: TSQLRestServerURIContext); var Results: TInt64DynArray; i: integer; begin if not (Ctxt.Method in [mPUT,mPOST]) then begin Ctxt.Error('PUT/POST only'); exit; end; try EngineBatchSend(Ctxt.Table,Ctxt.Call.InBody,TIDDynArray(Results),0); except on E: Exception do begin Ctxt.Error(E,'did break % BATCH process',[Ctxt.Table],HTTP_SERVERERROR); exit; end; end; // send back operation status array Ctxt.Call.OutStatus := HTTP_SUCCESS; for i := 0 to length(Results)-1 do if Results[i]<>HTTP_SUCCESS then begin Ctxt.Call.OutBody := Int64DynArrayToCSV(pointer(Results),length(Results),'[',']'); exit; end; Ctxt.Call.OutBody := '["OK"]'; // to save bandwith if no adding end; var ServerNonceHash: TSHA3; // faster than THMAC_SHA256 on small input ServerNonceCache: array[boolean] of record tix: cardinal; res: RawUTF8; end; function CurrentServerNonce(Previous: boolean): RawUTF8; var ticks: cardinal; hash: TSHA3; res: THash256; begin ticks := UnixTimeUTC div (60*5); // 5 minutes resolution if Previous then dec(ticks); with ServerNonceCache[Previous] do if ticks=tix then begin result := res; exit; end; if ServerNonceHash.Algorithm<>SHA3_256 then begin ServerNonceHash.Init(SHA3_256); TAESPRNG.Main.Fill(@res,SizeOf(res)); // ensure unpredictable nonce ServerNonceHash.Update(@res,SizeOf(res)); end; hash := ServerNonceHash; // thread-safe SHA-3 sponge reuse hash.Update(@ticks,SizeOf(ticks)); hash.Final(res,true); result := BinToHexLower(@res,SizeOf(res)); with ServerNonceCache[Previous] do begin tix := ticks; res := result; end; end; procedure TSQLRestServer.SessionCreate(var User: TSQLAuthUser; Ctxt: TSQLRestServerURIContext; out Session: TAuthSession); var i: PtrInt; begin Session := nil; if (reOneSessionPerUser in Ctxt.Call^.RestAccessRights^.AllowRemoteExecute) and (fSessions<>nil) then for i := 0 to fSessions.Count-1 do if TAuthSession(fSessions.List[i]).User.fID=User.fID then begin {$ifdef WITHLOG} with TAuthSession(fSessions.List[i]) do Ctxt.Log.Log(sllUserAuth,'User.LogonName=% already connected from %/%', [User.LogonName,RemoteIP,Ctxt.Call^.LowLevelConnectionID],self); {$endif} Ctxt.AuthenticationFailed(afSessionAlreadyStartedForThisUser); exit; // user already connected end; Session := fSessionClass.Create(Ctxt,User); if Assigned(OnSessionCreate) then if OnSessionCreate(self,Session,Ctxt) then begin // TRUE aborts session creation {$ifdef WITHLOG} Ctxt.Log.Log(sllUserAuth,'Session aborted by OnSessionCreate() callback '+ 'for User.LogonName=% (connected from %/%) - clients=%, sessions=%', [User.LogonName,Session.RemoteIP,Ctxt.Call^.LowLevelConnectionID, fStats.GetClientsCurrent,fSessions.Count],self); {$endif} Ctxt.AuthenticationFailed(afSessionCreationAborted); User := nil; FreeAndNil(Session); exit; end; User := nil; // will be freed by TAuthSession.Destroy fSessions.Add(Session); fStats.ClientConnect; end; procedure TSQLRestServer.Auth(Ctxt: TSQLRestServerURIContext); var i: integer; begin if fSessionAuthentication=nil then exit; fSessions.Safe.Lock; try for i := 0 to length(fSessionAuthentication)-1 do if fSessionAuthentication[i].Auth(Ctxt) then break; // found an authentication, which may be successfull or not finally fSessions.Safe.UnLock; end; end; procedure TSQLRestServer.InternalSafeProtocol(var Call: TSQLRestURIParams; var SafeID: integer); var res: TProtocolResult; P: PUTF8Char; prot: IProtocol; begin if SafeID<>0 then begin // todo: encrypt Call.OutBody+OutHeader exit; end; P := pointer(Call.Url); inc(P,length(fSafeRootUpper)); // IdemPChar() done by caller if P^='/' then inc(P); if IdemPChar(P,'OPEN') then begin if fSafeProtocolNext=nil then fSafeProtocolNext := fSafeProtocol.Clone; res := fSafeProtocolNext.ProcessHandshake(Call.InBody,Call.OutBody); InternalLog('InternalSafeProtocol ProcessHandshake=%',[ToText(res)^]); if not (res in [sprSuccess,sprUnsupported]) then exit; prot := fSafeProtocolNext; fSafeProtocolNext := nil; // todo: register the new connection and return new SafeID exit; end; SafeID := GetCardinal(P); if SafeID=0 then exit; // todo: store IProtocol instance in the associated session // todo: decrypt Call.Url+Method+Inbody+InHeader or close connection end; procedure TSQLRestServer.SessionDelete(aSessionIndex: integer; Ctxt: TSQLRestServerURIContext); var sess: TAuthSession; begin if (self<>nil) and (cardinal(aSessionIndex)nil then (Services as TServiceContainerServer).OnCloseSession(sess.IDCardinal); if Ctxt=nil then InternalLog('Deleted session %:%/%',[sess.User.LogonName,sess.IDCardinal, fSessions.Count],sllUserAuth) else InternalLog('Deleted session %:%/% from %/%',[sess.User.LogonName,sess.IDCardinal, fSessions.Count,sess.RemoteIP,Ctxt.Call^.LowLevelConnectionID],sllUserAuth); if Assigned(OnSessionClosed) then OnSessionClosed(self,sess,Ctxt); fSessions.Delete(aSessionIndex); fStats.ClientDisconnect; end; end; function TSQLRestServer.SessionAccess(Ctxt: TSQLRestServerURIContext): TAuthSession; var i: integer; tix, session: cardinal; sessions: ^TAuthSession; begin // caller of RetrieveSession() made fSessions.Safe.Lock if (self<>nil) and (fSessions<>nil) then begin tix := GetTickCount64 shr 10; if tix<>fSessionsDeprecatedTix then begin fSessionsDeprecatedTix := tix; // check deprecated sessions every second for i := fSessions.Count-1 downto 0 do if tix>TAuthSession(fSessions.List[i]).TimeOutTix then SessionDelete(i,nil); end; // retrieve session from its ID sessions := pointer(fSessions.List); session := Ctxt.Session; for i := 1 to fSessions.Count do if sessions^.IDCardinal=session then begin result := sessions^; result.fTimeOutTix := tix+result.TimeoutShr10; Ctxt.fSession := result; // for TSQLRestServer internal use // make local copy of TAuthSession information Ctxt.SessionUser := result.User.fID; Ctxt.SessionGroup := result.User.GroupRights.fID; Ctxt.SessionUserName := result.User.LogonName; if (result.RemoteIP<>'') and (Ctxt.fRemoteIP='') then Ctxt.fRemoteIP := result.RemoteIP; Ctxt.fSessionAccessRights := result.fAccessRights; Ctxt.Call^.RestAccessRights := @Ctxt.fSessionAccessRights; exit; end else inc(sessions); end; result := nil; end; function TSQLRestServer.SessionGetUser(aSessionID: Cardinal): TSQLAuthUser; var i: integer; begin result := nil; if self=nil then exit; fSessions.Safe.Lock; try for i := 0 to fSessions.Count-1 do with TAuthSession(fSessions.List[i]) do if IDCardinal=aSessionID then begin if User<>nil then begin result := User.CreateCopy as fSQLAuthUserClass; result.GroupRights := nil; end; Break; end; finally fSessions.Safe.UnLock; end; end; function TSQLRestServer.SessionsAsJson: RawJSON; var i: integer; W: TJSONSerializer; temp: TTextWriterStackBuffer; begin result := ''; if (self=nil) or (fSessions.Count=0) then exit; W := TJSONSerializer.CreateOwnedStream(temp); try fSessions.Safe.Lock; try W.Add('['); for i := 0 to fSessions.Count-1 do begin W.WriteObject(fSessions.List[i]); W.Add(','); end; W.CancelLastComma; W.Add(']'); W.SetText(RawUTF8(result)); finally fSessions.Safe.UnLock; end; finally W.Free; end; end; const MAGIC_SESSION: cardinal = $A5ABA5AB; procedure TSQLRestServer.SessionsSaveToFile(const aFileName: TFileName); var i: integer; MS: TRawByteStringStream; W: TFileBufferWriter; s: RawByteString; begin if self=nil then exit; DeleteFile(aFileName); MS := TRawByteStringStream.Create; try W := TFileBufferWriter.Create(MS); fSessions.Safe.Lock; try W.WriteVarUInt32(InternalState); SQLAuthUserClass.RecordProps.SaveBinaryHeader(W); SQLAuthGroupClass.RecordProps.SaveBinaryHeader(W); W.WriteVarUInt32(fSessions.Count); for i := 0 to fSessions.Count-1 do TAuthSession(fSessions.List[i]).SaveTo(W); W.Write4(fSessionCounter); W.Write4(MAGIC_SESSION+1); W.Flush; finally fSessions.Safe.UnLock; W.Free; end; s := SynLZCompress(MS.DataString); SymmetricEncrypt(MAGIC_SESSION,s); FileFromString(s,aFileName,true); finally MS.Free; end; end; procedure TSQLRestServer.SessionsLoadFromFile(const aFileName: TFileName; andDeleteExistingFileAfterRead: boolean); procedure ContentError; begin raise ESynException.CreateUTF8('%.SessionsLoadFromFile("%")',[self,aFileName]); end; var i,n: integer; s: RawByteString; R: TFileBufferReader; P,PEnd: PAnsiChar; begin if self=nil then exit; s := StringFromFile(aFileName); SymmetricEncrypt(MAGIC_SESSION,s); s := SynLZDecompress(s); if s='' then exit; R.OpenFrom(pointer(s),length(s)); fSessions.Safe.Lock; try InternalState := R.ReadVarUInt32; if not SQLAuthUserClass.RecordProps.CheckBinaryHeader(R) or not SQLAuthGroupClass.RecordProps.CheckBinaryHeader(R) then ContentError; n := R.ReadVarUInt32; P := R.CurrentMemory(0,@PEnd); fSessions.Clear; for i := 1 to n do begin fSessions.Add(fSessionClass.CreateFrom(P,PEnd,self)); fStats.ClientConnect; end; fSessionCounter := PCardinal(P)^; inc(P,4); if PCardinal(P)^<>MAGIC_SESSION+1 then ContentError; finally fSessions.Safe.UnLock; R.Close; end; if andDeleteExistingFileAfterRead then DeleteFile(aFileName); end; function TSQLRestServer.CacheWorthItForTable(aTableIndex: cardinal): boolean; begin if self=nil then result := false else result := (aTableIndex>=cardinal(length(fStaticData))) or not fStaticData[aTableIndex].InheritsFrom(TSQLRestStorageInMemory); end; procedure TSQLRestServer.BeginCurrentThread(Sender: TThread); var i, tc: integer; id: TThreadID; begin tc := fStats.NotifyThreadCount(1); id := GetCurrentThreadId; if Sender=nil then raise ECommunicationException.CreateUTF8('%.BeginCurrentThread(nil)',[self]); InternalLog('BeginCurrentThread(%) root=% ThreadID=% ThreadCount=%', [Sender.ClassType,Model.Root,pointer(id),tc]); if Sender.ThreadID<>id then raise ECommunicationException.CreateUTF8( '%.BeginCurrentThread(Thread.ID=%) and CurrentThreadID=% should match', [self,pointer(Sender.ThreadID),pointer(id)]); with PServiceRunningContext(@ServiceContext)^ do // P..(@..)^ for ONE GetTls() if RunningThread<>Sender then // e.g. if length(TSQLHttpServer.fDBServers)>1 if RunningThread<>nil then raise ECommunicationException.CreateUTF8('%.BeginCurrentThread() twice',[self]) else RunningThread := Sender; if fStaticVirtualTable<>nil then for i := 0 to high(fStaticVirtualTable) do if (fStaticVirtualTable[i]<>nil) and fStaticVirtualTable[i].InheritsFrom(TSQLRestStorage) then TSQLRestStorage(fStaticVirtualTable[i]).BeginCurrentThread(Sender); end; procedure TSQLRestServer.EndCurrentThread(Sender: TThread); var i, tc: integer; id: TThreadID; Inst: TServiceFactoryServerInstance; begin tc := fStats.NotifyThreadCount(-1); id := GetCurrentThreadId; if Sender=nil then raise ECommunicationException.CreateUTF8('%.EndCurrentThread(nil)',[self]); InternalLog('EndCurrentThread(%) ThreadID=% ThreadCount=%', [Sender.ClassType,pointer(id),tc]); if Sender.ThreadID<>id then raise ECommunicationException.CreateUTF8( '%.EndCurrentThread(%.ID=%) should match CurrentThreadID=%', [self,Sender,pointer(Sender.ThreadID),pointer(id)]); if fStaticVirtualTable<>nil then for i := 0 to high(fStaticVirtualTable) do if (fStaticVirtualTable[i]<>nil) and fStaticVirtualTable[i].InheritsFrom(TSQLRestStorage) then TSQLRestStorage(fStaticVirtualTable[i]).EndCurrentThread(Sender); if Services<>nil then begin Inst.InstanceID := PtrUInt(id); for i := 0 to Services.Count-1 do with TServiceFactoryServer(Services.fInterface[i].Service) do if InstanceCreation=sicPerThread then InternalInstanceRetrieve(Inst,ord(imFree),0); end; with PServiceRunningContext(@ServiceContext)^ do // P..(@..)^ for ONE GetTls() if RunningThread<>nil then // e.g. if length(TSQLHttpServer.fDBServers)>1 if RunningThread<>Sender then raise ECommunicationException.CreateUTF8( '%.EndCurrentThread(%) should match RunningThread=%', [self,Sender,RunningThread]) else RunningThread := nil; inherited EndCurrentThread(Sender); // should be done eventually end; { TSQLRecordModification } function TSQLRecordModification.ModifiedID: TID; begin if self=nil then result := 0 else result := RecordRef(fModifiedRecord).ID; end; function TSQLRecordModification.ModifiedTable(Model: TSQLModel): TSQLRecordClass; begin if (self=nil) or (Model=nil) then result := nil else result := RecordRef(fModifiedRecord).Table(Model); end; function TSQLRecordModification.ModifiedTableIndex: integer; begin if self=nil then result := 0 else result := RecordRef(fModifiedRecord).TableIndex; end; { TSQLRecordHistory } class procedure TSQLRecordHistory.InitializeTable(Server: TSQLRestServer; const FieldName: RawUTF8; Options: TSQLInitializeTableOptions); begin inherited InitializeTable(Server,FieldName,Options); if FieldName='' then Server.CreateSQLMultiIndex(Self,['ModifiedRecord','Event'],false); end; destructor TSQLRecordHistory.Destroy; begin inherited; fHistoryAdd.Free; end; constructor TSQLRecordHistory.CreateHistory(aClient: TSQLRest; aTable: TSQLRecordClass; aID: TID); var Reference: RecordRef; Rec: TSQLRecord; HistJson: TSQLRecordHistory; begin if (aClient=nil) or (aID<=0) then raise EORMException.CreateUTF8('Invalid %.CreateHistory(%,%,%) call', [self,aClient,aTable,aID]); // read BLOB changes Reference.From(aClient.Model,aTable,aID); fModifiedRecord := Reference.Value; fEvent := heArchiveBlob; Create(aClient,'ModifiedRecord=? and Event=%',[ord(heArchiveBlob)],[fModifiedRecord]); if fID<>0 then aClient.RetrieveBlobFields(self); // load former fHistory field if not HistoryOpen(aClient.Model) then raise EORMException.CreateUTF8('HistoryOpen in %.CreateHistory(%,%,%)', [self,aClient,aTable,aID]); // append JSON changes HistJson := RecordClass.CreateAndFillPrepare(aClient, 'ModifiedRecord=? and Event<>%',[ord(heArchiveBlob)],[fModifiedRecord]) as TSQLRecordHistory; try if HistJson.FillTable.RowCount=0 then exit; // no JSON to append Rec := HistoryGetLast; try while HistJson.FillOne do begin Rec.FillFrom(pointer(HistJson.SentDataJSON)); HistoryAdd(Rec,HistJson); end; HistorySave(nil); // update internal fHistory field finally Rec.Free; end; finally HistJson.Free; end; // prepare for HistoryCount and HistoryGet() from internal fHistory field HistoryOpen(aClient.Model); end; class procedure TSQLRecordHistory.InitializeFields(const Fields: array of const; var JSON: RawUTF8); begin // you may use a TDocVariant to add some custom fields in your own class JSON := JSONEncode(Fields); end; function TSQLRecordHistory.HistoryOpen(Model: TSQLModel): boolean; var len: cardinal; start,i: integer; R: TFileBufferReader; tmp: RawByteString; begin result := false; fHistoryModel := Model; fHistoryUncompressed := ''; fHistoryTable := ModifiedTable(Model); fHistoryUncompressedCount := 0; fHistoryUncompressedOffset := nil; if fHistoryTable=nil then exit; // invalid Model or ModifiedRecord tmp := SynLZDecompress(fHistory); len := length(tmp); if len>4 then begin R.OpenFrom(pointer(tmp),len); if not fHistoryTable.RecordProps.CheckBinaryHeader(R) then exit; // invalid content: TSQLRecord layout may have changed R.ReadVarUInt32Array(fHistoryUncompressedOffset); fHistoryUncompressedCount := length(fHistoryUncompressedOffset); start := R.CurrentPosition; for i := 0 to fHistoryUncompressedCount-1 do inc(fHistoryUncompressedOffset[i],start); fHistoryUncompressed := tmp; end; result := true; end; function TSQLRecordHistory.HistoryCount: integer; begin if (self=nil) or (fHistoryUncompressed='') then result := 0 else result := fHistoryUncompressedCount; end; function TSQLRecordHistory.HistoryGet(Index: integer; out Event: TSQLHistoryEvent; out Timestamp: TModTime; Rec: TSQLRecord): boolean; var P,PEnd: PAnsiChar; begin result := false; if cardinal(Index)>=cardinal(HistoryCount) then exit; P := pointer(fHistoryUncompressed); PEnd := P+length(fHistoryUncompressed); inc(P,fHistoryUncompressedOffset[Index]); if P>=PEnd then exit; Event := TSQLHistoryEvent(P^); inc(P); P := pointer(FromVarUInt64Safe(pointer(P),pointer(PEnd),PQWord(@Timestamp)^)); if P=nil then exit; if (Rec<>nil) and (Rec.RecordClass=fHistoryTable) then begin if Event=heDelete then Rec.ClearProperties else Rec.SetBinaryValuesSimpleFields(P,PEnd); Rec.fID := ModifiedID; if P=nil then exit; end; result := true; end; function TSQLRecordHistory.HistoryGet(Index: integer; Rec: TSQLRecord): boolean; var Event: TSQLHistoryEvent; Timestamp: TModTime; begin result := HistoryGet(Index,Event,Timestamp,Rec); end; function TSQLRecordHistory.HistoryGet(Index: integer): TSQLRecord; var Event: TSQLHistoryEvent; Timestamp: TModTime; begin if fHistoryTable=nil then result := nil else begin result := fHistoryTable.Create; if not HistoryGet(Index,Event,Timestamp,result) then FreeAndNil(result); end; end; function TSQLRecordHistory.HistoryGetLast(Rec: TSQLRecord): boolean; begin result := HistoryGet(fHistoryUncompressedCount-1,Rec); end; function TSQLRecordHistory.HistoryGetLast: TSQLRecord; var Event: TSQLHistoryEvent; Timestamp: TModTime; begin if fHistoryTable=nil then result := nil else begin result := fHistoryTable.Create; // always return an instance HistoryGet(fHistoryUncompressedCount-1,Event,Timestamp,result); end; end; procedure TSQLRecordHistory.HistoryAdd(Rec: TSQLRecord; Hist: TSQLRecordHistory); begin if (self=nil) or (fHistoryModel=nil) or (Rec.RecordClass<>fHistoryTable) then exit; if fHistoryAdd=nil then fHistoryAdd := TFileBufferWriter.Create(TRawByteStringStream); AddInteger(fHistoryAddOffset,fHistoryAddCount,fHistoryAdd.TotalWritten); fHistoryAdd.Write1(Ord(Hist.Event)); fHistoryAdd.WriteVarUInt64(Hist.Timestamp); if Hist.Event<>heDelete then Rec.GetBinaryValuesSimpleFields(fHistoryAdd); end; function TSQLRecordHistory.HistorySave(Server: TSQLRestServer; LastRec: TSQLRecord): boolean; var size,i,maxSize,TableHistoryIndex: integer; firstOldIndex,firstOldOffset, firstNewIndex,firstNewOffset: integer; newOffset: TIntegerDynArray; DBRec: TSQLRecord; HistTemp: TSQLRecordHistory; W: TFileBufferWriter; begin result := false; if (self=nil) or (fHistoryTable=nil) or (fModifiedRecord=0) then exit; // wrong call try // ensure latest item matches "official" one, as read from DB if (Server<>nil) and (LastRec<>nil) and (LastRec.fID=ModifiedID) then begin DBRec := Server.Retrieve(ModifiedRecord); if DBRec<>nil then try // may be just deleted if not DBRec.SameRecord(LastRec) then begin HistTemp := RecordClass.Create as TSQLRecordHistory; try HistTemp.fEvent := heUpdate; HistTemp.fTimestamp := Server.ServerTimestamp; HistoryAdd(DBRec,HistTemp); finally HistTemp.Free; end; end; finally DBRec.Free; end; end; if fHistoryAdd=nil then exit; // nothing new // ensure resulting size matches specified criteria firstOldIndex := 0; TableHistoryIndex := 0; if Server=nil then maxSize := maxInt else begin TableHistoryIndex := Server.Model.GetTableIndexExisting(RecordClass); maxSize := Server.fTrackChangesHistory[TableHistoryIndex].MaxUncompressedBlobSize; end; size := fHistoryAdd.TotalWritten; if (size>maxSize) or (fHistoryUncompressedCount=0) then // e.g. if fHistory.Add() is already bigger than expected firstOldIndex := fHistoryUncompressedCount else begin inc(size,Length(fHistoryUncompressed)-fHistoryUncompressedOffset[0]); while (firstOldIndexmaxSize) do begin dec(size,fHistoryUncompressedOffset[firstOldIndex+1]-fHistoryUncompressedOffset[firstOldIndex]); inc(firstOldIndex); end; end; // creates and store new History BLOB W := TFileBufferWriter.Create(TRawByteStringStream); try // compute offsets if firstOldIndex=fHistoryUncompressedCount then firstOldOffset := length(fHistoryUncompressed) else firstOldOffset := fHistoryUncompressedOffset[firstOldIndex]; SetLength(newOffset,fHistoryUncompressedCount-firstOldIndex+fHistoryAddCount); for i := firstOldIndex to fHistoryUncompressedCount-1 do newOffset[i-firstOldIndex] := fHistoryUncompressedOffset[i]-firstOldOffset; firstNewIndex := fHistoryUncompressedCount-firstOldIndex; firstNewOffset := Length(fHistoryUncompressed)-firstOldOffset; for i := 0 to fHistoryAddCount-1 do newOffset[firstNewIndex+i] := fHistoryAddOffset[i]+firstNewOffset; // write header fHistoryTable.RecordProps.SaveBinaryHeader(W); W.WriteVarUInt32Array(newOffset,length(newOffset),wkOffsetU); // write data W.Write(@PByteArray(fHistoryUncompressed)[firstOldOffset],firstNewOffset); fHistoryAdd.Flush; W.WriteBinary((fHistoryAdd.Stream as TRawByteStringStream).DataString); W.Flush; fHistoryUncompressed := (W.Stream as TRawByteStringStream).DataString; fHistory := SynLZCompress(fHistoryUncompressed); if (Server<>nil) and (fID<>0) then begin Server.EngineUpdateField(TableHistoryIndex, 'Timestamp',Int64ToUTF8(Server.ServerTimestamp),'RowID',Int64ToUtf8(fID)); Server.EngineUpdateBlob(TableHistoryIndex,fID, RecordProps.BlobFields[0].PropInfo,fHistory); end; result := true; finally W.Free; end; finally fHistoryUncompressed := ''; fHistoryUncompressedOffset := nil; FreeAndNil(fHistoryAdd); fHistoryAddOffset := nil; fHistoryAddCount := 0; end; end; procedure TSQLRestServer.TrackChangesFlush(aTableHistory: TSQLRecordHistoryClass); var HistBlob: TSQLRecordHistory; Rec: TSQLRecord; HistJson: TSQLRecordHistory; WhereClause, JSON: RawUTF8; HistID, ModifiedRecord: TInt64DynArray; TableHistoryIndex,i,HistIDCount,n: integer; ModifRecord, ModifRecordCount, MaxRevisionJSON: integer; {$ifdef WITHLOG} log: ISynLog; // for Enter auto-leave to work with FPC {$endif} begin {$ifdef WITHLOG} log := fLogClass.Enter('TrackChangesFlush(%)',[aTableHistory],self); {$endif} fAcquireExecution[execORMWrite].Safe.Lock; // avoid race condition try // low-level Add(TSQLRecordHistory) without cache TableHistoryIndex := Model.GetTableIndexExisting(aTableHistory); MaxRevisionJSON := fTrackChangesHistory[TableHistoryIndex].MaxRevisionJSON; if MaxRevisionJSON<=0 then MaxRevisionJSON := 10; // we will compress into BLOB only when we got more than 10 revisions of a record with MultiFieldValues(aTableHistory,'RowID,ModifiedRecord', 'Event<>%',[ord(heArchiveBlob)],[]) do try GetRowValues(fFieldIndexID,HistID); GetRowValues(FieldIndex('ModifiedRecord'),ModifiedRecord); finally Free; end; QuickSortInt64(pointer(ModifiedRecord),pointer(HistID),0,high(ModifiedRecord)); ModifRecord := 0; ModifRecordCount := 0; n := 0; HistIDCount := 0; for i := 0 to high(ModifiedRecord) do begin if (ModifiedRecord[i]=0) or (HistID[i]=0) then raise EORMException.CreateUTF8('%.TrackChangesFlush: Invalid %.ID=%', [self,aTableHistory,HistID[i]]); if ModifiedRecord[i]<>ModifRecord then begin if ModifRecordCount>MaxRevisionJSON then HistIDCount := n else n := HistIDCount; ModifRecord := ModifiedRecord[i]; ModifRecordCount := 1; end else inc(ModifRecordCount); HistID[n] := HistID[i]; inc(n); end; if ModifRecordCount>MaxRevisionJSON then HistIDCount := n; if HistIDCount=0 then exit; // nothing to compress QuickSortInt64(Pointer(HistID),0,HistIDCount-1); WhereClause := Int64DynArrayToCSV(Pointer(HistID),HistIDCount,'RowID in (',')'); { following SQL is much slower with external tables, and won't work with TSQLRestStorageInMemory -> manual process instead WhereClause := FormatUTF8('ModifiedRecord in (select ModifiedRecord from '+ '(select ModifiedRecord, count(*) NumItems from % group by ModifiedRecord) '+ 'where NumItems>% order by ModifiedRecord) and History is null', [aTableHistory.SQLTableName,MaxRevisionJSON]); } Rec := nil; HistBlob := nil; HistJson := aTableHistory.CreateAndFillPrepare(self,WhereClause); try HistBlob := aTableHistory.Create; while HistJson.FillOne do begin if HistJson.ModifiedRecord<>HistBlob.ModifiedRecord then begin if HistBlob.ModifiedRecord<>0 then HistBlob.HistorySave(self,Rec); FreeAndNil(Rec); HistBlob.fHistory := ''; HistBlob.fID := 0; HistBlob.fEvent := heArchiveBlob; if not Retrieve('ModifiedRecord=? and Event=%', [ord(heArchiveBlob)],[HistJson.ModifiedRecord],HistBlob) then HistBlob.fModifiedRecord := HistJson.ModifiedRecord else RetrieveBlobFields(HistBlob); if not HistBlob.HistoryOpen(Model) then begin InternalLog('Invalid %.History BLOB content for ID=%: % '+ 'layout may have changed -> flush any previous content', [HistBlob.RecordClass,HistBlob.fID,HistJson.ModifiedTable(Model)],sllError); HistBlob.fID := 0; end; if HistBlob.fID<>0 then // allow changes appending to HistBlob Rec := HistBlob.HistoryGetLast else begin // HistBlob.fID=0 -> no previous BLOB content JSON := JSONEncode(['ModifiedRecord',HistJson.ModifiedRecord, 'Timestamp',ServerTimestamp,'Event',ord(heArchiveBlob)]); if HistJson.Event=heAdd then begin // allow versioning from scratch HistBlob.fID := EngineAdd(TableHistoryIndex,JSON); Rec := HistJson.ModifiedTable(Model).Create; HistBlob.HistoryOpen(Model); end else begin Rec := Retrieve(HistJson.ModifiedRecord); if Rec<>nil then try // initialize BLOB with latest revision HistBlob.fID := EngineAdd(TableHistoryIndex,JSON); HistBlob.HistoryOpen(Model); HistBlob.HistoryAdd(Rec,HistJson); finally FreeAndNil(Rec); // ignore partial SentDataJSON for this record end; end; end; end; if (Rec=nil) or (HistBlob.fID=0) then continue; // only append modifications to BLOB if valid Rec.FillFrom(pointer(HistJson.SentDataJSON)); HistBlob.HistoryAdd(Rec,HistJson); end; if HistBlob.ModifiedRecord<>0 then HistBlob.HistorySave(self,Rec); SetLength(HistID,HistIDCount); EngineDeleteWhere(TableHistoryIndex,WhereClause,TIDDynArray(HistID)); finally HistJson.Free; HistBlob.Free; Rec.Free; end; finally fAcquireExecution[execORMWrite].Safe.UnLock; end; end; function TSQLRestServer.InternalUpdateEvent(aEvent: TSQLEvent; aTableIndex: integer; aID: TID; const aSentData: RawUTF8; aIsBlobFields: PSQLFieldBits): boolean; procedure DoTrackChanges(TableHistoryIndex: integer); var TableHistoryClass: TSQLRecordHistoryClass; JSON: RawUTF8; Event: TSQLHistoryEvent; begin case aEvent of seAdd: Event := heAdd; seUpdate: Event := heUpdate; seDelete: Event := heDelete; else exit; end; TableHistoryClass := TSQLRecordHistoryClass(Model.Tables[TableHistoryIndex]); TableHistoryClass.InitializeFields(['ModifiedRecord',aTableIndex+aID shl 6, 'Event',ord(Event),'SentDataJSON',aSentData,'Timestamp',ServerTimestamp],JSON); fAcquireExecution[execORMWrite].Safe.Lock; // avoid race condition try // low-level Add(TSQLRecordHistory) without cache EngineAdd(TableHistoryIndex,JSON); { TODO: use a BATCH (in background thread) to speed up TSQLHistory storage? } if fTrackChangesHistory[TableHistoryIndex].CurrentRow> fTrackChangesHistory[TableHistoryIndex].MaxSentDataJsonRow then begin // gather & compress TSQLRecordHistory.SentDataJson into History BLOB TrackChangesFlush(TableHistoryClass); fTrackChangesHistory[TableHistoryIndex].CurrentRow := 0; end else // fast append as JSON until reached MaxSentDataJsonRow inc(fTrackChangesHistory[TableHistoryIndex].CurrentRow); finally fAcquireExecution[execORMWrite].Safe.UnLock; end; end; var TableHistoryIndex: integer; begin if aID<=0 then result := false else if aIsBlobFields<>nil then // BLOB fields update if (aEvent=seUpdateBlob) and Assigned(OnBlobUpdateEvent) then result := OnBlobUpdateEvent( self,seUpdate,fModel.Tables[aTableIndex],aID,aIsBlobFields^) else result := true else begin // track simple fields modification if cardinal(aTableIndex)=0 then DoTrackChanges(TableHistoryIndex); end; if Assigned(OnUpdateEvent) then result := OnUpdateEvent(self,aEvent,fModel.Tables[aTableIndex],aID,aSentData) else result := true; // true on success, false if error (but action continues) end; end; procedure TSQLRestServer.TrackChanges(const aTable: array of TSQLRecordClass; aTableHistory: TSQLRecordHistoryClass; aMaxHistoryRowBeforeBlob, aMaxHistoryRowPerRecord, aMaxUncompressedBlobSize: integer); var t, tableIndex, TableHistoryIndex: integer; begin if (self=nil) or (high(aTable)<0) then exit; if aMaxHistoryRowBeforeBlob<=0 then // disable change tracking TableHistoryIndex := -1 else begin if aTableHistory=nil then aTableHistory := TSQLRecordHistory; TableHistoryIndex := Model.GetTableIndexExisting(aTableHistory); end; for t := 0 to high(aTable) do begin tableIndex := Model.GetTableIndexExisting(aTable[t]); if aTable[t].InheritsFrom(TSQLRecordHistory) then raise EORMException.CreateUTF8('%.TrackChanges([%]) not allowed',[self,aTable[t]]); if cardinal(tableIndex)=0 then with fTrackChangesHistory[TableHistoryIndex] do begin if CurrentRow=0 then CurrentRow := TableRowCount(aTableHistory); MaxSentDataJsonRow := aMaxHistoryRowBeforeBlob; MaxRevisionJSON := aMaxHistoryRowPerRecord; MaxUncompressedBlobSize := aMaxUncompressedBlobSize; end; end; end; end; function TSQLRestServer.InternalUpdateEventNeeded(aTableIndex: integer): boolean; begin result := (self<>nil) and (Assigned(OnUpdateEvent) or ((cardinal(aTableIndex)=0))); end; function TSQLRestServer.EngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; var Rest: TSQLRest; begin Rest := GetStaticTableIndex(TableModelIndex); if Rest=nil then result := MainEngineAdd(TableModelIndex,SentData) else result := Rest.EngineAdd(TableModelIndex,SentData); end; function TSQLRestServer.EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; var Rest: TSQLRest; begin Rest := GetStaticTableIndex(TableModelIndex); if Rest=nil then result := MainEngineRetrieve(TableModelIndex,ID) else result := Rest.EngineRetrieve(TableModelIndex,ID); end; function TSQLRestServer.EngineList(const SQL: RawUTF8; ForceAJAX: Boolean; ReturnedRowCount: PPtrInt): RawUTF8; var Rest: TSQLRest; StaticSQL: RawUTF8; begin StaticSQL := SQL; Rest := InternalAdaptSQL(Model.GetTableIndexFromSQLSelect(SQL,false),StaticSQL); if Rest=nil then result := MainEngineList(SQL,ForceAJAX,ReturnedRowCount) else result := Rest.EngineList(StaticSQL,ForceAJAX,ReturnedRowCount); end; function TSQLRestServer.EngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; var Rest: TSQLRest; begin Rest := GetStaticTableIndex(TableModelIndex); if Rest=nil then result := MainEngineUpdate(TableModelIndex,ID,SentData) else result := Rest.EngineUpdate(TableModelIndex,ID,SentData); end; function TSQLRestServer.EngineDelete(TableModelIndex: integer; ID: TID): boolean; var Rest: TSQLRest; begin Rest := GetStaticTableIndex(TableModelIndex); if Rest=nil then result := MainEngineDelete(TableModelIndex,ID) else result := Rest.EngineDelete(TableModelIndex,ID); if result then if Model.TableProps[TableModelIndex].Props.RecordVersionField<>nil then InternalRecordVersionDelete(TableModelIndex,ID,nil); end; function TSQLRestServer.EngineDeleteWhere(TableModelIndex: integer; const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean; var Rest: TSQLRest; Batch: TSQLRestBatch; i: integer; begin case length(IDs) of 0: result := false; 1: result := EngineDelete(TableModelIndex,IDs[0]); else begin Rest := GetStaticTableIndex(TableModelIndex); if Rest=nil then result := MainEngineDeleteWhere(TableModelIndex,SQLWhere,IDs) else result := Rest.EngineDeleteWhere(TableModelIndex,SQLWhere,IDs); if (Model.TableProps[TableModelIndex].Props.RecordVersionField=nil) or not result then exit; Batch := TSQLRestBatch.Create(Self,Model.Tables[TableModelIndex],1000); try for i := 0 to high(IDs) do InternalRecordVersionDelete(TableModelIndex,IDs[i],Batch); BatchSend(Batch); // allow faster deletion for engines allowing it finally Batch.Free; end; end; end; end; function TSQLRestServer.EngineRetrieveBlob(TableModelIndex: integer; aID: TID; BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean; var Rest: TSQLRest; begin Rest := GetStaticTableIndex(TableModelIndex); if Rest=nil then result := MainEngineRetrieveBlob(TableModelIndex,aID,BlobField,BlobData) else result := Rest.EngineRetrieveBlob(TableModelIndex,aID,BlobField,BlobData); end; function TSQLRestServer.EngineUpdateBlob(TableModelIndex: integer; aID: TID; BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean; var Rest: TSQLRest; begin Rest := GetStaticTableIndex(TableModelIndex); if Rest=nil then result := MainEngineUpdateBlob(TableModelIndex,aID,BlobField,BlobData) else result := Rest.EngineUpdateBlob(TableModelIndex,aID,BlobField,BlobData); end; function TSQLRestServer.EngineUpdateField(TableModelIndex: integer; const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; var Rest: TSQLRest; begin Rest := GetStaticTableIndex(TableModelIndex); if Rest=nil then result := MainEngineUpdateField(TableModelIndex,SetFieldName,SetValue, WhereFieldName,WhereValue) else result := Rest.EngineUpdateField(TableModelIndex,SetFieldName,SetValue, WhereFieldName,WhereValue); end; function TSQLRestServer.EngineUpdateFieldIncrement(TableModelIndex: integer; ID: TID; const FieldName: RawUTF8; Increment: Int64): boolean; var Rest: TSQLRest; begin Rest := GetStaticTableIndex(TableModelIndex); if Rest=nil then result := MainEngineUpdateFieldIncrement(TableModelIndex,ID,FieldName,Increment) else result := Rest.EngineUpdateFieldIncrement(TableModelIndex,ID,FieldName,Increment); end; function TSQLRestServer.EngineBatchSend(Table: TSQLRecordClass; var Data: RawUTF8; var Results: TIDDynArray; ExpectedResultsCount: integer): integer; var EndOfObject: AnsiChar; wasString, OK: boolean; TableName, Value, ErrMsg: RawUTF8; URIMethod, RunningBatchURIMethod: TSQLURIMethod; RunningBatchRest, RunningRest: TSQLRest; Sent, Method, MethodTable: PUTF8Char; AutomaticTransactionPerRow: cardinal; RowCountForCurrentTransaction: cardinal; RunTableTransactions: array of TSQLRest; RunMainTransaction: boolean; ID: TID; Count: integer; timeoutTix: Int64; batchOptions: TSQLRestBatchOptions; RunTable, RunningBatchTable: TSQLRecordClass; RunTableIndex,i,TableIndex: integer; RunStatic: TSQLRest; RunStaticKind: TSQLRestServerKind; CurrentContext: TSQLRestServerURIContext; counts: array[mPOST..mDELETE] of cardinal; procedure PerformAutomaticCommit; var i: integer; begin if RunningBatchRest<>nil then begin RunningBatchRest.InternalBatchStop; // send pending rows before commit RunningBatchRest := nil; RunningBatchTable := nil; end; for i := 0 to high(RunTableTransactions) do if RunTableTransactions[i]<>nil then begin RunTableTransactions[i].Commit(CONST_AUTHENTICATION_NOT_USED,true); if RunTableTransactions[i]=Self then RunMainTransaction := false; RunTableTransactions[i] := nil; // to acquire and begin a new transaction end; RowCountForCurrentTransaction := 0; end; function IsNotAllowed: boolean; begin result := (CurrentContext<>nil) and (CurrentContext.Command=execORMWrite) and not CurrentContext.Call.RestAccessRights^.CanExecuteORMWrite( URIMethod,RunTable,RunTableIndex,ID,CurrentContext); end; {$ifdef WITHLOG} var log: ISynLog; // for Enter auto-leave to work with FPC {$endif} begin {$ifdef WITHLOG} log := fLogClass.Enter('EngineBatchSend % inlen=%',[Table,length(Data)],self); {$endif} Sent := UniqueRawUTF8(Data); // parsed, therefore modified in-placed if Sent=nil then raise EORMBatchException.CreateUTF8('%.EngineBatchSend(%,"")',[self,Table]); if Table<>nil then begin TableIndex := Model.GetTableIndexExisting(Table); // unserialize expected sequence array as '{"Table":["cmd",values,...]}' if not NextNotSpaceCharIs(Sent,'{') then raise EORMBatchException.CreateUTF8('%.EngineBatchSend: Missing {',[self]); TableName := GetJSONPropName(Sent); if (TableName='') or (Sent=nil) or not IdemPropNameU(TableName,Model.TableProps[TableIndex].Props.SQLTableName) then raise EORMBatchException.CreateUTF8('%.EngineBatchSend(%): Wrong "Table":"%"', [self,Table,TableName]); end else // or '["cmd@Table":values,...]' TableIndex := -1; if not NextNotSpaceCharIs(Sent,'[') then raise EORMBatchException.CreateUTF8('%.EngineBatchSend: Missing [',[self]); if IdemPChar(Sent,'"AUTOMATICTRANSACTIONPERROW",') then begin inc(Sent,29); AutomaticTransactionPerRow := GetNextItemCardinal(Sent,','); end else AutomaticTransactionPerRow := 0; SetLength(RunTableTransactions,Model.TablesMax+1); RunMainTransaction := false; RowCountForCurrentTransaction := 0; if IdemPChar(Sent,'"OPTIONS",') then begin inc(Sent,10); byte(batchOptions) := GetNextItemCardinal(Sent,','); end else byte(batchOptions) := 0; CurrentContext := ServiceContext.Request; MethodTable := nil; RunningBatchRest := nil; RunningBatchTable := nil; RunningBatchURIMethod := mNone; Count := 0; FillCharFast(counts,SizeOf(counts),0); fAcquireExecution[execORMWrite].fSafe.Lock; // multi thread protection try // to protect automatic transactions and global write lock try // to protect InternalBatchStart/Stop locking repeat // main loop: process one POST/PUT/DELETE per iteration // retrieve method name and associated (static) table Method := GetJSONField(Sent,Sent,@wasString); if (Sent=nil) or (Method=nil) or not wasString then raise EORMBatchException.CreateUTF8('%.EngineBatchSend: Missing CMD',[self]); MethodTable := PosChar(Method,'@'); if MethodTable=nil then begin // e.g. '{"Table":[...,"POST",{object},...]}' if TableIndex<0 then raise EORMBatchException.CreateUTF8( '%.EngineBatchSend: "..@Table" expected',[self]); RunTableIndex := TableIndex; RunTable := Table; end else begin // e.g. '[...,"POST@Table",{object},...]' RunTableIndex := Model.GetTableIndexPtr(MethodTable+1); if RunTableIndex<0 then raise EORMBatchException.CreateUTF8('%.EngineBatchSend: Unknown %', [self,MethodTable]); RunTable := Model.Tables[RunTableIndex]; end; RunStatic := GetStaticTableIndex(RunTableIndex,RunStaticKind); if RunStatic=nil then RunningRest := self else RunningRest := RunStatic; // get CRUD method and associated Value/ID case IdemPCharArray(Method,['POST','PUT','DELETE','SIMPLE']) of // IdemPCharArray() will ignore '@' char if appended after method name 0: begin // '{"Table":[...,"POST",{object},...]}' or '[...,"POST@Table",{object},...]' URIMethod := mPOST; Value := JSONGetObject(Sent,@ID,EndOfObject,true); if Sent=nil then raise EORMBatchException.CreateUTF8( '%.EngineBatchSend: Wrong POST',[self]); if IsNotAllowed then raise EORMBatchException.CreateUTF8( '%.EngineBatchSend: POST/Add not allowed on %',[self,RunTable]); if not RecordCanBeUpdated(RunTable,ID,seAdd,@ErrMsg) then raise EORMBatchException.CreateUTF8( '%.EngineBatchSend: POST impossible: %',[self,ErrMsg]); end; 1: begin // '{"Table":[...,"PUT",{object},...]}' or '[...,"PUT@Table",{object},...]' URIMethod := mPUT; Value := JSONGetObject(Sent,@ID,EndOfObject,false); if (Sent=nil) or (Value='') or (ID<=0) then raise EORMBatchException.CreateUTF8('%.EngineBatchSend: Wrong PUT',[self]); if IsNotAllowed then raise EORMBatchException.CreateUTF8( '%.EngineBatchSend: PUT/Update not allowed on %',[self,RunTable]); end; 2: begin // '{"Table":[...,"DELETE",ID,...]}' or '[...,"DELETE@Table",ID,...]' URIMethod := mDELETE; ID := GetInt64(GetJSONField(Sent,Sent,@wasString,@EndOfObject)); if (ID<=0) or wasString then raise EORMBatchException.CreateUTF8( '%.EngineBatchSend: Wrong DELETE',[self]); if IsNotAllowed then raise EORMBatchException.CreateUTF8( '%.EngineBatchSend: DELETE not allowed on %',[self,RunTable]); if not RecordCanBeUpdated(RunTable,ID,seDelete,@ErrMsg) then raise EORMBatchException.CreateUTF8( '%.EngineBatchSend: DELETE impossible [%]',[self,ErrMsg]); end; 3: begin // '{"Table":[...,"SIMPLE",[values],...]}' or '[...,"SIMPLE@Table",[values],...]' URIMethod := mPOST; Value := Model.TableProps[RunTableIndex].Props. SaveSimpleFieldsFromJsonArray(Sent,EndOfObject,true); ID := 0; // no ID is never transmitted with simple fields if (Sent=nil) or (Value='') then raise EORMBatchException.CreateUTF8( '%.EngineBatchSend: Wrong SIMPLE',[self]); if IsNotAllowed then raise EORMBatchException.CreateUTF8( '%.EngineBatchSend: SIMPLE/Add not allowed on %',[self,RunTable]); if not RecordCanBeUpdated(RunTable,0,seAdd,@ErrMsg) then raise EORMBatchException.CreateUTF8( '%.EngineBatchSend: SIMPLE/Add impossible: %',[self,ErrMsg]); end; else raise EORMBatchException.CreateUTF8( '%.EngineBatchSend: Unknown [%] method',[self,Method]); end; if (Count=0) and (EndOfObject=']') then begin // single operation do not need a transaction nor InternalBatchStart/Stop AutomaticTransactionPerRow := 0; SetLength(Results,1); end else begin // handle auto-committed transaction process if AutomaticTransactionPerRow>0 then begin if RowCountForCurrentTransaction=AutomaticTransactionPerRow then PerformAutomaticCommit; // reached AutomaticTransactionPerRow chunk inc(RowCountForCurrentTransaction); if RunTableTransactions[RunTableIndex]=nil then // initiate transaction for this table if not started yet if (RunStatic<>nil) or not RunMainTransaction then begin timeoutTix := GetTickCount64+2000; repeat if RunningRest.TransactionBegin(RunTable, // acquire transaction CONST_AUTHENTICATION_NOT_USED) then begin RunTableTransactions[RunTableIndex] := RunningRest; if RunStatic=nil then RunMainTransaction := true; Break; end; if GetTickCount64>timeoutTix then raise EORMBatchException.CreateUTF8( '%.EngineBatchSend: %.TransactionBegin timeout',[self,RunningRest]); SleepHiRes(1); // retry in 1 ms until fShutdownRequested; end; end; // handle batch pending request sending (if table or method changed) if (RunningBatchRest<>nil) and ((RunTable<>RunningBatchTable) or (RunningBatchURIMethod<>URIMethod)) then begin RunningBatchRest.InternalBatchStop; // send pending statements RunningBatchRest := nil; RunningBatchTable := nil; end; if (RunStatic<>nil) and (RunStatic<>RunningBatchRest) and RunStatic.InternalBatchStart(URIMethod,batchOptions) then begin RunningBatchRest := RunStatic; RunningBatchTable := RunTable; RunningBatchURIMethod := URIMethod; end else if (RunningBatchRest=nil) and (RunStatic=nil) and InternalBatchStart(URIMethod,batchOptions) then begin RunningBatchRest := self; // e.g. multi-insert in main SQlite3 engine RunningBatchTable := RunTable; RunningBatchURIMethod := URIMethod; end; if Count>=length(Results) then SetLength(Results,NextGrow(Count)); end; // process CRUD method operation OK := false; Results[Count] := HTTP_NOTMODIFIED; case URIMethod of mDELETE: begin if EngineDelete(RunTableIndex,ID) then begin if fCache<>nil then fCache.NotifyDeletion(RunTableIndex,ID); if (RunningBatchRest<>nil) or AfterDeleteForceCoherency(RunTableIndex,ID) then begin Results[Count] := HTTP_SUCCESS; // 200 OK OK := true; end; end; end; mPOST: begin ID := EngineAdd(RunTableIndex,Value); Results[Count] := ID; if ID<>0 then begin if fCache<>nil then fCache.Notify(RunTableIndex,ID,Value,soInsert); OK := true; end; end; mPUT: if EngineUpdate(RunTableIndex,ID,Value) then begin Results[Count] := HTTP_SUCCESS; // 200 OK OK := true; if fCache<>nil then // JSON Value may be uncomplete -> delete from cache if not (boPutNoCacheFlush in batchOptions) then fCache.NotifyDeletion(RunTableIndex,ID); end; end; if (boRollbackOnError in batchOptions) and not OK then raise EORMBatchException.CreateUTF8('%.EngineBatchSend: Results[%]=% on % %', [self,Count,Results[Count],Method,RunTable]); inc(Count); inc(counts[URIMethod]); until EndOfObject=']'; if (AutomaticTransactionPerRow>0) and (RowCountForCurrentTransaction>0) then // send pending rows within transaction PerformAutomaticCommit; finally try if RunningBatchRest<>nil then RunningBatchRest.InternalBatchStop; // send pending rows, and release Safe.Lock finally fAcquireExecution[execORMWrite].fSafe.UnLock; InternalLog('EngineBatchSend json=% add=% update=% delete=% %%', [KB(Data),counts[mPOST],counts[mPUT],counts[mDELETE],MethodTable,Table]); end; end; except on E: Exception do begin if (AutomaticTransactionPerRow>0) and (RowCountForCurrentTransaction>0) then begin for i := 0 to high(RunTableTransactions) do if RunTableTransactions[i]<>nil then RunTableTransactions[i].RollBack(CONST_AUTHENTICATION_NOT_USED); UniqueRawUTF8ZeroToTilde(data,1 shl 16); InternalLog('% -> PARTIAL rollback of latest auto-committed transaction data=%', [E,data], sllWarning); end; raise; end; end; if Table<>nil then begin // '{"Table":["cmd":values,...]}' format if Sent=nil then raise EORMBatchException.CreateUTF8('%.EngineBatchSend: % Truncated',[self,Table]); while not (Sent^ in ['}',#0]) do inc(Sent); if Sent^<>'}' then raise EORMBatchException.CreateUTF8('%.EngineBatchSend(%): Missing }',[self,Table]); end; // if we reached here, process was OK SetLength(Results,Count); result := HTTP_SUCCESS; end; function CurrentServiceContext: TServiceRunningContext; begin result := ServiceContext; end; function CurrentServiceContextServer: TSQLRestServer; begin with PServiceRunningContext(@ServiceContext)^ do if Request<>nil then result := Request.Server else result := nil; end; function ToText(gran: TSynMonitorUsageGranularity): PShortString; begin result := GetEnumName(TypeInfo(TSynMonitorUsageGranularity),ord(gran)); end; function ToText(ft: TSQLFieldType): PShortString; begin result := GetEnumName(TypeInfo(TSQLFieldType),ord(ft)); end; function ToText(vk: TSQLRecordVirtualKind): PShortString; begin result := GetEnumName(TypeInfo(TSQLRecordVirtualKind),ord(vk)); end; function ToText(e: TSQLEvent): PShortString; begin result := GetEnumName(TypeInfo(TSQLEvent),ord(e)); end; function ToText(he: TSQLHistoryEvent): PShortString; begin result := GetEnumName(TypeInfo(TSQLHistoryEvent),ord(he)); end; function ToText(o: TSQLOccasion): PShortString; begin result := GetEnumName(TypeInfo(TSQLOccasion),ord(o)); end; function ToText(dft: TSQLDBFieldType): PShortString; begin result := GetEnumName(TypeInfo(TSQLDBFieldType),ord(dft)); end; function ToText(si: TServiceInstanceImplementation): PShortString; begin result := GetEnumName(TypeInfo(TServiceInstanceImplementation),ord(si)); end; function ToText(cmd: TSQLRestServerURIContextCommand): PShortString; begin result := GetEnumName(TypeInfo(TSQLRestServerURIContextCommand),ord(cmd)); end; function ToText(op: TSQLQueryOperator): PShortString; begin result := GetEnumName(TypeInfo(TSQLQueryOperator),ord(op)); end; function ToText(V: TInterfaceMockSpyCheck): PShortString; begin result := GetEnumName(TypeInfo(TInterfaceMockSpyCheck),ord(V)); end; function ToText(m: TSQLURIMethod): PShortString; begin result := GetEnumName(TypeInfo(TSQLURIMethod),ord(m)); end; function ToText(o: TSynTableStatementOperator): PShortString; begin result := GetEnumName(TypeInfo(TSynTableStatementOperator),ord(o)); end; function ToText(t: TSQLVirtualTableTransaction): PShortString; begin result := GetEnumName(TypeInfo(TSQLVirtualTableTransaction),ord(t)); end; function ToText(a: TSQLRestServerAuthenticationSignedURIAlgo): PShortString; begin result := GetEnumName(TypeInfo(TSQLRestServerAuthenticationSignedURIAlgo),ord(a)); end; function ToText(res: TNotifyAuthenticationFailedReason): PShortString; begin result := GetEnumName(TypeInfo(TNotifyAuthenticationFailedReason),ord(res)); end; { TSQLRestClientURIDll } constructor TSQLRestClientURIDll.Create(aModel: TSQLModel; const DllName: TFileName); var aRequest: TURIMapRequest; {$ifdef FPC} aDLL: TLibHandle; {$else} aDLL: THandle; {$endif} begin {$ifdef KYLIX3} aDLL := LoadLibrary(pointer(DllName)); {$else} {$ifdef FPC} aDLL := LoadLibrary(DllName); {$else} aDLL := LoadLibrary(pointer(DllName)); {$endif} {$endif} if aDLL=0 then raise ECommunicationException.CreateUTF8('%.Create: LoadLibrary(%)',[self,DllName]); aRequest := GetProcAddress(aDLL,'URIRequest'); if (@aRequest=nil) or (aRequest(nil,nil,nil,nil,nil).Lo<>HTTP_NOTFOUND) then begin FreeLibrary(aDLL); raise ECommunicationException.CreateUTF8( '%.Create: % doesn''t export a valid URIRequest() function',[self,DllName]); end; Create(aModel,aRequest); fLibraryHandle := aDLL; end; constructor TSQLRestClientURIDll.Create(aModel: TSQLModel; aRequest: TURIMapRequest); begin inherited Create(aModel); Func := aRequest; end; destructor TSQLRestClientURIDll.Destroy; begin if fLibraryHandle<>0 then FreeLibrary(fLibraryHandle); inherited; end; procedure TSQLRestClientURIDll.InternalURI(var Call: TSQLRestURIParams); var result: Int64Rec; pHead, pResp: PUTF8Char; begin if @Func=nil then begin Call.OutStatus := HTTP_NOTIMPLEMENTED; // 501 (no valid application or library) exit; end; pResp := nil; pHead := nil; try result := Func(pointer(Call.Url),pointer(Call.Method),pointer(Call.InBody), @pResp,@pHead); Call.OutStatus := result.Lo; Call.OutInternalState := result.Hi; if pHead<>nil then Call.OutHead := pHead; if pResp<>nil then Call.OutBody := pResp; finally // always release response memory allocated by the server if pResp<>nil then {$ifdef MSWINDOWS} if not USEFASTMM4ALLOC then GlobalFree(PtrUInt(pResp)) else {$endif} Freemem(pResp); if pHead<>nil then {$ifdef MSWINDOWS} if not USEFASTMM4ALLOC then GlobalFree(PtrUInt(pHead)) else {$endif} Freemem(pHead); end; end; function TSQLRestClientURIDll.InternalCheckOpen: boolean; begin result := true; // success end; procedure TSQLRestClientURIDll.InternalClose; begin end; { TSQLRestClientRedirect } constructor TSQLRestClientRedirect.Create(aModel: TSQLModel); begin inherited Create(aModel); fModel.Owner := self; end; constructor TSQLRestClientRedirect.Create(aRedirected: TSQLRest); begin if aRedirected=nil then raise EORMException.CreateUTF8('%.Create(nil)',[self]); Create(TSQLModel.Create(aRedirected.Model)); RedirectTo(aRedirected); end; constructor TSQLRestClientRedirect.CreateOwned(aRedirected: TSQLRestServer); begin Create(aRedirected); fPrivateGarbageCollector.Add(aRedirected); end; procedure TSQLRestClientRedirect.RedirectTo(aRedirected: TSQLRest); begin fSafe.Enter; try fRedirectedClient := nil; fRedirectedServer := nil; if aRedirected=nil then exit; // redirection disabled if aRedirected.InheritsFrom(TSQLRestServer) then fRedirectedServer := aRedirected as TSQLRestServer else if aRedirected.InheritsFrom(TSQLRestClientURI) then fRedirectedClient := aRedirected as TSQLRestClientURI else raise EORMException.CreateUTF8('%.RedirectTo: % should be either % or %', [self,aRedirected,TSQLRestServer,TSQLRestClientURI]); finally fSafe.Leave; end; end; function TSQLRestClientRedirect.InternalCheckOpen: boolean; begin result := Assigned(fRedirectedServer) or Assigned(fRedirectedClient); end; procedure TSQLRestClientRedirect.InternalClose; begin end; procedure TSQLRestClientRedirect.InternalURI(var Call: TSQLRestURIParams); begin fSafe.Enter; try if Assigned(fRedirectedServer) then fRedirectedServer.URI(Call) else if Assigned(fRedirectedClient) then // hook to access InternalURI() protected method TSQLRestClientRedirect(fRedirectedClient).InternalURI(Call) else Call.OutStatus := HTTP_GATEWAYTIMEOUT; finally fSafe.Leave; end; end; {$ifdef MSWINDOWS} {$ifdef ANONYMOUSNAMEDPIPE} // it should be necessary to Edit settings under Local Security Policy -> Local // policies -> Security options -> Edit settings under "Network access" to allow // for anonymous connections. // BUT even with the pipe name added to the // SYSTEM\CurrentControlSet\Services\lanmanserver\parameters\NullSessionPipes // registry key, code below didn't work function GetUserSid(var SID: PSID; var Token: THandle): boolean; var TokenUserSize: DWORD; TokenUserP: PSIDAndAttributes; begin result := false; if not OpenThreadToken(GetCurrentThread,TOKEN_QUERY,True,Token) then if (GetLastError<>ERROR_NO_TOKEN) or not OpenProcessToken(GetCurrentProcess,TOKEN_QUERY,Token) then exit; TokenUserP := nil; TokenUserSize := 0; try if not GetTokenInformation(Token,TokenUser,nil,0,TokenUserSize) and (GetLastError<>ERROR_INSUFFICIENT_BUFFER) then exit; TokenUserP := AllocMem(TokenUserSize); if not GetTokenInformation(Token,TokenUser,TokenUserP,TokenUserSize,TokenUserSize) then exit; SID := TokenUserP^.Sid; result := true; finally FreeMem(TokenUserP); end; end; type ACE_HEADER = record AceType: BYTE; AceFlags: BYTE; AceSize: WORD; end; ACCESS_ALLOWED_ACE = record Header: ACE_HEADER; Mask: ACCESS_MASK; SidStart: DWORD; end; procedure InitializeSecurity(var SA: TSecurityAttributes; var SD); const SECURITY_NT_AUTHORITY: TSIDIdentifierAuthority = (Value: (0, 0, 0, 0, 0, 5)); SECURITY_ANONYMOUS_LOGON_RID = ($00000007); ACL_REVISION = 2; var pSidAnonymous, pSidOwner: PSID; dwAclSize: integer; ACLP: PACL; Token: THandle; begin FillcharFast(SD,SECURITY_DESCRIPTOR_MIN_LENGTH,0); // Initialize the new security descriptor if InitializeSecurityDescriptor(@SD, SECURITY_DESCRIPTOR_REVISION) and GetUserSid(pSidOwner,Token) then begin AllocateAndInitializeSid(SECURITY_NT_AUTHORITY,1, SECURITY_ANONYMOUS_LOGON_RID,0,0,0,0,0,0,0,pSidAnonymous); try dwAclSize := SizeOf(TACL) + 2 * ( SizeOf(ACCESS_ALLOWED_ACE) - SizeOf(DWORD) ) + GetLengthSid(pSidAnonymous) + GetLengthSid(pSidOwner) ; ACLP := AllocMem(dwAclSize); try InitializeAcl(ACLP^,dwAclSize,ACL_REVISION); if not AddAccessAllowedAce(ACLP^,ACL_REVISION, GENERIC_ALL,pSidOwner) then exit; if not AddAccessAllowedAce(ACLP^,ACL_REVISION, GENERIC_READ or GENERIC_WRITE,pSidAnonymous) then exit; if SetSecurityDescriptorDacl(@SD,true,ACLP,false) then begin // Set up the security attributes structure SA.nLength := SizeOf(TSecurityAttributes); SA.lpSecurityDescriptor := @SD; SA.bInheritHandle := true; exit; // mark OK end; finally FreeMem(ACLP); end; finally FreeSid(pSidAnonymous); CloseHandle(Token); end; end; FillcharFast(SA,SizeOf(SA),0); // mark error: no security end; {$else} {$ifndef NOSECURITYFORNAMEDPIPECLIENTS} {$if CompilerVersion >= 22.0} // fix Delphi XE incompatilibility function InitializeSecurityDescriptor(pSecurityDescriptor: PSecurityDescriptor; dwRevision: DWORD): BOOL; stdcall; external advapi32; function SetSecurityDescriptorDacl(pSecurityDescriptor: PSecurityDescriptor; bDaclPresent: BOOL; pDacl: PACL; bDaclDefaulted: BOOL): BOOL; stdcall; external advapi32; {$ifend} procedure InitializeSecurity(var SA: TSecurityAttributes; var SD); begin FillcharFast(SD,SECURITY_DESCRIPTOR_MIN_LENGTH,0); // Initialize the new security descriptor if InitializeSecurityDescriptor(@SD, SECURITY_DESCRIPTOR_REVISION) then begin // Add a NULL descriptor ACL to the security descriptor if SetSecurityDescriptorDacl(@SD, true, nil, false) then begin // Set up the security attributes structure SA.nLength := SizeOf(TSecurityAttributes); SA.lpSecurityDescriptor := @SD; SA.bInheritHandle := true; exit; // mark OK end; end; FillcharFast(SA,SizeOf(SA),0); // mark error: no security end; {$endif NOSECURITYFORNAMEDPIPECLIENTS} {$endif ANONYMOUSNAMEDPIPE} { TSQLRestServerNamedPipe } constructor TSQLRestServerNamedPipe.Create(aServer: TSQLRestServer; const PipeName: TFileName); begin fServer := aServer; fPipeName := PipeName; inherited Create(aServer,false,false); end; destructor TSQLRestServerNamedPipe.Destroy; var i: integer; begin if fChildRunning>0 then begin for i := 0 to length(fChild)-1 do // close any still opened pipe if fChild[i]<>nil then fChild[i].Terminate; while fChildRunning>0 do SleepHiRes(64); // wait for all TSQLRestServerNamedPipeResponse.Destroy end; inherited; end; procedure TSQLRestServerNamedPipe.InternalExecute; {$ifdef FPC} const PIPE_UNLIMITED_INSTANCES = 255; {$endif} var aPipe: cardinal; Available: cardinal; {$ifndef NOSECURITYFORNAMEDPIPECLIENTS} fPipeSecurityAttributes: TSecurityAttributes; fPipeSecurityDescriptor: array[0..SECURITY_DESCRIPTOR_MIN_LENGTH] of byte; {$endif} begin // see http://msdn.microsoft.com/en-us/library/aa365588(v=VS.85).aspx //writeln('TSQLRestServerNamedPipe=',integer(TSQLRestServerNamedPipe),'.Execute'); {$ifndef NOSECURITYFORNAMEDPIPECLIENTS} InitializeSecurity(fPipeSecurityAttributes,fPipeSecurityDescriptor); {$endif} while not Terminated do begin //writeln('TSQLRestServerNamedPipe.CreateNamedPipe(',fPipeName,')'); aPipe := CreateNamedPipe(pointer(fPipeName), PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE or PIPE_READMODE_BYTE or PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, 0, 0, 0, {$ifdef NOSECURITYFORNAMEDPIPECLIENTS}nil{$else}@fPipeSecurityAttributes{$endif}); if aPipe=cardinal(INVALID_HANDLE_VALUE) then break; while not Terminated do if PeekNamedPipe(aPipe,nil,0,nil,@Available,nil) then if (Available>=4) then begin // PeekNamedPipe() made an implicit ConnectNamedPipe(aPipe,nil) TSQLRestServerNamedPipeResponse.Create(fServer,self,aPipe); aPipe := 0; // aPipe will be closed in TSQLRestServerNamedPipeResponse break; end else break // invalid request else SleepHiRes(128); // doesn't slow down connection but decreases CSwitch if aPipe<>0 then begin DisconnectNamedPipe(aPipe); CloseHandle(aPipe); end; end; end; procedure TSQLRestServerNamedPipe.AddChild(new: TSQLRestServerNamedPipeResponse); var i: integer; begin InterlockedIncrement(fChildRunning); i := ObjArrayFind(fChild,nil); // any free slot? if i<0 then ObjArrayAdd(fChild,new) else fChild[i] := new; end; procedure TSQLRestServerNamedPipe.RemoveChild(new: TSQLRestServerNamedPipeResponse); var i: integer; begin if self=nil then exit; new.fMasterThread := self; i := ObjArrayFind(fChild,new); if i>=0 then fChild[i] := nil; // reuse slot InterlockedDecrement(fChildRunning); end; { TSQLRestServerNamedPipeResponse } constructor TSQLRestServerNamedPipeResponse.Create(aServer: TSQLRestServer; aMasterThread: TSQLRestServerNamedPipe; aPipe: cardinal); begin fServer := aServer; fPipe := aPipe; {$ifdef LVCL} FOnTerminate := fServer.EndCurrentThread; {$endif} fMasterThread := aMasterThread; fMasterThread.AddChild(self); FreeOnTerminate := true; inherited Create(fServer,false,false); end; destructor TSQLRestServerNamedPipeResponse.Destroy; begin fMasterThread.RemoveChild(self); inherited; end; procedure TSQLRestServerNamedPipeResponse.InternalExecute; var call: TSQLRestURIParams; Sleeper, Code: integer; Ticks64, ClientTimeOut64: Int64; RemoteIPHeader: RawUTF8; Available: cardinal; begin if (fPipe=0) or (fPipe=Cardinal(INVALID_HANDLE_VALUE)) or (fServer=nil) then exit; RemoteIPHeader := 'RemoteIP: 127.0.0.1'; call.Init; call.LowLevelConnectionID := fPipe; call.LowLevelFlags := [llfSecured]; // assume pipes communication is safe Ticks64 := 0; Sleeper := 0; ClientTimeOut64 := GetTickCount64+30*60*1000; // disconnect after 30 min idle try while not Terminated and not fMasterThread.Terminated do if // (WaitForSingleObject(fPipe,200)=WAIT_OBJECT_0) = don't wait PeekNamedPipe(fPipe,nil,0,nil,@Available,nil) and (Available>=4) then begin FileRead(fPipe,Code,4); if (Code=integer(MAGIC_SYN)) // magic word for URI like request and not Terminated then try call.Url := ReadString(fPipe); call.Method := ReadString(fPipe); call.InHead := ReadString(fPipe); AddToCSV(RemoteIPHeader,call.InHead,#13#10); call.InBody := ReadString(fPipe); call.RestAccessRights := @SUPERVISOR_ACCESS_RIGHTS; call.OutHead := ''; // may not be reset explicitly by fServer.URI() call.OutBody := ''; // it's up to URI overridden method to implement access rights fServer.URI(call); FileWrite(fPipe,call.OutStatus,SizeOf(cardinal)); FileWrite(fPipe,call.OutInternalState,SizeOf(cardinal)); WriteString(fPipe,call.OutHead); WriteString(fPipe,call.OutBody); FlushFileBuffers(fPipe); // Flush the pipe to allow the client to read Ticks64 := GetTickCount64+20; // start sleeping after 20 ms ClientTimeOut64 := Ticks64+30*60*1000; Sleeper := 0; SleepHiRes(0); except on Exception do // error in ReadString() or fServer.URI() break; // disconnect client end else break; // invalid magic word: disconnect client end else if (Ticks64=0) or (GetTickCount64>Ticks64) then begin if Sleeper<128 then inc(Sleeper,8); SleepHiRes(Sleeper); // doesn't slow down connection but decreases CSwitch Ticks64 := 0; if GetTickCount64>ClientTimeOut64 then begin fServer.InternalLog('Disconnected % after 30 min of inactivity',[self]); break; end; end else SleepHiRes(0); finally DisconnectNamedPipe(fPipe); CloseHandle(fPipe); end; end; { TSQLRestClientURINamedPipe } function ImpersonateAnonymousToken(ThreadHandle: THANDLE): BOOL; stdcall; external advapi32; constructor TSQLRestClientURINamedPipe.Create(aModel: TSQLModel; const ApplicationName: TFileName); begin inherited Create(aModel); if {$ifdef UNICODE}IdemPCharW{$else}IdemPChar{$endif}(pointer(ApplicationName),'\\') then fPipeName := ApplicationName else // caller specified a full path fPipeName := ServerPipeNamePrefix+ApplicationName; end; procedure TSQLRestClientURINamedPipe.DefinitionTo(Definition: TSynConnectionDefinition); begin if Definition=nil then exit; inherited DefinitionTo(Definition); // write Kind + User/Password Definition.ServerName := StringToUTF8(fPipeName); end; constructor TSQLRestClientURINamedPipe.RegisteredClassCreateFrom(aModel: TSQLModel; aDefinition: TSynConnectionDefinition); begin Create(aModel,UTF8ToString(aDefinition.ServerName)); inherited RegisteredClassCreateFrom(aModel,aDefinition); // call SetUser() end; function TSQLRestClientURINamedPipe.InternalCheckOpen: boolean; procedure InternalCreateClientPipe; var Pipe: THandle; StartTime64: Int64; {$ifdef WITHLOG} log: ISynLog; {$endif} procedure CreatePipe; begin Pipe := CreateFile(pointer(fPipeName), GENERIC_READ or GENERIC_WRITE, {$ifdef ANONYMOUSNAMEDPIPE} FILE_SHARE_READ or FILE_SHARE_WRITE, nil, OPEN_EXISTING, SECURITY_SQOS_PRESENT or SECURITY_ANONYMOUS, 0); {$else} 0, {$ifdef NOSECURITYFORNAMEDPIPECLIENTS}nil{$else}@fPipeSecurityAttributes{$endif}, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL or FILE_FLAG_OVERLAPPED, 0); {$endif} end; begin {$ifdef WITHLOG} log := fLogClass.Enter(self, 'InternalCheckOpen'); {$endif} {$ifdef ANONYMOUSNAMEDPIPE} if not ImpersonateAnonymousToken(GetCurrentThread) then raise Exception.Create('ImpersonateAnonymousToken'); try {$else} {$ifndef NOSECURITYFORNAMEDPIPECLIENTS} InitializeSecurity(fPipeSecurityAttributes,fPipeSecurityDescriptor); {$endif} {$endif} StartTime64 := GetTickCount64; CreatePipe; while (Pipe=INVALID_HANDLE_VALUE) and (GetLastError=ERROR_FILE_NOT_FOUND) do begin SleepHiRes(10); // wait for TSQLRestServerNamedPipe.EngineExecute to be reached CreatePipe; if (Pipe<>INVALID_HANDLE_VALUE) or (GetTickCount64>StartTime64+500) then break; end; StartTime64 := GetTickCount64; if (Pipe=INVALID_HANDLE_VALUE) and (GetLastError=ERROR_PIPE_BUSY) then InternalLog('Busy % -> retry',[fPipeName],sllDebug); repeat SleepHiRes(10); if WaitNamedPipe(pointer(fPipeName),50) then begin CreatePipe; if GetLastError<>ERROR_PIPE_BUSY then break; end; until GetTickCount64>StartTime64+2000; if Pipe=INVALID_HANDLE_VALUE then begin InternalLog('when connecting to % after % ms', [fPipeName,GetTickCount64-StartTime64],sllLastError); exit; end; {$ifdef ANONYMOUSNAMEDPIPE} finally RevertToSelf; // we just needed to be anonymous during pipe connection end; {$endif} InternalLog('Connected to %',[fPipeName],sllDebug); fServerPipe := Pipe; end; begin if fServerPipe<>0 then begin result := true; exit; // only reconnect if forced by InternalClose call or at first access end; InternalCreateClientPipe; // local sub-procedure to reduce stack overhead result := fServerPipe<>0; end; procedure TSQLRestClientURINamedPipe.InternalClose; begin if fServerPipe<>0 then begin // inherited; may use pipe -> close after WriteString(fServerPipe,''); // send integer=0 -> force server disconnect FileClose(fServerPipe); end; end; procedure TSQLRestClientURINamedPipe.InternalURI(var Call: TSQLRestURIParams); var Card: cardinal; {.$define TSQLRestClientURIDll_TIMEOUT} // to be tried over slow networks if errors {$ifdef TSQLRestClientURIDll_TIMEOUT} i: integer; {$endif} {$ifdef WITHLOG} log: ISynLog; // for Enter auto-leave to work with FPC {$endif} begin {$ifdef WITHLOG} log := fLogClass.Enter(self, 'InternalURI'); {$endif} Call.OutStatus := HTTP_NOTIMPLEMENTED; // 501 (no valid application or library) fSafe.Enter; try if InternalCheckOpen then try Card := MAGIC_SYN; // magic word if FileWrite(fServerPipe,Card,4)<>4 then begin SleepHiRes(0); WaitNamedPipe(pointer(fPipeName),200); if FileWrite(fServerPipe,Card,4)<>4 then begin // pipe may be broken SleepHiRes(10); FileClose(fServerPipe); fServerPipe := 0; if not InternalCheckOpen then // recreate connection exit; if (fServerPipe=Invalid_Handle_Value) or (FileWrite(fServerPipe,Card,4)<>4) then begin Card := GetLastError; InternalLog('reconnecting to %',[fPipeName],sllLastError); if fServerPipe<>Invalid_Handle_Value then FileClose(fServerPipe); fServerPipe := 0; exit; // no existing pipe end; end; end; // send the request WriteString(fServerPipe,Call.Url); WriteString(fServerPipe,Call.Method); WriteString(fServerPipe,Call.InHead); WriteString(fServerPipe,Call.InBody); FlushFileBuffers(fServerPipe); // receive the answer {$ifdef TSQLRestClientURIDll_TIMEOUT} for i := 0 to 25 do // wait up to 325 ms if PeekNamedPipe(fServerPipe,nil,0,nil,@Card,nil) and (Card>=SizeOf(Int64)) then begin FileRead(fServerPipe,Call.OutStatus,SizeOf(cardinal)); FileRead(fServerPipe,Call.OutInternalState,SizeOf(cardinal)); Call.OutHead := ReadString(fServerPipe); Call.OutBody := ReadString(fServerPipe); exit; end else SleepHiRes(i); Call.OutStatus := HTTP_TIMEOUT; // 408 Request Timeout Error {$else} if FileRead(fServerPipe,Call.OutStatus,SizeOf(cardinal))=SizeOf(cardinal) then begin // FileRead() waits till response arrived (or pipe is broken) FileRead(fServerPipe,Call.OutInternalState,SizeOf(cardinal)); Call.OutHead := ReadString(fServerPipe); Call.OutBody := ReadString(fServerPipe); end else Call.OutStatus := HTTP_NOTFOUND; {$endif} except on E: Exception do begin // error in ReadString() InternalLog('% for PipeName=%',[E,fPipeName],sllLastError); Call.OutStatus := HTTP_NOTIMPLEMENTED; // 501 (no valid application or library) WriteString(fServerPipe,''); // try to notify the server of client logout FileClose(fServerPipe); fServerPipe := 0; end; end; finally fSafe.Leave; end; with Call do InternalLog('% % status=% state=%',[method,url,OutStatus,OutInternalState],sllClient); end; {$endif MSWINDOWS} { TSQLRestServerMonitor } constructor TSQLRestServerMonitor.Create(aServer: TSQLRestServer); begin if aServer=nil then raise EORMException.CreateUTF8('%.Create(nil)',[self]); inherited Create(aServer.Model.Root); fServer := aServer; SetLength(fPerTable[false],length(aServer.Model.Tables)); SetLength(fPerTable[true],length(aServer.Model.Tables)); fStartDate := NowUTCToString; end; destructor TSQLRestServerMonitor.Destroy; begin ObjArrayClear(fPerTable[false]); ObjArrayClear(fPerTable[true]); inherited; end; procedure TSQLRestServerMonitor.ProcessSuccess(IsOutcomingFile: boolean); begin fSafe^.Lock; try inc(fSuccess); if IsOutcomingFile then inc(fOutcomingFiles); Changed; finally fSafe^.UnLock; end; end; procedure TSQLRestServerMonitor.NotifyORM(aMethod: TSQLURIMethod); begin fSafe^.Lock; try case aMethod of mGET,mLOCK: inc(fRead); mPOST: inc(fCreated); mPUT: inc(fUpdated); mDELETE: inc(fDeleted); end; Changed; finally fSafe^.UnLock; end; end; procedure TSQLRestServerMonitor.NotifyORMTable(TableIndex, DataSize: integer; Write: boolean; const MicroSecondsElapsed: QWord); const RW: array[boolean] of RawUTF8 = ('.read','.write'); var st: TSynMonitorWithSize; begin if TableIndex<0 then exit; fSafe^.Lock; try if TableIndex>=length(fPerTable[Write]) then // tables may have been added after Create() SetLength(fPerTable[Write],TableIndex+1); if fPerTable[Write,TableIndex]=nil then fPerTable[Write,TableIndex] := TSynMonitorWithSize.Create( fServer.Model.TableProps[TableIndex].Props.SQLTableName+RW[Write]); st := fPerTable[Write,TableIndex]; st.FromExternalMicroSeconds(MicroSecondsElapsed); st.AddSize(DataSize); if fServer.fStatUsage<>nil then fServer.fStatUsage.Modified(st,[]); finally fSafe^.UnLock; end; end; function TSQLRestServerMonitor.NotifyThreadCount(delta: integer): integer; begin if self=nil then result := 0 else begin fSafe^.Lock; try inc(fCurrentThreadCount,delta); result := fCurrentThreadCount; if delta<>0 then Changed; finally fSafe^.UnLock; end; end; end; { TSQLMonitorUsage } function TSQLMonitorUsage.UsageID(aProcessIDShift: integer): integer; begin result := fID shr aProcessIDShift; end; { TSynMonitorUsageRest } constructor TSynMonitorUsageRest.Create(aStorage: TSQLRest; aProcessID: Int64; aStoredClass: TSQLMonitorUsageClass; aProcessIDShift: integer); var g: TSynMonitorUsageGranularity; begin if aStorage=nil then raise ESynException.CreateUTF8('%.Create(nil)',[self]); if aProcessIDShift<0 then aProcessIDShift := 16 { see TSynUniqueIdentifierProcess } else if aProcessIDShift>40 then aProcessIDShift := 40; fProcessIDShift := aProcessIDShift; if aStoredClass=nil then fStoredClass := TSQLMonitorUsage else fStoredClass := aStoredClass; fStorage := aStorage; for g := low(fStoredCache) to high(fStoredCache) do fStoredCache[g] := fStoredClass.Create; fProcessID := aProcessID; {$ifdef WITHLOG} fLog := fStorage.LogFamily; {$endif} inherited Create; end; destructor TSynMonitorUsageRest.Destroy; var g: TSynMonitorUsageGranularity; begin inherited Destroy; // will save pending changes for g := low(fStoredCache) to high(fStoredCache) do fStoredCache[g].Free; end; function TSynMonitorUsageRest.LoadDB(ID: integer; Gran: TSynMonitorUsageGranularity; out Track: variant): boolean; var recid: TID; rec: TSQLMonitorUsage; begin if (ID=0) or (Granhigh(fStoredCache)) then begin result := false; exit; end; rec := fStoredCache[Gran]; recid := (Int64(ID) shl fProcessIDShift) or Int64(fProcessID); if rec.IDValue=recid then result := true else if fStorage.Retrieve(recid,rec) then begin // may use REST cache Track := rec.Info; if rec.Gran=mugHour then fComment := rec.Comment; if rec.Process<>fProcessID then fLog.SynLog.Log(sllWarning,'%.LoadDB(%,%) received Process=%, expected %', [ClassType,ID,ToText(Gran)^,rec.Process,fProcessID]); result := true; end else begin rec.ClearProperties; result := false; end; end; function TSynMonitorUsageRest.SaveDB(ID: integer; const Track: variant; Gran: TSynMonitorUsageGranularity): boolean; var update: boolean; recid: TID; rec: TSQLMonitorUsage; begin if (ID=0) or (Granhigh(fStoredCache)) then begin result := false; exit; end; rec := fStoredCache[Gran]; recid := (Int64(ID) shl fProcessIDShift) or Int64(fProcessID); if rec.IDValue=recid then // already available update := true else begin update := fStorage.Retrieve(recid,rec); // may use REST cache rec.IDValue := recid; end; rec.Gran := Gran; rec.Process := fProcessID; if Gran=mugHour then rec.Comment := fComment; rec.Info := Track; if fSaveBatch<>nil then if update then result := fSaveBatch.Update(rec)>=0 else result := fSaveBatch.Add(rec,true,true)>=0 else if update then result := fStorage.Update(rec) else result := fStorage.Add(rec,true,true)=recid; end; { TSQLRestServerURI } function TSQLRestServerURI.GetURI: TSQLRestServerURIString; begin result := Address; if Port<>'' then result := result+':'+Port; if Root<>'' then result := result+'/'+Root; end; procedure TSQLRestServerURI.SetURI(const Value: TSQLRestServerURIString); begin Split(Value,':',Address,Port); if Port<>'' then Split(Port,'/',Port,Root) else Split(Address,'/',Address,Root); end; function TSQLRestServerURI.Equals(const other: TSQLRestServerURI): boolean; begin result := IdemPropNameU(Address,other.Address) and IdemPropNameU(Port,other.Port) and IdemPropNameU(Root,other.Root); end; { TServicesPublishedInterfacesList } constructor TServicesPublishedInterfacesList.Create(aTimeoutMS: integer); begin inherited Create; fTimeOut := aTimeoutMS; fDynArray.Init(TypeInfo(TServicesPublishedInterfacesDynArray),List,@Count); fDynArrayTimeoutTix.Init(TypeInfo(TInt64DynArray),fTimeoutTix,@fTimeoutTixCount); end; function TServicesPublishedInterfacesList.FindURI( const aPublicURI: TSQLRestServerURI): integer; var tix: Int64; begin tix := GetTickCount64; Safe.Lock; try for result := 0 to Count-1 do if List[result].PublicURI.Equals(aPublicURI) then if (fTimeOut=0) or (fTimeoutTix[result]=0 then if (fTimeOut=0) or (fTimeoutTix[i]=0 then if (fTimeOut=0) or (fTimeoutTix[i]=0 then if (fTimeOut=0) or (fTimeoutTix[i]'') then RegisterFromServerJSON(json); end; procedure TServicesPublishedInterfacesList.RegisterFromServerJSON( var PublishedJson: RawUTF8); var tix: Int64; i: integer; begin Safe.Lock; try fDynArray.LoadFromJSON(pointer(PublishedJson)); fDynArrayTimeoutTix.Count := Count; tix := GetTickCount64; if fTimeout=0 then inc(tix,maxInt) else inc(tix,fTimeout); for i := 0 to Count-1 do fTimeoutTix[i] := tix; finally Safe.UnLock; end; end; procedure TServicesPublishedInterfacesList.RegisterFromClientJSON( var PublishedJson: RawUTF8); var i: integer; nfo: TServicesPublishedInterfaces; crc: cardinal; tix: Int64; P: PUTF8Char; begin if PublishedJson='' then exit; crc := crc32c(0,pointer(PublishedJson),length(PublishedJson)); if (self=nil) or ((fLastPublishedJson<>0) and (crc=fLastPublishedJson)) then exit; // rough but working good in practice, when similar _contract_ P := Pointer(PublishedJson); if P^='[' then inc(P); // when transmitted as [params] in a _contract_ HTTP body content if (RecordLoadJSON(nfo,P,TypeInfo(TServicesPublishedInterfaces))=nil) or (nfo.PublicURI.Address='') then exit; // invalid supplied JSON content Safe.Lock; try // store so that the latest updated version is always at the end for i := 0 to Count-1 do if List[i].PublicURI.Equals(nfo.PublicURI) then begin // ignore Timeout fDynArray.Delete(i); fDynArrayTimeoutTix.Delete(i); break; end; if nfo.Names<>nil then begin fDynArray.Add(nfo); tix := GetTickCount64; if fTimeout=0 then inc(tix,maxInt) else inc(tix,fTimeout); fDynArrayTimeoutTix.Add(tix); end; fLastPublishedJson := crc; finally Safe.UnLock; end; end; { TSQLRestStorageRecordBased } function TSQLRestStorageRecordBased.EngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; var Rec: TSQLRecord; begin result := 0; // mark error if (TableModelIndex<0) or (Model.Tables[TableModelIndex]<>fStoredClass) then exit; Rec := fStoredClass.Create; try Rec.FillFrom(SentData); StorageLock(true,'EngineAdd'); try result := AddOne(Rec,Rec.fID>0,SentData); finally StorageUnLock; end; finally if result<=0 then Rec.Free; // on success, Rec is owned by fValue: TObjectList end; end; function TSQLRestStorageRecordBased.EngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; var Rec: TSQLRecord; begin // this implementation won't handle partial fields update (e.g. BatchUpdate // after FillPrepare) - but TSQLRestStorageInMemory.EngineUpdate will if (ID<=0) or (TableModelIndex<0) or (Model.Tables[TableModelIndex]<>fStoredClass) then begin result := false; // mark error exit; end; StorageLock(true,'EngineUpdate'); try Rec := fStoredClass.Create; try Rec.FillFrom(SentData); Rec.fID := ID; result := UpdateOne(Rec,SentData); finally Rec.Free; end; finally StorageUnLock; end; end; function TSQLRestStorageRecordBased.UpdateOne(ID: TID; const Values: TSQLVarDynArray): boolean; var Rec: TSQLRecord; begin if (ID<=0) then begin result := false; // mark error exit; end; StorageLock(true,'UpdateOne'); try Rec := fStoredClass.Create; try Rec.SetFieldSQLVars(Values); Rec.fID := ID; result := UpdateOne(Rec,Rec.GetJSONValues(true,False,soUpdate)); finally Rec.Free; end; finally StorageUnLock; end; end; { TSQLRestStorageInMemoryUnique } constructor TSQLRestStorageInMemoryUnique.Create(aOwner: TSQLRestStorageInMemory; aField: TSQLPropInfo); begin fOwner := aOwner; fPropInfo := aField; fCaseInsensitive := not(aBinaryCollation in aField.Attributes); fHasher.Init(@fOwner.fValues,nil,EventHash,nil,nil,EventCompare,false); end; function TSQLRestStorageInMemoryUnique.EventCompare(const A,B): integer; begin result := fPropInfo.CompareValue(TSQLRecord(A),TSQLRecord(B),fCaseInsensitive); end; function TSQLRestStorageInMemoryUnique.EventHash(const Elem): cardinal; begin result := fPropInfo.GetHash(TSQLRecord(Elem),fCaseInsensitive); end; function TSQLRestStorageInMemoryUnique.Find(Rec: TSQLRecord): integer; begin if self=nil then // no Unique index for this field result := -1 else begin fLastFindHashCode := fPropInfo.GetHash(Rec,fCaseInsensitive); result := fHasher.Find(@Rec,fLastFindHashCode); end; end; function TSQLRestStorageInMemoryUnique.AddedAfterFind(Rec: TSQLRecord): boolean; begin fHasher.FindBeforeAdd(@Rec,result,fLastFindHashCode); end; { TSQLRestStorageInMemory } constructor TSQLRestStorageInMemory.Create(aClass: TSQLRecordClass; aServer: TSQLRestServer; const aFileName: TFileName; aBinaryFile: boolean); var f: integer; begin inherited Create(aClass,aServer); if (fStoredClassProps<>nil) and (fStoredClassProps.Kind in INSERT_WITH_ID) then raise EModelException.CreateUTF8('%.Create: % virtual table can''t be static', [self,aClass]); fFileName := aFileName; fBinaryFile := aBinaryFile; fValues.Init(TypeInfo(TSQLRecordObjArray),fValue,TSQLRecordDynArrayHashOne, TSQLRecordDynArrayCompare,nil,@fCount); // hashed and compared by ID fValues.{$ifdef UNDIRECTDYNARRAY}InternalDynArray.{$endif}IsObjArray := true; fSearchRec := fStoredClass.Create; if (ClassType<>TSQLRestStorageInMemory) and (fStoredClassProps<>nil) then with fStoredClassProps do begin // used by AdaptSQLForEngineList() method fBasicUpperSQLSelect[false] := SynCommons.UpperCase(SQL.SelectAllWithRowID); SetLength(fBasicUpperSQLSelect[false],length(fBasicUpperSQLSelect[false])-1); // trim right ';' fBasicUpperSQLSelect[true] := StringReplaceAll(fBasicUpperSQLSelect[false],' ROWID,',' ID,'); end; if not IsZero(fIsUnique) then with fStoredClassRecordProps.Fields do begin SetLength(fUnique,Count); for f := 0 to Count-1 do if f in fIsUnique then fUnique[f] := TSQLRestStorageInMemoryUnique.Create(self,List[f]); end; ReloadFromFile; end; destructor TSQLRestStorageInMemory.Destroy; begin UpdateFile; ObjArrayClear(fUnique); fValues.Clear; // to free all stored TSQLRecord instances fSearchRec.Free; inherited Destroy; end; function TSQLRestStorageInMemory.IDToIndex(ID: TID): PtrInt; begin if self<>nil then begin fSearchRec.fID := ID; result := fValues.FindHashed(fSearchRec); end else result := -1; end; function TSQLRestStorageInMemory.AddOne(Rec: TSQLRecord; ForceID: boolean; const SentData: RawUTF8): TID; var ndx, f: PtrInt; added: boolean; begin result := -1; // error if (self=nil) or (Rec=nil) then exit; // ensure no duplicated ID or unique field for f := 0 to high(fUnique) do if f in fIsUnique then begin ndx := fUnique[f].Find(Rec); if ndx>=0 then begin InternalLog('AddOne: non unique %.% on % %', [fStoredClass,fUnique[f].PropInfo.Name,fValue[ndx],Rec],sllDB); exit; end; end; if ForceID then begin if Rec.fID<=0 then raise EORMException.CreateUTF8('%.AddOne(%.ForceID=0)',[self,Rec]); ndx := fValues.FindHashed(Rec); if ndx>=0 then begin InternalLog('AddOne: non unique %.ID on % %',[fStoredClass,fValue[ndx],Rec],sllDB); exit; end; if Rec.fID>fMaxID then fMaxID := Rec.fID else fUnSortedID := true; end else begin inc(fMaxID); // increasing sequence Rec.fID := fMaxID; end; // update internal hash tables and add to internal list for f := 0 to high(fUnique) do if f in fIsUnique then if not fUnique[f].AddedAfterFind(Rec) then // paranoid raise EORMException.CreateUTF8('%.AddOne on %.%',[self,Rec,fUnique[f].PropInfo.Name]); ndx := fValues.FindHashedForAdding(Rec,added); if added then fValue[ndx] := Rec else raise EORMException.CreateUTF8('%.AddOne % failed',[self,Rec]); // paranoid result := Rec.fID; // success fModified := true; if Owner<>nil then Owner.InternalUpdateEvent(seAdd,fStoredClassProps.TableIndex,result,SentData,nil); end; function TSQLRestStorageInMemory.UniqueFieldsUpdateOK(aRec: TSQLRecord; aUpdateIndex: integer): boolean; var f,ndx: PtrInt; begin result := false; for f := 0 to high(fUnique) do if f in fIsUnique then begin ndx := fUnique[f].Find(aRec); if (ndx>=0) and (ndx<>aUpdateIndex) then begin InternalLog('UniqueFieldsUpdateOK failed on % %',[fUnique[f].PropInfo.Name,aRec],sllDB); exit; end; end; result := true; end; function TSQLRestStorageInMemory.EngineDelete(TableModelIndex: integer; ID: TID): boolean; begin if (self=nil) or (ID<=0) or (TableModelIndex<0) or (Model.Tables[TableModelIndex]<>fStoredClass) then result := false else begin StorageLock(True,'EngineDelete'); try result := DeleteOne(IDToIndex(ID)); finally StorageUnLock; end; end; end; function FindMaxID(p: PSQLRecord; n: integer): TID; var id: TID; begin result := 0; if n>0 then repeat id := p^.fID; if id>result then // branchless cmovg on 64-bit FPC result := id; inc(p); dec(n); until n=0; end; function FindMaxIDAndCheckSorted(p: PSQLRecord; n: integer; var unsorted: boolean): TID; var id,prev: TID; {$ifndef CPUX86} lastnotsorted: pointer; {$endif} begin prev := 0; result := 0; {$ifdef CPUX86} unsorted := false; {$else} lastnotsorted := nil; {$endif} if n>0 then repeat id := p^.fID; if id>result then // cmovg on 64-bit FPC result := id; if id<=prev then {$ifdef CPUX86} unsorted := true; {$else} lastnotsorted := p; // cmovle on 64-bit FPC {$endif} prev := id; inc(p); dec(n); until n=0; {$ifndef CPUX86} unsorted := lastnotsorted<>nil; {$endif} end; function TSQLRestStorageInMemory.DeleteOne(aIndex: integer): boolean; var f: integer; rec: TSQLRecord; begin if cardinal(aIndex)>=cardinal(fCount) then result := false else begin rec := fValue[aIndex]; if rec.fID=fMaxID then fMaxID := 0; // recompute if Owner<>nil then // notify BEFORE deletion Owner.InternalUpdateEvent(seDelete,fStoredClassProps.TableIndex,rec.fID,'',nil); for f := 0 to high(fUnique) do if f in fIsUnique then if fUnique[f].Hasher.FindBeforeDelete(@rec)aIndex then raise EORMException.CreateUTF8('%.DeleteOne(%) failed',[self,aIndex]); if fMaxID=0 then fMaxID := FindMaxID(pointer(fValue),fCount); fModified := true; result := true; end; end; function TSQLRestStorageInMemory.EngineDeleteWhere(TableModelIndex: Integer; const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean; var ndx: TIntegerDynArray; n,i: PtrInt; begin // RecordCanBeUpdated() has already been called result := false; n := length(IDs); SetLength(ndx,n); dec(n); StorageLock(True,'EngineDeleteWhere'); try for i := 0 to n do begin if IDs[i]=fMaxID then fMaxID := 0; // force recompute ndx[i] := IDToIndex(IDs[i]); if ndx[i]<0 then exit; end; QuickSortInteger(pointer(ndx),0,n); // slightly faster in reverse order for i := n downto 0 do DeleteOne(ndx[i]); if fMaxID=0 then fMaxID := FindMaxID(pointer(fValue),fCount); result := true; finally StorageUnLock; end; end; function TSQLRestStorageInMemory.EngineExecute(const aSQL: RawUTF8): boolean; begin result := false; // there is no SQL engine with this class end; function TSQLRestStorageInMemory.GetID(Index: integer): TID; begin if (self=nil) or (cardinal(Index)>=cardinal(fCount)) then result := 0 else result := fValue[Index].fID; end; function TSQLRestStorageInMemory.GetItem(Index: integer): TSQLRecord; begin if self<>nil then if cardinal(Index)>=cardinal(fCount) then raise EORMException.CreateUTF8('%.GetItem(%) out of range',[self,Index]) else result := fValue[Index] else result := nil; end; procedure TSQLRestStorageInMemory.GetJSONValuesEvent(aDest: pointer; aRec: TSQLRecord; aIndex: integer); var W: TJSONSerializer absolute aDest; begin aRec.GetJSONValues(W); W.Add(','); end; function TSQLRestStorageInMemory.AdaptSQLForEngineList(var SQL: RawUTF8): boolean; var P: PUTF8Char; Prop: RawUTF8; WithoutRowID: boolean; begin result := inherited AdaptSQLForEngineList(SQL); if result then exit; // 'select * from table' if IdemPropNameU(fBasicSQLCount,SQL) or IdemPropNameU(fBasicSQLHasRows[false],SQL) or IdemPropNameU(fBasicSQLHasRows[true],SQL) then begin result := true; exit; // 'select count(*) from table' will be handled as static end; if fBasicUpperSQLSelect[false]='' then exit; if IdemPChar(pointer(SQL),pointer(fBasicUpperSQLSelect[false])) then WithoutRowID := false else if IdemPChar(pointer(SQL),pointer(fBasicUpperSQLSelect[true])) then WithoutRowID := true else exit; P := pointer(SQL); inc(P,length(fBasicUpperSQLSelect[WithoutRowID])); if P^ in [#0,';'] then begin result := true; // properly ended the WHERE clause as 'SELECT * FROM table' exit; end; P := GotoNextNotSpace(P); if not IdemPChar(P,'WHERE ') then begin if IdemPChar(P,'LIMIT ') then result := true; exit; end; P := GotoNextNotSpace(P+6); GetNextItem(P,'=',Prop); if (P=nil) or (fStoredClassRecordProps.Fields.IndexByName(Prop)<0) then exit; if PWord(P)^=ord(':')+ord('(') shl 8 then inc(P,2); // +2 to ignore :(...): parameter if P^ in ['''','"'] then begin P := GotoEndOfQuotedString(P); if not (P^ in ['''','"']) then exit; end; repeat inc(P) until P^ in [#0..' ',';',')']; // go to end of value if PWord(P)^=ord(')')+ord(':')shl 8 then inc(P,2); // ignore :(...): parameter P := GotoNextNotSpace(P); if (P^ in [#0,';']) or IdemPChar(P,'LIMIT ') then result := true; // properly ended the WHERE clause as 'FIELDNAME=value' end; function TSQLRestStorageInMemory.FindWhereEqual(const WhereFieldName, WhereValue: RawUTF8; OnFind: TFindWhereEqualEvent; Dest: pointer; FoundLimit,FoundOffset: integer; CaseInsensitive: boolean): PtrInt; var WhereFieldIndex: integer; begin result := 0; if (Self=nil) or not Assigned(OnFind) then exit; if IsRowID(pointer(WhereFieldName)) then WhereFieldIndex := 0 else begin WhereFieldIndex := fStoredClassRecordProps.Fields.IndexByName(pointer(WhereFieldName)); if WhereFieldIndex<0 then exit; inc(WhereFieldIndex); // FindWhereEqual() expects index = RTTI+1 end; result := FindWhereEqual(WhereFieldIndex,WhereValue,Onfind,Dest, FoundLimit,FoundOffset,CaseInsensitive); end; function TSQLRestStorageInMemory.FindWhereEqual(WhereField: integer; const WhereValue: RawUTF8; OnFind: TFindWhereEqualEvent; Dest: pointer; FoundLimit,FoundOffset: PtrInt; CaseInsensitive: boolean): PtrInt; var i, currentRow, found: PtrInt; v: Int64; err: integer; P: TSQLPropInfo; nfo: PPropInfo; offs: PtrUInt; ot: TOrdType; vp: PPtrUInt; function FoundOneAndReachedLimit: boolean; begin result := false; // continue search if FoundOffset>0 then begin // omit first FoundOffset rows inc(currentRow); if currentRow>FoundOffset then FoundOffset := 0 else exit; end; if Assigned(OnFind) then OnFind(Dest,fValue[i],i); inc(found); if found>=FoundLimit then result := true; // stop the loop end; begin result := 0; if fCount=0 then exit; if FoundLimit<=0 then FoundLimit := maxInt; if WhereField=SYNTABLESTATEMENTWHEREID then begin if FoundOffset<=0 then begin // omit first FoundOffset rows v := GetInt64(pointer(WhereValue),err); if (err=0) and (v>0) then begin i := IDToIndex(v); // use fast ID hash table if i>=0 then begin if Assigned(OnFind) then OnFind(Dest,fValue[i],i); inc(result); end; end; end; exit; end else if cardinal(WhereField)>cardinal(fStoredClassRecordProps.Fields.Count) then exit; dec(WhereField); // WHERE WhereField=WhereValue (WhereField=RTTIfield+1) P := fStoredClassRecordProps.Fields.List[WhereField]; if not (P.SQLFieldType in COPIABLE_FIELDS) then exit; // nothing to search (e.g. sftUnknown or sftMany) // use fUnique[] hash array for O(1) search if available if WhereField in fIsUnique then begin if FoundOffset<=0 then begin // omit first FoundOffset rows P.SetValueVar(fSearchRec,WhereValue,false); // private copy for comparison i := fUnique[WhereField].Find(fSearchRec); if i>=0 then begin if Assigned(OnFind) then OnFind(Dest,fValue[i],i); inc(result); end; end; exit; end; // full scan optimized search for a specified value found := 0; currentRow := 0; if P.InheritsFrom(TSQLPropInfoRTTIInt32) and (TSQLPropInfoRTTIInt32(P).PropInfo^. PropType^.Kind in [tkInteger,tkEnumeration,tkSet]) then begin // 8/16/32-bit v := GetInt64(pointer(WhereValue),err); // 64-bit for cardinal if err<>0 then exit; nfo := TSQLPropInfoRTTI(P).PropInfo; offs := TSQLPropInfoRTTI(P).fGetterIsFieldPropOffset; if offs<>0 then begin // plain field with no getter ot := nfo^.PropType^.OrdType; if ot in [otSLong,otULong] then begin // handle very common 32-bit Integer field vp := pointer(fValue); for i := 0 to fCount-1 do if (PCardinal(vp^+offs)^=PCardinal(@v)^) and FoundOneAndReachedLimit then break else inc(vp); end else // inlined GetOrdProp() for 8-bit or 16-bit values for i := 0 to fCount-1 do if (FromOrdType(ot,pointer(PtrUInt(fValue[i])+offs))=v) and FoundOneAndReachedLimit then break; end else // has getter -> use GetOrdProp() for i := 0 to fCount-1 do if (nfo^.GetOrdProp(fValue[i])=v) and FoundOneAndReachedLimit then break; end else if P.InheritsFrom(TSQLPropInfoRTTIInt64) then begin // 64-bit integer v := GetInt64(pointer(WhereValue),err); if err<>0 then exit; nfo := TSQLPropInfoRTTI(P).PropInfo; offs := TSQLPropInfoRTTI(P).fGetterIsFieldPropOffset; if offs<>0 then begin // plain field with no getter vp := pointer(fValue); for i := 0 to fCount-1 do if (PInt64(vp^+offs)^=v) and FoundOneAndReachedLimit then break else inc(vp); end else // handle getter for i := 0 to fCount-1 do if (nfo^.GetInt64Prop(fValue[i])=v) and FoundOneAndReachedLimit then break; end else begin // generic search using fast CompareValue() overridden methods P.SetValueVar(fSearchRec,WhereValue,false); // private copy for comparison for i := 0 to fCount-1 do if (P.CompareValue(fValue[i],fSearchRec,CaseInsensitive)=0) and FoundOneAndReachedLimit then break; end; result := found; end; function TSQLRestStorageInMemory.FindMax(WhereField: integer; out max: Int64): boolean; var P: TSQLPropInfo; nfo: PPropInfo; i: PtrInt; v: Int64; begin result := false; max := low(Int64); if fCount=0 then exit; if WhereField=SYNTABLESTATEMENTWHEREID then begin max := fMaxID; result := true; exit; end; if cardinal(WhereField)>cardinal(fStoredClassRecordProps.Fields.Count) then exit; dec(WhereField); // WHERE WhereField=WhereValue (WhereField=RTTIfield+1) P := fStoredClassRecordProps.Fields.List[WhereField]; if P.InheritsFrom(TSQLPropInfoRTTIInt32) then begin nfo := TSQLPropInfoRTTI(P).PropInfo; for i := 0 to fCount-1 do begin v := nfo.GetOrdProp(fValue[i]); if v>max then max := v; end; result := true; end else if P.InheritsFrom(TSQLPropInfoRTTIInt64) then begin nfo := TSQLPropInfoRTTI(P).PropInfo; for i := 0 to fCount-1 do begin v := nfo.GetInt64Prop(fValue[i]); if v>max then max := v; end; result := true; end; end; procedure TSQLRestStorageInMemory.ForEach(WillModifyContent: boolean; OnEachProcess: TFindWhereEqualEvent; Dest: pointer); var i: PtrInt; begin if (self=nil) or (fCount=0) or not Assigned(OnEachProcess) then exit; StorageLock(WillModifyContent,'ForEach'); try for i := 0 to fCount-1 do OnEachProcess(Dest,fValue[i],i); finally StorageUnLock; end; end; function TSQLRestStorageInMemory.GetJSONValues(Stream: TStream; Expand: boolean; Stmt: TSynTableStatement): PtrInt; var ndx,KnownRowsCount: PtrInt; {$ifndef NOVARIANTS} j: PtrInt; id: Int64; {$endif} W: TJSONSerializer; IsNull: boolean; Prop: TSQLPropInfo; bits: TSQLFieldBits; withID: boolean; label err; begin // exact same format as TSQLTable.GetJSONValues() result := 0; if length(Stmt.Where)>1 then raise EORMException.CreateUTF8('%.GetJSONValues on % with Stmt.Where[]=%', [self,fStoredClass,length(Stmt.Where)]); if Stmt.Where=nil then // no WHERE statement -> get all rows -> set rows count if (Stmt.Limit>0) and (fCount>Stmt.Limit) then KnownRowsCount := Stmt.Limit else KnownRowsCount := fCount else KnownRowsCount := 0; Stmt.SelectFieldBits(bits,withID); W := fStoredClassRecordProps.CreateJSONWriter(Stream,Expand,withID,bits, KnownRowsCount,{bufsize=}256 shl 10); if W<>nil then try if Expand then W.Add('['); if Stmt.Where=nil then begin // no WHERE statement -> all rows for ndx := 0 to KnownRowsCount-1 do begin if Expand then W.AddCR; // for better readability fValue[ndx].GetJSONValues(W); W.Add(','); end; result := KnownRowsCount; end else case Stmt.Where[0].Operator of opEqualTo: result := FindWhereEqual(Stmt.Where[0].Field,Stmt.Where[0].Value, GetJSONValuesEvent,W,Stmt.Limit,Stmt.Offset); {$ifndef NOVARIANTS} opIn: if (Stmt.Where[0].Field<>0) or // only handle ID IN (..) by now (Stmt.Offset>0) then goto err else with _Safe(Stmt.Where[0].ValueVariant)^ do for ndx := 0 to Count-1 do if VariantToInt64(Values[ndx],id) then begin j := IDToIndex(id); if j>=0 then begin fValue[j].GetJSONValues(W); W.Add(','); inc(result); if (Stmt.Limit>0) and (result>=Stmt.Limit) then break; end; end else goto err; {$endif} opIsNull, opIsNotNull: if Stmt.Where[0].Field>0 then begin Prop := fStoredClassRecordProps.Fields.List[Stmt.Where[0].Field-1]; if Prop.InheritsFrom(TSQLPropInfoRTTIRawBlob) then begin IsNull := Stmt.Where[0].Operator=opIsNull; for ndx := 0 to fCount-1 do if TSQLPropInfoRTTIRawBlob(Prop).IsNull(fValue[ndx])=IsNull then begin fValue[ndx].GetJSONValues(W); W.Add(','); inc(result); if (Stmt.Limit>0) and (result>=Stmt.Limit) then break; end; end else goto err; end else goto err; else begin err: W.CancelAll; result := 0; exit; end; end; if (result=0) and W.Expand then begin // we want the field names at least, even with no data W.Expand := false; // {"fieldCount":2,"values":["col1","col2"]} W.CancelAll; fStoredClassRecordProps.SetJSONWriterColumnNames(W,0); end; W.EndJSONObject(KnownRowsCount,result); finally W.Free; end; end; procedure TSQLRestStorageInMemory.GetAllIDs(out ID: TIDDynArray); var i: PtrInt; begin StorageLock(false,'GetAllIDs'); try SetLength(ID,fCount); for i := 0 to Count-1 do ID[i] := fValue[i].fID; finally StorageUnlock; end; end; function TSQLRestStorageInMemory.EngineList(const SQL: RawUTF8; ForceAJAX: Boolean; ReturnedRowCount: PPtrInt): RawUTF8; // - GetJSONValues/FindWhereEqual will handle basic REST commands (not all SQL) // only valid SQL command is "SELECT Field1,Field2 FROM Table WHERE ID=120;", // i.e one Table SELECT with one optional "WHERE fieldname = value" statement // - handle also basic "SELECT Count(*) FROM TableName;" SQL statement // Note: this is sufficient for OneFieldValue() and MultiFieldValue() to work var MS: TRawByteStringStream; ResCount: PtrInt; Stmt: TSynTableStatement; max: Int64; procedure SetCount(aCount: integer); begin FormatUTF8('[{"Count(*)":%}]'#$A,[aCount],result); ResCount := 1; end; begin result := ''; ResCount := 0; StorageLock(false,'EngineList'); try if IdemPropNameU(fBasicSQLCount,SQL) then SetCount(TableRowCount(fStoredClass)) else if IdemPropNameU(fBasicSQLHasRows[false],SQL) or IdemPropNameU(fBasicSQLHasRows[true],SQL) then if TableRowCount(fStoredClass)=0 then begin result := '{"fieldCount":1,"values":["RowID"]}'#$A; ResCount := 0; end else begin // return one row with fake ID=1 result := '[{"RowID":1}]'#$A; ResCount := 1; end else begin Stmt := TSynTableStatement.Create(SQL, fStoredClassRecordProps.Fields.IndexByName, fStoredClassRecordProps.SimpleFieldsBits[soSelect]); try if (Stmt.SQLStatement='') or // parsing failed (length(Stmt.Where)>1) or // only a SINGLE expression is allowed yet not IdemPropNameU(Stmt.TableName,fStoredClassRecordProps.SQLTableName) then // invalid request -> return '' exit; if Stmt.SelectFunctionCount=0 then begin // save rows as JSON, with appropriate search according to Where.* arguments MS := TRawByteStringStream.Create; try ForceAJAX := ForceAJAX or not Owner.NoAJAXJSON; ResCount := GetJSONValues(MS,ForceAJAX,Stmt); result := MS.DataString; finally MS.Free; end; end else if (length(Stmt.Select)<>1) or (Stmt.SelectFunctionCount<>1) or ((Stmt.Limit>1) or (Stmt.Offset<>0)) then // handle a single max() or count() function with no LIMIT nor OFFSET exit else case Stmt.Select[0].FunctionKnown of funcCountStar: if Stmt.Where=nil then // was e.g. "SELECT Count(*) FROM TableName;" SetCount(TableRowCount(fStoredClass)) else begin // was e.g. "SELECT Count(*) FROM TableName WHERE ..." ResCount := FindWhereEqual(Stmt.Where[0].Field,Stmt.Where[0].Value, DoNothingEvent,nil,0,0); case Stmt.Where[0].Operator of opEqualTo: SetCount(ResCount); opNotEqualTo: SetCount(TableRowCount(fStoredClass)-ResCount); end; end; funcMax: if (Stmt.Where=nil) and FindMax(Stmt.Select[0].Field,max) then begin FormatUTF8('[{"Max()":%}]'#$A,[max],result); ResCount := 1; end; else exit; // unhandled Distinct() or other SQL functions end; finally Stmt.Free; end; end; finally StorageUnLock; end; if ReturnedRowCount<>nil then ReturnedRowCount^ := ResCount; end; procedure TSQLRestStorageInMemory.DropValues(andUpdateFile: boolean); var f: PtrInt; timer: TPrecisionTimer; begin StorageLock(true,'DropValues'); try fUnSortedID := false; fMaxID := 0; if fCount>0 then begin timer.Start; for f := 0 to high(fUnique) do if f in fIsUnique then fUnique[f].Hasher.Clear; fValues.Hasher.Clear; fValues.Clear; if andUpdateFile then begin fModified := true; UpdateFile; end; InternalLog('DropValues % in %',[fStoredClass,timer.Stop]); end; finally StorageUnLock; end; end; procedure TSQLRestStorageInMemory.LoadFromJSON(const aJSON: RawUTF8); var tmp: TSynTempBuffer; begin tmp.Init(aJSON); try LoadFromJSON(tmp.buf,tmp.len); finally tmp.Done; end; end; procedure TSQLRestStorageInMemory.ComputeStateAfterLoad(var loaded: TPrecisionTimer; binary: boolean); const _CALLER: array[boolean] of string[7] = ('JSON','Binary'); var f,c: PtrInt; cf: RawUTF8; timer: TPrecisionTimer; begin // now fValue[] contains the just loaded data loaded.Pause; timer.Start; fCount := length(fValue); c := fValues.ReHash; if c>0 then cf := 'ID' else for f := 0 to high(fUnique) do if f in fIsUnique then begin c := fUnique[f].Hasher.ReHash({forced=}true,{grow=}false); if c>0 then begin cf := fUnique[f].PropInfo.Name; break; end; end; if c>0 then begin DropValues({andupdatefile=}false); raise EORMException.CreateUTF8('%.LoadFrom%: found % % in %.% field', [self,_CALLER[binary],Plural('duplicate',c),fStoredClass,cf]); end; if binary then begin fMaxID := FindMaxID(pointer(fValue),fCount); fUnSortedID := false; // by SaveToBinary design end else // JSON may have been tampered fMaxID := FindMaxIDAndCheckSorted(pointer(fValue),fCount,fUnSortedID); InternalLog('LoadFrom% % count=% load=% index=%', [_CALLER[binary],fStoredClass,fCount,loaded.Stop,timer.Stop]); end; procedure TSQLRestStorageInMemory.LoadFromJSON(JSONBuffer: PUTF8Char; JSONBufferLen: integer); var T: TSQLTableJSON; timer: TPrecisionTimer; begin StorageLock(true,'LoadFromJSON'); try timer.Start; if fCount>0 then DropValues({andupdatefile=}false); fModified := false; if JSONBuffer=nil then exit; T := TSQLTableJSON.CreateFromTables([fStoredClass],'',JSONBuffer,JSONBufferLen); try if T.fFieldIndexID<0 then // no ID field -> load is impossible exit; T.ToObjArray(fValue,fStoredClass); finally T.Free; end; ComputeStateAfterLoad(timer,{binary=}false); finally StorageUnLock; end; end; procedure TSQLRestStorageInMemory.SaveToJSON(Stream: TStream; Expand: Boolean); var i,j: PtrInt; W: TJSONSerializer; ndx: TIntegerDynArray; begin if self=nil then exit; StorageLock(false,'SaveToJSON'); try if fUnSortedID then fValues.CreateOrderedIndex(ndx,nil); // write in ascending ID order W := fStoredClassRecordProps.CreateJSONWriter( Stream,Expand,true,ALL_FIELDS,fCount,{bufsize=}1 shl 20); try if Expand then W.Add('['); for i := 0 to fCount-1 do begin if Expand then W.AddCR; // for better readability if ndx=nil then j := i else j := ndx[i]; fValue[j].GetJSONValues(W); W.Add(','); end; W.EndJSONObject(fCount,fCount); finally W.Free; end; finally StorageUnLock; end; end; function TSQLRestStorageInMemory.SaveToJSON(Expand: Boolean): RawUTF8; var MS: TRawByteStringStream; begin if self=nil then result := '' else begin MS := TRawByteStringStream.Create; try SaveToJSON(MS,Expand); result := MS.DataString; finally MS.Free; end; end; end; function TSQLRestStorageInMemory.SaveToBinary: RawByteString; var MS: TRawByteStringStream; begin if self=nil then result := '' else begin MS := TRawByteStringStream.Create; try SaveToBinary(MS); result := MS.DataString; finally MS.Free; end; end; end; const TSQLRESTSTORAGEINMEMORY_MAGIC = $A5ABA5A5; function TSQLRestStorageInMemory.LoadFromBinary(Stream: TStream): boolean; var R: TFileBufferReader; MS: TMemoryStream; i,n,f: PtrInt; ID32: TIntegerDynArray; P,PEnd: PAnsiChar; rec: TSQLRecord; id: TID; s: RawUTF8; prop: TSQLPropInfo; timer: TPrecisionTimer; begin result := false; if self=nil then exit; timer.Start; MS := StreamUnSynLZ(Stream,TSQLRESTSTORAGEINMEMORY_MAGIC); if MS=nil then exit; StorageLock(true,'LoadFromBinary'); try // check header: expect same exact RTTI R.OpenFrom(MS.Memory,MS.Size); R.Read(s); if (s<>'') and // new fixed format not IdemPropNameU(s,'TSQLRecordProperties') then // old buggy format exit; if not fStoredClassRecordProps.CheckBinaryHeader(R) then exit; // create instances and read their IDs if fCount>0 then DropValues({andupdatefile=}false); fModified := false; n := R.ReadVarUInt32Array(ID32); SetLength(fValue,abs(n)); // allocate all at once if n<0 then begin // was wkFakeMarker -> TID were stored as VarUInt64 diffs n := abs(n); id := 0; for i := 0 to n-1 do begin rec := fStoredClass.Create; // avoid URW699 with Delphi6/Kylix id := id+{$ifdef FPC_OR_UNICODE}TID{$endif}(R.ReadVarUInt64); rec.fID := id; fValue[i] := rec; end; end else for i := 0 to n-1 do begin rec := fStoredClass.Create; rec.fID := ID32[i]; fValue[i] := rec; end; // read content, grouped by field (for better compression) P := R.CurrentMemory(0,@PEnd); for f := 0 to fStoredClassRecordProps.Fields.Count-1 do begin prop := fStoredClassRecordProps.Fields.List[f]; for i := 0 to n-1 do begin P := prop.SetBinary(fValue[i],P,PEnd); if P=nil then begin DropValues(false); // on error, reset all values exit; end; end; end; ComputeStateAfterLoad(timer,{binary=}true); Result := true; finally StorageUnlock; R.Close; MS.Free; end; end; function TSQLRestStorageInMemory.LoadFromBinary(const Buffer: RawByteString): boolean; var S: TStream; begin S := TRawByteStringStream.Create(Buffer); try result := LoadFromBinary(S); finally S.Free; end; end; procedure TSQLRestStorageInMemory.LoadFromResource(ResourceName: string; Instance: THandle); var S: TStream; begin if ResourceName = '' then ResourceName := fStoredClass.ClassName; if Instance=0 then Instance := HInstance; S := TResourceStream.Create(Instance,ResourceName,pointer(10)); try if not LoadFromBinary(S) then raise EORMException.CreateUTF8('%.LoadFromResource with invalid % content', [self,fStoredClass]); finally S.Free; end; end; function TSQLRestStorageInMemory.SaveToBinary(Stream: TStream): integer; var W: TFileBufferWriter; MS: THeapMemoryStream; i,j,f: PtrInt; hasInt64ID: boolean; p: PID; lastID,newID: TID; ndx,id32: TIntegerDynArray; begin result := 0; if (self=nil) or (Stream=nil) then exit; MS := THeapMemoryStream.Create; W := TFileBufferWriter.Create(MS,1 shl 20); try StorageLock(false,'SaveToBinary'); try // primitive magic and fields signature for file type identification W.Write1(0); // ClassName='TSQLRecordProperties' in old buggy format fStoredClassRecordProps.SaveBinaryHeader(W); // write IDs - in increasing order if fUnSortedID then fValues.CreateOrderedIndex(ndx,nil); SetLength(id32,fCount); hasInt64ID := false; for i := 0 to fCount-1 do begin if ndx=nil then j := i else j := ndx[i]; p := @fValue[j].fID; if p^>high(cardinal) then begin hasInt64ID := true; break; end else id32[i] := PInteger(p)^; end; if hasInt64ID then begin W.WriteVarUInt32(fCount); W.Write1(ord(wkFakeMarker)); // fake marker lastID := 0; for i := 0 to fCount-1 do begin // a bit less efficient than wkSorted if ndx=nil then j := i else j := ndx[i]; newID := fValue[j].fID; if newID<=lastID then raise EORMException.CreateUTF8('%.SaveToBinary(%): duplicated ID', [self,fStoredClass]); W.WriteVarUInt64(newID-lastID); lastID := newID; end; end else W.WriteVarUInt32Values(pointer(id32),fCount,wkSorted); // very efficient // write content, grouped by field (for better compression) for f := 0 to fStoredClassRecordProps.Fields.Count-1 do with fStoredClassRecordProps.Fields.List[f] do if ndx=nil then for i := 0 to fCount-1 do GetBinary(fValue[i],W) else for i := 0 to fCount-1 do GetBinary(fValue[ndx[i]],W); finally StorageUnLock; end; W.Flush; result := StreamSynLZ(MS,Stream,TSQLRESTSTORAGEINMEMORY_MAGIC); finally W.Free; MS.Free; end; end; function TSQLRestStorageInMemory.EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; var i: integer; begin // TableModelIndex is not useful here StorageLock(false,'EngineRetrieve'); try i := IDToIndex(ID); if i<0 then result := '' else result := fValue[i].GetJSONValues(true,true,soSelect); finally StorageUnLock; end; end; function TSQLRestStorageInMemory.GetOne(aID: TID): TSQLRecord; var i: integer; begin if aID = 0 then result := nil else begin StorageLock(false,'GetOne'); try i := IDToIndex(aID); if i<0 then result := nil else result := fValue[i].CreateCopy; finally StorageUnLock; end; end; end; function TSQLRestStorageInMemory.EngineUpdateFieldIncrement(TableModelIndex: integer; ID: TID; const FieldName: RawUTF8; Increment: Int64): boolean; var i,err: integer; P: TSQLPropInfo; V: RawUTF8; wasString: boolean; int: Int64; begin result := false; if (ID<0) or (TableModelIndex<0) or (Model.Tables[TableModelIndex]<>fStoredClass) then exit; P := fStoredClassProps.Prop[FieldName]; if P=nil then exit; if P.PropertyIndex in fIsUnique then begin InternalLog('EngineUpdateFieldIncrement(%) on UNIQUE %.%',[ID,fStoredClass,P.Name],sllDB); exit; end; StorageLock(false,'EngineUpdateFieldIncrement'); try i := IDToIndex(ID); if i<0 then begin InternalLog('EngineUpdateFieldIncrement(%): %.ID=% not found',[P.Name,fStoredClass,ID],sllDB); exit; end; P.GetValueVar(fValue[i],false,V,@wasstring); int := GetInt64(pointer(V),err); if wasString or (err<>0) then begin InternalLog('EngineUpdateFieldIncrement: %.%=[%] not an integer',[fStoredClass,P.Name,V],sllDB); exit; end; Int64ToUtf8(int+Increment,V); P.SetValueVar(fValue[i],V,false); fModified := true; result := true; finally StorageUnLock; end; end; function TSQLRestStorageInMemory.EngineUpdateField(TableModelIndex: integer; const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; var P: TSQLPropInfo; WhereValueString, SetValueString, SetValueJson: RawUTF8; i, WhereFieldIndex: PtrInt; SetValueWasString: boolean; match: TSynList; rec: TSQLRecord; begin result := false; if (TableModelIndex<0) or (fModel.Tables[TableModelIndex]<>fStoredClass) or (SetFieldName='') or (SetValue='') or (WhereFieldName='') or (WhereValue='') then exit; // handle destination field RTTI P := fStoredClassRecordProps.Fields.ByRawUTF8Name(SetFieldName); if P=nil then exit; // don't allow setting ID field, which is Read Only if P.PropertyIndex in fIsUnique then begin InternalLog('EngineUpdateField on UNIQUE %.%',[fStoredClass,P.Name],sllDB); exit; { TODO : allow update UNIQUE field? } end; SetValueWasString := SetValue[1]='"'; if SetValueWasString then UnQuoteSQLStringVar(pointer(SetValue),SetValueString) else SetValueString := SetValue; // handle search field RTTI if IsRowID(pointer(WhereFieldName)) then begin WhereFieldIndex := 0; WhereValueString := WhereValue; end else begin WhereFieldIndex := fStoredClassRecordProps.Fields.IndexByName(WhereFieldName); if WhereFieldIndex<0 then exit; inc(WhereFieldIndex); // FindWhereEqual() expects index = RTTI+1 end; if WhereValue[1]='"' then UnQuoteSQLStringVar(pointer(WhereValue),WhereValueString) else WhereValueString := WhereValue; // search indexes, then apply updates match := TSynList.Create; StorageLock(true,'EngineUpdateField'); try // find matching match[] if FindWhereEqual(WhereFieldIndex,WhereValueString,DoAddToListEvent,match,0,0)=0 then exit; // match.Count=0 -> nothing to update // check that all records can be updated for i := 0 to match.Count-1 do if not RecordCanBeUpdated(fStoredClass,TSQLRecord(match.List[i]).fID,seUpdate) then exit; // one record update fails -> abort all // update field value for i := 0 to match.Count-1 do begin rec := match.List[i]; P.SetValueVar(rec,SetValueString,SetValueWasString); if Owner<>nil then begin if SetValueJson='' then JSONEncodeNameSQLValue(P.Name,SetValue,SetValueJson); Owner.InternalUpdateEvent(seUpdate,fStoredClassProps.TableIndex,rec.fID,SetValueJson,nil); end; end; fModified := true; result := true; finally StorageUnLock; match.Free; end; end; function TSQLRestStorageInMemory.EngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; var i: PtrInt; rec: TSQLRecord; begin result := false; if (ID<0) or (TableModelIndex<0) or (Model.Tables[TableModelIndex]<>fStoredClass) then exit; if SentData='' then begin result := True; exit; end; StorageLock(true,'EngineUpdate'); try i := IDToIndex(ID); if (i<0) or not RecordCanBeUpdated(fStoredClass,ID,seUpdate) then exit; if fUnique<>nil then begin rec := fValue[i].CreateCopy; // copy since can be a partial update rec.FillFrom(SentData); // overwrite updated properties if not UniqueFieldsUpdateOK(rec,i) then begin rec.Free; exit; end; fValue[i].Free; // avoid memory leak fValue[i] := rec; // replace item in list end else // direct in-place (partial) update fValue[i].FillFrom(SentData); fModified := true; result := true; if Owner<>nil then Owner.InternalUpdateEvent(seUpdate,fStoredClassProps.TableIndex,ID,SentData,nil); finally StorageUnLock; end; end; function TSQLRestStorageInMemory.UpdateOne(Rec: TSQLRecord; const SentData: RawUTF8): boolean; var i: PtrInt; begin result := false; if (Rec=nil) or (PSQLRecordClass(Rec)^<>fStoredClass) or (Rec.fID<=0) then exit; StorageLock(true,'UpdateOne'); try i := IDToIndex(Rec.fID); if (i<0) or not RecordCanBeUpdated(fStoredClass,Rec.fID,seUpdate) then exit; if (fUnique<>nil) and not UniqueFieldsUpdateOK(Rec,i) then exit; CopyObject(Rec,fValue[i]); fModified := true; result := true; if Owner<>nil then Owner.InternalUpdateEvent(seUpdate,fStoredClassProps.TableIndex,Rec.fID,SentData,nil); finally StorageUnLock; end; end; function TSQLRestStorageInMemory.UpdateOne(ID: TID; const Values: TSQLVarDynArray): boolean; var i: PtrInt; rec: TSQLRecord; begin result := false; if ID<=0 then exit; StorageLock(true,'UpdateOne'); try i := IDToIndex(ID); if (i<0) or not RecordCanBeUpdated(fStoredClass,ID,seUpdate) then exit; if fUnique<>nil then begin rec := fValue[i].CreateCopy; // copy since can be a partial update if not rec.SetFieldSQLVars(Values) or not UniqueFieldsUpdateOK(rec,i) then begin rec.Free; exit; end; fValue[i].Free; // avoid memory leak fValue[i] := rec; end else if not fValue[i].SetFieldSQLVars(Values) then exit; fModified := true; result := true; if Owner<>nil then Owner.InternalUpdateEvent(seUpdate,fStoredClassProps.TableIndex,ID, fValue[i].GetJSONValues(True,False,soUpdate),nil); finally StorageUnLock; end; end; function TSQLRestStorageInMemory.EngineRetrieveBlob(TableModelIndex: integer; aID: TID; BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean; var i: integer; begin result := false; if (TableModelIndex<0) or not BlobField^.IsBlob or (fModel.Tables[TableModelIndex]<>fStoredClass) then exit; StorageLock(false,'EngineRetrieveBlob'); try i := IDToIndex(aID); if i<0 then exit; // get result blob directly from RTTI property description BlobField.GetLongStrProp(fValue[i],RawByteString(BlobData)); result := true; finally StorageUnLock; end; end; function TSQLRestStorageInMemory.RetrieveBlobFields(Value: TSQLRecord): boolean; var i,f: integer; begin result := false; if (Value<>nil) and (Value.fID>0) and (PSQLRecordClass(Value)^=fStoredClass) then with Value.RecordProps do if BlobFields<>nil then begin StorageLock(false,'RetrieveBlobFields'); try i := IDToIndex(Value.fID); if i<0 then exit; for f := 0 to high(BlobFields) do BlobFields[f].CopyValue(fValue[i],Value); result := true; finally StorageUnLock; end; end; end; function TSQLRestStorageInMemory.EngineUpdateBlob(TableModelIndex: integer; aID: TID; BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean; var i: integer; AffectedField: TSQLFieldBits; begin result := false; if (aID<0) or (TableModelIndex<0) or not BlobField^.IsBlob or (fModel.Tables[TableModelIndex]<>fStoredClass) then exit; StorageLock(true,'EngineUpdateBlob'); try i := IDToIndex(aID); if (i<0) or not RecordCanBeUpdated(fStoredClass,aID,seUpdate) then exit; // set blob value directly from RTTI property description BlobField.SetLongStrProp(fValue[i],BlobData); if Owner<>nil then begin fStoredClassRecordProps.FieldBitsFromBlobField(BlobField,AffectedField); Owner.InternalUpdateEvent(seUpdateBlob,fStoredClassProps.TableIndex,aID,'',@AffectedField); end; fModified := true; result := true; finally StorageUnLock; end; end; function TSQLRestStorageInMemory.UpdateBlobFields(Value: TSQLRecord): boolean; var i,f: integer; begin result := false; if (Value<>nil) and (Value.fID>0) and (PSQLRecordClass(Value)^=fStoredClass) then with Value.RecordProps do if BlobFields<>nil then begin StorageLock(true,'UpdateBlobFields'); try i := IDToIndex(Value.fID); if (i<0) or not RecordCanBeUpdated(Table,Value.fID,seUpdate) then exit; for f := 0 to high(BlobFields) do BlobFields[f].CopyValue(Value,fValue[i]); if Owner<>nil then Owner.InternalUpdateEvent(seUpdateBlob,fStoredClassProps.TableIndex,Value.fID,'', @fStoredClassRecordProps.FieldBits[sftBlob]); fModified := true; result := true; finally StorageUnLock; end; end else result := true; // as TSQLRest.UpdateblobFields() end; function TSQLRestStorageInMemory.TableRowCount(Table: TSQLRecordClass): Int64; begin if Table<>fStoredClass then result := 0 else result := fCount; end; function TSQLRestStorageInMemory.TableHasRows(Table: TSQLRecordClass): boolean; begin result := (Table=fStoredClass) and (fCount>0); end; function TSQLRestStorageInMemory.MemberExists(Table: TSQLRecordClass; ID: TID): boolean; begin StorageLock(false,'UpdateFile'); try result := (Table=fStoredClass) and (IDToIndex(ID)>=0); finally StorageUnLock; end; end; procedure TSQLRestStorageInMemory.UpdateFile; var F: TFileStream; timer: TPrecisionTimer; begin if (self=nil) or not fModified or (FileName='') then exit; timer.Start; StorageLock(false,'UpdateFile'); try DeleteFile(FileName); // always erase previous file if fCount>0 then begin F := TFileStream.Create(FileName,fmCreate); try if BinaryFile then SaveToBinary(F) else SaveToJSON(F,true); finally F.Free; end; end; fModified := false; finally StorageUnLock; end; InternalLog('UpdateFile % in %',[fStoredClass,timer.Stop],sllDB); end; procedure TSQLRestStorageInMemory.SetFileName(const aFileName: TFileName); begin if aFileName=fFileName then exit; fFileName := aFileName; fModified := true; end; procedure TSQLRestStorageInMemory.SetBinaryFile(aBinary: boolean); begin if aBinary=fBinaryFile then Exit; fBinaryFile := aBinary; fModified := true; end; procedure TSQLRestStorageInMemory.ReloadFromFile; var JSON: RawUTF8; Stream: TStream; begin if (fFileName<>'') and FileExists(fFileName) then begin if fBinaryFile then begin Stream := FileStreamSequentialRead(fFileName); try LoadFromBinary(Stream) finally Stream.Free; end; end else begin JSON := AnyTextFileToRawUTF8(fFileName,true); LoadFromJSON(pointer(JSON),length(JSON)); // buffer parsed in-place end; end; end; function TSQLRestStorageInMemory.SearchField(const FieldName, FieldValue: RawUTF8; out ResultID: TIDDynArray): boolean; var n, WhereField,i: integer; match: TSynList; begin result := false; if (self=nil) or (fCount=0) then exit; if IsRowID(pointer(FieldName)) then WhereField := SYNTABLESTATEMENTWHEREID else begin WhereField := fStoredClassRecordProps.Fields.IndexByName(FieldName); if WhereField<0 then exit; inc(WhereField); // FindWhereEqual() expects index = RTTI+1 end; match := TSynList.Create; try StorageLock(false,'SearchField'); try n := FindWhereEqual(WhereField,FieldValue,DoAddToListEvent,match,0,0); if n=0 then exit; SetLength(ResultID,n); for i := 0 to n-1 do ResultID[i] := TSQLRecord(match.List[i]).fID; finally StorageUnLock; end; finally match.Free; end; end; function TSQLRestStorageInMemory.SearchEvent(const FieldName, FieldValue: RawUTF8; OnFind: TFindWhereEqualEvent; Dest: pointer; FoundLimit,FoundOffset: PtrInt): integer; begin result := 0; if (self=nil) or (fCount=0) or (FieldName='') then exit; StorageLock(false,'SearchEvent'); try result := FindWhereEqual(FieldName,FieldValue,OnFind,Dest,FoundLimit,FoundOffset); finally StorageUnlock; end; end; function TSQLRestStorageInMemory.SearchCopy(const FieldName, FieldValue: RawUTF8): pointer; begin if SearchEvent(FieldName,FieldValue,DoCopyEvent,@result,1,0)=0 then result := nil; end; function TSQLRestStorageInMemory.SearchInstance(const FieldName, FieldValue: RawUTF8): pointer; begin if SearchEvent(FieldName,FieldValue,DoInstanceEvent,@result,1,0)=0 then result := nil; end; function TSQLRestStorageInMemory.SearchIndex(const FieldName, FieldValue: RawUTF8): integer; begin if SearchEvent(FieldName,FieldValue,DoIndexEvent,@result,1,0)=0 then result := -1; end; function TSQLRestStorageInMemory.SearchCount(const FieldName, FieldValue: RawUTF8): integer; begin result := SearchEvent(FieldName,FieldValue,DoNothingEvent,nil,0,0); end; class procedure TSQLRestStorageInMemory.DoNothingEvent(aDest: pointer; aRec: TSQLRecord; aIndex: integer); begin end; class procedure TSQLRestStorageInMemory.DoCopyEvent(aDest: pointer; aRec: TSQLRecord; aIndex: integer); begin if aDest<>nil then PPointer(aDest)^ := aRec.CreateCopy; end; class procedure TSQLRestStorageInMemory.DoAddToListEvent(aDest: pointer; aRec: TSQLRecord; aIndex: integer); begin if aDest<>nil then TSynList(aDest).Add(aRec); end; class procedure TSQLRestStorageInMemory.DoInstanceEvent(aDest: pointer; aRec: TSQLRecord; aIndex: integer); begin if aDest<>nil then PPointer(aDest)^ := aRec; end; class procedure TSQLRestStorageInMemory.DoIndexEvent(aDest: pointer; aRec: TSQLRecord; aIndex: integer); begin if aDest<>nil then PInteger(aDest)^ := aIndex; end; { TSQLRestStorageInMemoryExternal } constructor TSQLRestStorageInMemoryExternal.Create(aClass: TSQLRecordClass; aServer: TSQLRestServer; const aFileName: TFileName = ''; aBinaryFile: boolean=false); begin inherited Create(aClass,aServer,aFileName,aBinaryFile); fStorageLockShouldIncreaseOwnerInternalState := false; // done by overriden StorageLock() end; procedure TSQLRestStorageInMemoryExternal.StorageLock(WillModifyContent: boolean; const msg: RawUTF8); begin inherited StorageLock(WillModifyContent,msg); if WillModifyContent and (Owner<>nil) then Owner.FlushInternalDBCache; end; { TSQLRestStorageRemote } constructor TSQLRestStorageRemote.Create(aClass: TSQLRecordClass; aServer: TSQLRestServer; aRemoteRest: TSQLRest); begin if aRemoteRest=nil then raise EORMException.CreateUTF8('%.Create(nil)',[self]); inherited Create(aClass,aServer); fRemoteTableIndex := aRemoteRest.Model.GetTableIndexExisting(aClass); fRemoteRest := aRemoteRest; end; function TSQLRestStorageRemote.EngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; begin result := fRemoteRest.EngineAdd(fRemoteTableIndex,SentData); end; function TSQLRestStorageRemote.EngineDelete(TableModelIndex: integer; ID: TID): boolean; begin result := fRemoteRest.EngineDelete(fRemoteTableIndex,ID); end; function TSQLRestStorageRemote.EngineDeleteWhere(TableModelIndex: Integer; const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean; begin result := fRemoteRest.EngineDeleteWhere(fRemoteTableIndex,SQLWhere,IDs); end; function TSQLRestStorageRemote.EngineExecute(const aSQL: RawUTF8): boolean; begin result := fRemoteRest.EngineExecute(aSQL); end; function TSQLRestStorageRemote.EngineList(const SQL: RawUTF8; ForceAJAX: Boolean; ReturnedRowCount: PPtrInt): RawUTF8; begin result := fRemoteRest.EngineList(SQL,ForceAJAX,ReturnedRowCount); end; function TSQLRestStorageRemote.EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; begin result := fRemoteRest.EngineRetrieve(fRemoteTableIndex,ID); end; function TSQLRestStorageRemote.EngineRetrieveBlob(TableModelIndex: integer; aID: TID; BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean; begin if (self=nil) or (BlobField=nil) then result := false else result := fRemoteRest.EngineRetrieveBlob(fRemoteTableIndex,aID,BlobField,BlobData); end; function TSQLRestStorageRemote.EngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; begin result := fRemoteRest.EngineUpdate(fRemoteTableIndex,ID,SentData); end; function TSQLRestStorageRemote.EngineUpdateBlob(TableModelIndex: integer; aID: TID; BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean; begin if (self=nil) or (BlobField=nil) then result := false else result := fRemoteRest.EngineUpdateBlob(fRemoteTableIndex,aID,BlobField,BlobData); end; function TSQLRestStorageRemote.EngineUpdateField(TableModelIndex: integer; const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; begin result := fRemoteRest.EngineUpdateField(fRemoteTableIndex,SetFieldName,SetValue,WhereFieldName,WhereValue); end; function TSQLRestStorageRemote.EngineUpdateFieldIncrement(TableModelIndex: integer; ID: TID; const FieldName: RawUTF8; Increment: Int64): boolean; begin result := fRemoteRest.EngineUpdateFieldIncrement(fRemoteTableIndex,ID,FieldName,Increment); end; { TSQLRestStorageShard } const MIN_SHARD = 1000; constructor TSQLRestStorageShard.Create(aClass: TSQLRecordClass; aServer: TSQLRestServer; aShardRange: TID; aOptions: TSQLRestStorageShardOptions; aMaxShardCount: integer); var i,n: integer; begin if aShardRangenil) and (cardinal(aShardIndex)<=cardinal(fShardLast)) then begin FreeAndNil(fShards[aShardIndex]); fShardTableIndex[aShardIndex] := -1; end; finally StorageUnLock; end; end; procedure TSQLRestStorageShard.InternalAddNewShard; var rest: TSQLRest; i: integer; {$ifdef WITHLOG} log: ISynLog; // for Enter auto-leave to work with FPC {$endif} begin {$ifdef WITHLOG} log := fLogClass.Enter('InternalAddNewShard: #% for %',[fShardLast+1,fStoredClass],self); {$endif} rest := InitNewShard; if rest=nil then raise EORMException.CreateUTF8('%.InitNewShard(%) =nil',[self,fStoredClass]); inc(fShardNextID,fShardRange); SetLength(fShardTableIndex,fShardLast+1); fShardTableIndex[fShardLast] := rest.Model.GetTableIndexExisting(fStoredClass); if fShardLast>=fMaxShardCount then for i := 0 to fShardLast do if fShards[i]<>nil then begin RemoveShard(i); break; end; end; function TSQLRestStorageShard.ShardFromID(aID: TID; out aShardTableIndex: integer; out aShard: TSQLRest; aOccasion: TSQLOccasion; aShardIndex: PInteger): boolean; var ndx: cardinal; begin result := false; if aID<=0 then exit; case aOccasion of soUpdate: if ssoNoUpdate in fOptions then exit; soDelete: if ssoNoDelete in fOptions then exit; end; ndx := ((aID-1) div fShardRange)-fShardOffset; if (ndx<=cardinal(fShardLast)) and (fShards[ndx]<>nil) then begin case aOccasion of soUpdate: if (ssoNoUpdateButLastShard in fOptions) and (ndx<>cardinal(fShardLast)) then exit; soDelete: if (ssoNoDeleteButLastShard in fOptions) and (ndx<>cardinal(fShardLast)) then exit; end; aShard := fShards[ndx]; aShardTableIndex := fShardTableIndex[ndx]; if aShardIndex<>nil then aShardIndex^ := ndx; result := true; end; end; function TSQLRestStorageShard.EngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; var data: RawUTF8; i: Integer; begin if JSONGetID(pointer(SentData),result) then raise EORMException.CreateUTF8('%.EngineAdd(%) unexpected ID in %', [self,fStoredClass,SentData]); StorageLock(true,'EngineAdd'); try inc(fShardLastID); if fShardLastID>=fShardNextID then begin InternalAddNewShard; if fShardLastID>=fShardNextID then raise EORMException.CreateUTF8('%.EngineAdd(%) fShardNextID',[self,fStoredClass]); end; result := fShardLastID; i := PosExChar('{',SentData); if i=0 then FormatUTF8('{ID:%}',[result],data) else begin data := SentData; insert(FormatUTF8('ID:%,',[result]),data,i+1); end; if fShardBatch<>nil then InternalShardBatch(fShardLast).RawAdd(data) else begin if fShards[fShardLast].EngineAdd(fShardTableIndex[fShardLast],data)<>result then begin InternalLog('EngineAdd error %.ID=%',[fStoredClass,result],sllDB); result := 0; end; end; finally StorageUnLock; end; end; function TSQLRestStorageShard.EngineDelete(TableModelIndex: integer; ID: TID): boolean; var tableIndex,shardIndex: integer; rest: TSQLRest; begin StorageLock(true,'EngineDelete'); try if not ShardFromID(ID,tableIndex,rest,soDelete,@shardIndex) then result := false else if fShardBatch<>nil then result := InternalShardBatch(shardIndex).Delete(ID)>=0 else result := rest.EngineDelete(tableIndex,ID); finally StorageUnLock; end; end; function TSQLRestStorageShard.EngineDeleteWhere(TableModelIndex: integer; const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean; var i: integer; ndx: cardinal; id: array of TInt64DynArray; // IDs split per shard idn: TIntegerDynArray; sql: RawUTF8; begin result := false; if (IDs=nil) or (ssoNoDelete in fOptions) then exit; StorageLock(true,'EngineDeleteWhere'); try SetLength(id,fShardLast+1); SetLength(idn,fShardLast+1); for i := 0 to high(IDs) do begin ndx := ((IDs[i]-1) div fShardRange)-fShardOffset; // inlined ShardFromID() if (ndx>=cardinal(fShardLast)) or (fShards[ndx]=nil) then continue; if (ssoNoDeleteButLastShard in fOptions) and (ndx<>cardinal(fShardLast)) then continue; AddInt64(id[ndx],idn[ndx],IDs[i]); end; result := true; for i := 0 to high(id) do if id[i]<>nil then begin sql := Int64DynArrayToCSV(pointer(id[i]),idn[i],'ID in (',')'); if not fShards[i].EngineDeleteWhere(fShardTableIndex[i],sql,TIDDynArray(id[i])) then result := false; end; finally StorageUnLock; end; end; function TSQLRestStorageShard.EngineExecute(const aSQL: RawUTF8): boolean; begin StorageLock(false,'EngineExecute'); try if (fShardLast>=0) and not (ssoNoExecute in fOptions) then result := fShards[fShardLast].EngineExecute(aSQL) else result := false; finally StorageUnLock; end; end; function TSQLRestStorageShard.TableHasRows(Table: TSQLRecordClass): boolean; begin result := fShards<>nil; end; function TSQLRestStorageShard.TableRowCount(Table: TSQLRecordClass): Int64; var i: integer; begin result := 0; InternalLog('TableRowCount(%) may take a while',[fStoredClass],sllWarning); for i := 0 to high(fShards) do // no StorageLock protection to avoid blocking if fShards[i]<>nil then inc(result,fShards[i].TableRowCount(fStoredClass)); end; function TSQLRestStorageShard.EngineList(const SQL: RawUTF8; ForceAJAX: Boolean; ReturnedRowCount: PPtrInt): RawUTF8; var ResCount: PtrInt; begin result := ''; // indicates error occurred StorageLock(false,'EngineList'); try ResCount := 0; if IdemPropNameU(fBasicSQLCount,SQL) then begin FormatUTF8('[{"Count(*)":%}]'#$A,[TableRowCount(fStoredClass)],result); ResCount := 1; end else if IdemPropNameU(fBasicSQLHasRows[false],SQL) or IdemPropNameU(fBasicSQLHasRows[true],SQL) then if fShards<>nil then begin // return one row with fake ID=1 result := '[{"RowID":1}]'#$A; ResCount := 1; end else result := '{"fieldCount":1,"values":["RowID"]}'#$A else begin if (fShardLast>=0) and not (ssoNoList in fOptions) then result := fShards[fShardLast].EngineList(SQL,ForceAJAX,@ResCount); end; if ReturnedRowCount<>nil then ReturnedRowCount^ := ResCount; finally StorageUnLock; end; end; function TSQLRestStorageShard.EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; var tableIndex: integer; rest: TSQLRest; begin StorageLock(false,'EngineRetrieve'); try if not ShardFromID(ID,tableIndex,rest) then result := '' else result := rest.EngineRetrieve(tableIndex,ID); finally StorageUnLock; end; end; function TSQLRestStorageShard.EngineRetrieveBlob(TableModelIndex: integer; aID: TID; BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean; var tableIndex: integer; rest: TSQLRest; begin StorageLock(false,'EngineRetrieveBlob'); try if not ShardFromID(aID,tableIndex,rest) then result := false else result := rest.EngineRetrieveBlob(tableIndex,aID,BlobField,BlobData); finally StorageUnLock; end; end; function TSQLRestStorageShard.EngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; var tableIndex,shardIndex: integer; rest: TSQLRest; begin StorageLock(true,'EngineUpdate'); try if not ShardFromID(ID,tableIndex,rest,soUpdate,@shardIndex) then result := false else if fShardBatch<>nil then begin InternalShardBatch(shardIndex).RawUpdate(SentData,ID); result := true; end else result := rest.EngineUpdate(tableIndex,ID,SentData); finally StorageUnLock; end; end; function TSQLRestStorageShard.EngineUpdateBlob(TableModelIndex: integer; aID: TID; BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean; var tableIndex: integer; rest: TSQLRest; begin result := false; StorageLock(true,'EngineUpdateBlob'); try if ShardFromID(aID,tableIndex,rest,soUpdate) then result := rest.EngineUpdateBlob(tableIndex,aID,BlobField,BlobData); finally StorageUnLock; end; end; function TSQLRestStorageShard.EngineUpdateField(TableModelIndex: integer; const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; begin result := false; StorageLock(true,'EngineUpdateField'); try if not ((ssoNoUpdate in fOptions) or (ssoNoUpdateField in fOptions)) then result := fShards[fShardLast].EngineUpdateField(fShardTableIndex[fShardLast], SetFieldName,SetValue,WhereFieldName,WhereValue); finally StorageUnLock; end; end; function TSQLRestStorageShard.EngineUpdateFieldIncrement( TableModelIndex: integer; ID: TID; const FieldName: RawUTF8; Increment: Int64): boolean; var tableIndex: integer; rest: TSQLRest; begin result := false; StorageLock(true,'EngineUpdateFieldIncrement'); try if ShardFromID(ID,tableIndex,rest,soUpdate) then result := rest.EngineUpdateFieldIncrement(tableIndex,ID,FieldName,Increment); finally StorageUnLock; end; end; function TSQLRestStorageShard.InternalBatchStart(Method: TSQLURIMethod; BatchOptions: TSQLRestBatchOptions): boolean; begin result := false; if ssoNoBatch in fOptions then exit; StorageLock(true,'InternalBatchStart'); // protected by try..finally in TSQLRestServer.RunBatch try if fShardBatch<>nil then raise EORMException.CreateUTF8('%.InternalBatchStop should have been called',[self]); if fShardLast<0 then // new DB -> force fShardBatch<>nil SetLength(fShardBatch,1) else SetLength(fShardBatch,fShardLast+1); result := true; finally if not result then // release lock on error StorageUnLock; end; end; function TSQLRestStorageShard.InternalShardBatch(ShardIndex: integer): TSQLRestBatch; begin if cardinal(ShardIndex)>cardinal(fShardLast) then raise EORMException.CreateUTF8('%.InternalShardBatch(%)',[self,ShardIndex]); if fShardBatch=nil then raise EORMException.CreateUTF8('%.InternalBatchStart should have been called',[self]); if ShardIndex>=length(fShardBatch) then SetLength(fShardBatch,ShardIndex+1); // InitNewShard just called if fShardBatch[ShardIndex]=nil then if fShards[ShardIndex]<>nil then fShardBatch[ShardIndex] := TSQLRestBatch.Create( fShards[ShardIndex],fStoredClass,10000,[boExtendedJSON]) else raise EORMException.CreateUTF8('%.InternalShardBatch fShards[%]=nil',[self,ShardIndex]); result := fShardBatch[ShardIndex]; end; procedure TSQLRestStorageShard.InternalBatchStop; var i: integer; begin try for i := 0 to high(fShardBatch) do if fShardBatch[i]<>nil then if fShards[i].BatchSend(fShardBatch[i])<>HTTP_SUCCESS then InternalLog('InternalBatchStop(%): %.BatchSend failed for shard #%', [fStoredClass,fShards[i].ClassType,i],sllWarning); finally ObjArrayClear(fShardBatch); StorageUnLock; end; end; { TSQLRestStorage } constructor TSQLRestStorage.Create(aClass: TSQLRecordClass; aServer: TSQLRestServer); begin inherited Create(nil); if aClass=nil then raise EBusinessLayerException.CreateUTF8('%.Create(aClass=nil)',[self]); InitializeCriticalSection(fStorageCriticalSection); fStoredClass := aClass; fStoredClassRecordProps := aClass.RecordProps; if aServer<>nil then begin fOwner := aServer; fModel := aServer.Model; end else begin // fallback to an owned model instance fModel := TSQLModel.Create([aClass]); fModel.Owner := self; end; fStoredClassProps := fModel.Props[aClass]; fStoredClassMapping := @fStoredClassProps.ExternalDB; fIsUnique := fStoredClassRecordProps.IsUniqueFieldsBits; fBasicSQLCount := 'SELECT COUNT(*) FROM '+fStoredClassRecordProps.SQLTableName; fBasicSQLHasRows[false] := 'SELECT RowID FROM '+fStoredClassRecordProps.SQLTableName+' LIMIT 1'; fBasicSQLHasRows[true] := fBasicSQLHasRows[false]; system.delete(fBasicSQLHasRows[true],8,3); end; destructor TSQLRestStorage.Destroy; begin inherited; if fStorageCriticalSectionCount<>0 then raise EORMException.CreateUTF8('%.Destroy with CS=%',[self,fStorageCriticalSectionCount]); DeleteCriticalSection(fStorageCriticalSection); if fStorageVirtual<>nil then begin // no GPF e.g. if DB release after server fStorageVirtual.fStatic := nil; fStorageVirtual.fStaticStorage := nil; end; end; procedure TSQLRestStorage.BeginCurrentThread(Sender: TThread); begin // called by TSQLRestServer.BeginCurrentThread // nothing to do in this basic REST static class end; procedure TSQLRestStorage.EndCurrentThread(Sender: TThread); begin // called by TSQLRestServer.EndCurrentThread // nothing to do in this basic REST static class end; function TSQLRestStorage.ServiceContainer: TServiceContainer; begin result := nil; end; function TSQLRestStorage.CreateSQLMultiIndex(Table: TSQLRecordClass; const FieldNames: array of RawUTF8; Unique: boolean; IndexName: RawUTF8): boolean; begin result := false; // not implemented in this basic REST static class end; function TSQLRestStorage.SearchField(const FieldName: RawUTF8; FieldValue: Int64; out ResultID: TIDDynArray): boolean; begin result := SearchField(FieldName,Int64ToUTF8(FieldValue),ResultID); end; function TSQLRestStorage.RecordCanBeUpdated(Table: TSQLRecordClass; ID: TID; Action: TSQLEvent; ErrorMsg: PRawUTF8 = nil): boolean; begin result := ((Owner=nil) or Owner.RecordCanBeUpdated(Table,ID,Action,ErrorMsg)); end; function TSQLRestStorage.RefreshedAndModified: boolean; begin result := false; // no refresh necessary with "normal" static tables end; procedure TSQLRestStorage.StorageLock(WillModifyContent: boolean; const msg: RawUTF8); begin if fStorageLockLogTrace or (fStorageCriticalSectionCount>1) then InternalLog('StorageLock % [%] %',[fStoredClass,msg,fStorageCriticalSectionCount]); EnterCriticalSection(fStorageCriticalSection); inc(fStorageCriticalSectionCount); if WillModifyContent and fStorageLockShouldIncreaseOwnerInternalState and (Owner<>nil) then inc(Owner.InternalState); end; procedure TSQLRestStorage.StorageUnLock; begin dec(fStorageCriticalSectionCount); if fStorageLockLogTrace then InternalLog('StorageUnlock % %',[fStoredClass,fStorageCriticalSectionCount]); if fStorageCriticalSectionCount<0 then raise EORMException.CreateUTF8('%.StorageUnLock with CS=%', [self,fStorageCriticalSectionCount]); LeaveCriticalSection(fStorageCriticalSection); end; function TSQLRestStorage.GetCurrentSessionUserID: TID; begin if fOwner=nil then result := 0 else result := fOwner.GetCurrentSessionUserID; end; procedure TSQLRestStorage.RecordVersionFieldHandle(Occasion: TSQLOccasion; var Decoder: TJSONObjectDecoder); begin if fStoredClassRecordProps.RecordVersionField=nil then exit; if Owner=nil then raise EORMException.CreateUTF8('Owner=nil for %.%: TRecordVersion', [fStoredClass,fStoredClassRecordProps.RecordVersionField.Name]); Owner.InternalRecordVersionHandle(Occasion,fStoredClassProps.TableIndex, Decoder,fStoredClassRecordProps.RecordVersionField); end; function TSQLRestStorage.UnLock(Table: TSQLRecordClass; aID: TID): boolean; begin result := Model.UnLock(Table,aID); end; function TSQLRestStorage.AdaptSQLForEngineList(var SQL: RawUTF8): boolean; begin if fStoredClassProps=nil then result := false else begin result := IdemPropNameU(fStoredClassProps.SQL.SelectAllWithRowID,SQL); if result then SQL := fStoredClassProps.SQL.SelectAllWithID else result := IdemPropNameU(fStoredClassProps.SQL.SelectAllWithID,SQL); end; end; function TSQLRestStorage.GetStoredClassName: RawUTF8; begin if self=nil then result := '' else ToText(fStoredClass,result); end; { TSQLRestServerFullMemory } constructor TSQLRestServerFullMemory.Create(aModel: TSQLModel; aHandleUserAuthentication: boolean); var t: integer; begin inherited Create(aModel,aHandleUserAuthentication); fStaticDataCount := length(fModel.Tables); SetLength(fStorage,fStaticDataCount); for t := 0 to fStaticDataCount-1 do begin fStorage[t] := (StaticDataCreate(fModel.Tables[t]) as TSQLRestStorageInMemory); fStorage[t].fStorageLockShouldIncreaseOwnerInternalState := true; end; end; constructor TSQLRestServerFullMemory.Create(aModel: TSQLModel; const aFileName: TFileName; aBinaryFile, aHandleUserAuthentication: boolean); begin fFileName := aFileName; fBinaryFile := aBinaryFile; Create(aModel,aHandleUserAuthentication); LoadFromFile; CreateMissingTables(0,[]); end; constructor TSQLRestServerFullMemory.CreateWithOwnedAuthenticatedModel( const Tables: array of TSQLRecordClass; const aUserName,aHashedPassword: RawUTF8; aRoot: RawUTF8); var User: TSQLAuthUser; begin if aRoot='' then aRoot := 'root'; if aUserName='' then CreateWithOwnModel(Tables,false,aRoot) else begin CreateWithOwnModel(Tables,true,aRoot); CreateMissingTables(0,[itoNoAutoCreateUsers]); User := TSQLAuthUser.Create; try User.LogonName := aUserName; User.PasswordHashHexa := aHashedPassword; User.GroupRights := TSQLAuthGroup(2); // member of 'Supervisor' group Add(User,true); finally User.Free; end; end; end; constructor TSQLRestServerFullMemory.RegisteredClassCreateFrom(aModel: TSQLModel; aServerHandleAuthentication: boolean; aDefinition: TSynConnectionDefinition); begin fFileName := UTF8ToString(aDefinition.ServerName); fBinaryFile := aDefinition.DatabaseName<>''; // DefinitionTo() set 'binary' Create(aModel,aServerHandleAuthentication); LoadFromFile; end; procedure TSQLRestServerFullMemory.DefinitionTo(Definition: TSynConnectionDefinition); begin if Definition=nil then exit; inherited; // set Kind Definition.ServerName := StringToUTF8(fFileName); if fBinaryFile then Definition.DatabaseName := 'binary'; end; procedure TSQLRestServerFullMemory.CreateMissingTables(user_version: cardinal=0; Options: TSQLInitializeTableOptions=[]); var t: integer; begin inherited; // create any missing static instances if integer(fStaticDataCount)<>length(fModel.Tables) then begin SetLength(fStorage,length(fModel.Tables)); for t := fStaticDataCount to high(fModel.Tables) do begin fStorage[t] := (StaticDataCreate(fModel.Tables[t]) as TSQLRestStorageInMemory); fStorage[t].fStorageLockShouldIncreaseOwnerInternalState := true; end; fStaticDataCount := length(fModel.Tables); end; // initialize new tables for t := 0 to fStaticDataCount-1 do with TSQLRestStorageInMemory(fStaticData[t]) do if Count=0 then // emulates TSQLRestServerDB.CreateMissingTables StoredClass.InitializeTable(Self,'',Options); end; destructor TSQLRestServerFullMemory.Destroy; begin UpdateToFile; inherited; end; procedure TSQLRestServerFullMemory.DropDatabase; var t: integer; begin for t := 0 to fStaticDataCount-1 do TSQLRestStorageInMemory(fStaticData[t]).DropValues; end; procedure TSQLRestServerFullMemory.LoadFromStream(aStream: TStream); var JSON: RawUTF8; P, TableName, Data: PUTF8Char; t: integer; wasString: boolean; begin if aStream=nil then exit; if fBinaryFile then begin if ReadStringFromStream(aStream)=ToText(ClassType)+'00' then repeat t := Model.GetTableIndex(ReadStringFromStream(aStream)); until (t<0) or not TSQLRestStorageInMemory(fStaticData[t]).LoadFromBinary(aStream); end else begin // [{"AuthUser":[{....},{...}]},{"AuthGroup":[{...},{...}]}] JSON := StreamToRawByteString(aStream); // assume UTF-8 content if JSON='' then exit; P := pointer(JSON); while (P^<>'[') do if P^=#0 then exit else inc(P); inc(P); repeat while (P^<>']') and (P^<>'{') do if P^=#0 then exit else inc(P); if P^=']' then break else inc(P); TableName := GetJSONField(P,P,@wasString); if not wasString or (P=nil) then exit; t := Model.GetTableIndexPtr(TableName); if t<0 then exit; Data := P; P := GotoNextJSONObjectOrArray(P); if P=nil then break else TSQLRestStorageInMemory(fStaticData[t]).LoadFromJSON(Data,P-Data); until false; end; end; procedure TSQLRestServerFullMemory.LoadFromFile; var S: THandleStream; begin if (fFileName='') or not FileExists(fFileName) then exit; DropDatabase; S := FileStreamSequentialRead(FileName); try LoadFromStream(S); finally S.Free; end; end; procedure TSQLRestServerFullMemory.UpdateToFile; const CHARS: array[0..6] of AnsiChar = '[{":,}]'; var S: TFileStream; // 0123456 t: integer; Modified: boolean; timer: TPrecisionTimer; begin if (self=nil) or (FileName='') then exit; Modified := false; for t := 0 to fStaticDataCount-1 do if TSQLRestStorageInMemory(fStaticData[t]).Modified then begin Modified := true; break; end; if not Modified then exit; timer.Start; S := TFileStream.Create(FileName,fmCreate); try if fBinaryFile then begin WriteStringToStream(S,ToText(ClassType)+'00'); for t := 0 to fStaticDataCount-1 do with TSQLRestStorageInMemory(fStaticData[t]) do begin WriteStringToStream(S,fStoredClassRecordProps.SQLTableName); SaveToBinary(S); end; end else begin S.WriteBuffer(CHARS[0],1); for t := 0 to fStaticDataCount-1 do with TSQLRestStorageInMemory(fStaticData[t]) do begin S.WriteBuffer(CHARS[1],2); with fStoredClassRecordProps do S.WriteBuffer(pointer(SQLTableName)^,length(SQLTableName)); S.WriteBuffer(CHARS[2],2); SaveToJSON(S,true); S.WriteBuffer(CHARS[5],1); if t=cardinal(length(fStorage)) then result := nil else result := fStorage[i]; end; // Engine*() methods will have direct access to static fStorage[]) function TSQLRestServerFullMemory.EngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; begin result := fStorage[TableModelIndex].EngineAdd(TableModelIndex,SentData); inc(InternalState); end; function TSQLRestServerFullMemory.EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; begin result := fStorage[TableModelIndex].EngineRetrieve(TableModelIndex,ID); end; function TSQLRestServerFullMemory.EngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; begin result := fStorage[TableModelIndex].EngineUpdate(TableModelIndex,ID,SentData); end; function TSQLRestServerFullMemory.EngineDelete(TableModelIndex: integer; ID: TID): boolean; begin result := fStorage[TableModelIndex].EngineDelete(TableModelIndex,ID); end; function TSQLRestServerFullMemory.EngineDeleteWhere(TableModelIndex: integer; const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean; begin result := fStorage[TableModelIndex].EngineDeleteWhere(TableModelIndex,SQLWhere,IDs); end; function TSQLRestServerFullMemory.EngineRetrieveBlob(TableModelIndex: integer; aID: TID; BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean; begin result := fStorage[TableModelIndex].EngineRetrieveBlob(TableModelIndex,aID,BlobField,BlobData); end; function TSQLRestServerFullMemory.EngineUpdateBlob(TableModelIndex: integer; aID: TID; BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean; begin result := fStorage[TableModelIndex].EngineUpdateBlob(TableModelIndex,aID,BlobField,BlobData); end; function TSQLRestServerFullMemory.EngineUpdateField(TableModelIndex: integer; const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; begin result := fStorage[TableModelIndex].EngineUpdateField(TableModelIndex, SetFieldName,SetValue,WhereFieldName,WhereValue); end; function TSQLRestServerFullMemory.EngineUpdateFieldIncrement(TableModelIndex: integer; ID: TID; const FieldName: RawUTF8; Increment: Int64): boolean; begin result := fStorage[TableModelIndex].EngineUpdateFieldIncrement(TableModelIndex, ID,FieldName,Increment); end; // MainEngine*() methods should return error (only access via static fStorage[]) function TSQLRestServerFullMemory.MainEngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; begin result := 0; end; function TSQLRestServerFullMemory.MainEngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; begin result := ''; end; function TSQLRestServerFullMemory.MainEngineList(const SQL: RawUTF8; ForceAJAX: Boolean; ReturnedRowCount: PPtrInt): RawUTF8; begin result := ''; end; function TSQLRestServerFullMemory.MainEngineUpdate(TableModelIndex: integer; aID: TID; const SentData: RawUTF8): boolean; begin result := false; end; function TSQLRestServerFullMemory.MainEngineDelete(TableModelIndex: integer; ID: TID): boolean; begin result := false; end; function TSQLRestServerFullMemory.MainEngineDeleteWhere(TableModelIndex: integer; const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean; begin result := false; end; function TSQLRestServerFullMemory.MainEngineRetrieveBlob(TableModelIndex: integer; aID: TID; BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean; begin result := false; end; function TSQLRestServerFullMemory.MainEngineUpdateBlob(TableModelIndex: integer; aID: TID; BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean; begin result := false; end; function TSQLRestServerFullMemory.MainEngineUpdateField(TableModelIndex: integer; const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; begin result := false; end; function TSQLRestServerFullMemory.MainEngineUpdateFieldIncrement( TableModelIndex: integer; ID: TID; const FieldName: RawUTF8; Increment: Int64): boolean; begin result := false; end; { TSQLRestServerRemoteDB } constructor TSQLRestServerRemoteDB.Create(aRemoteRest: TSQLRest; aHandleUserAuthentication: boolean); var i: integer; begin if aRemoteRest=nil then raise EORMException.CreateUTF8('%.Create(nil)',[self]); inherited Create(aRemoteRest.Model,aHandleUserAuthentication); SetLength(fRemoteTableIndex,Model.TablesMax+1); for i := 0 to Model.TablesMax do fRemoteTableIndex[i] := aRemoteRest.Model.GetTableIndexExisting(Model.Tables[i]); fRemoteRest := aRemoteRest; end; function TSQLRestServerRemoteDB.EngineAdd(TableModelIndex: integer; const SentData: RawUTF8): TID; begin result := fRemoteRest.EngineAdd(fRemoteTableIndex[TableModelIndex],SentData); end; function TSQLRestServerRemoteDB.EngineDelete(TableModelIndex: integer; ID: TID): boolean; begin result := fRemoteRest.EngineDelete(fRemoteTableIndex[TableModelIndex],ID); end; function TSQLRestServerRemoteDB.EngineDeleteWhere(TableModelIndex: Integer; const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean; begin result := fRemoteRest.EngineDeleteWhere(fRemoteTableIndex[TableModelIndex],SQLWhere,IDs); end; function TSQLRestServerRemoteDB.EngineExecute(const aSQL: RawUTF8): boolean; begin result := fRemoteRest.EngineExecute(aSQL); end; function TSQLRestServerRemoteDB.EngineList(const SQL: RawUTF8; ForceAJAX: Boolean; ReturnedRowCount: PPtrInt): RawUTF8; begin result := fRemoteRest.EngineList(SQL,ForceAJAX,ReturnedRowCount); end; function TSQLRestServerRemoteDB.EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; begin result := fRemoteRest.EngineRetrieve(fRemoteTableIndex[TableModelIndex],ID); end; function TSQLRestServerRemoteDB.EngineRetrieveBlob(TableModelIndex: integer; aID: TID; BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean; begin if (self=nil) or (BlobField=nil) then result := false else result := fRemoteRest.EngineRetrieveBlob(fRemoteTableIndex[TableModelIndex],aID,BlobField,BlobData); end; function TSQLRestServerRemoteDB.EngineUpdate(TableModelIndex: integer; ID: TID; const SentData: RawUTF8): boolean; begin result := fRemoteRest.EngineUpdate(fRemoteTableIndex[TableModelIndex],ID,SentData); end; function TSQLRestServerRemoteDB.EngineUpdateBlob(TableModelIndex: integer; aID: TID; BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean; begin if (self=nil) or (BlobField=nil) then result := false else result := fRemoteRest.EngineUpdateBlob(fRemoteTableIndex[TableModelIndex],aID,BlobField,BlobData); end; function TSQLRestServerRemoteDB.EngineUpdateField(TableModelIndex: integer; const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean; begin result := fRemoteRest.EngineUpdateField(fRemoteTableIndex[TableModelIndex],SetFieldName,SetValue,WhereFieldName,WhereValue); end; function TSQLRestServerRemoteDB.EngineUpdateFieldIncrement(TableModelIndex: integer; ID: TID; const FieldName: RawUTF8; Increment: Int64): boolean; begin result := fRemoteRest.EngineUpdateFieldIncrement(fRemoteTableIndex[TableModelIndex], ID,FieldName,Increment); end; function TSQLRestServerRemoteDB.AfterDeleteForceCoherency(TableIndex: integer; aID: TID): boolean; begin result := true; // coherency will be performed on the server side end; { TSQLRestClient } function TSQLRestClient.GetForceBlobTransfert: Boolean; var i: integer; begin result := false; if fForceBlobTransfert=nil then exit; for i := 0 to fModel.fTablesMax do if not fForceBlobTransfert[i] then exit; result := true; // all Tables have BLOB transfert set end; procedure TSQLRestClient.SetForceBlobTransfert(Value: boolean); var i: integer; begin Finalize(fForceBlobTransfert); if Value then begin SetLength(fForceBlobTransfert,fModel.fTablesMax+1); for i := 0 to fModel.fTablesMax do fForceBlobTransfert[i] := true; end; end; function TSQLRestClient.GetForceBlobTransfertTable(aTable: TSQLRecordClass): Boolean; begin if fForceBlobTransfert=nil then result := false else result := fForceBlobTransfert[fModel.GetTableIndexExisting(aTable)]; end; procedure TSQLRestClient.SetForceBlobTransfertTable(aTable: TSQLRecordClass; aValue: Boolean); var i: integer; begin i := fModel.GetTableIndexExisting(aTable); if fForceBlobTransfert=nil then if aValue then SetLength(fForceBlobTransfert,fModel.fTablesMax+1) else exit; // nothing to set fForceBlobTransfert[i] := aValue; end; function TSQLRestClient.InternalAdd(Value: TSQLRecord; SendData: boolean; CustomFields: PSQLFieldBits; ForceID, DoNotAutoComputeFields: boolean): TID; begin result := inherited InternalAdd(Value,SendData,CustomFields,ForceID,DoNotAutoComputeFields); if (result>0) and (fForceBlobTransfert<>nil) and fForceBlobTransfert[fModel.GetTableIndexExisting(PSQLRecordClass(Value)^)] then UpdateBlobFields(Value); end; function TSQLRestClient.EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; var dummy: cardinal; begin if not ClientRetrieve(TableModelIndex,ID,false,dummy,result) then result := ''; end; function TSQLRestClient.Retrieve(aID: TID; Value: TSQLRecord; ForUpdate: boolean=false): boolean; var Resp: RawUTF8; TableIndex: integer; begin result := false; if (self=nil) or (aID<=0) or (Value=nil) then exit; TableIndex := Model.GetTableIndexExisting(PSQLRecordClass(Value)^); if ForUpdate then begin if not Model.Lock(TableIndex,aID) then exit; // error marking as locked by the client end else begin Resp := fCache.Retrieve(TableIndex,aID); if Resp<>'' then begin Value.FillFrom(Resp); Value.fID := aID; // JSON object may not contain the ID result := true; exit; // fast retrieved from internal Client cache (BLOBs ignored) end; end; try if ClientRetrieve(TableIndex,aID,ForUpdate,Value.fInternalState,Resp) then begin if not ForUpdate then fCache.Notify(TableIndex,aID,Resp,soSelect); Value.FillFrom(Resp); Value.fID := aID; // JSON object may not contain the ID if (fForceBlobTransfert<>nil) and fForceBlobTransfert[TableIndex] then result := RetrieveBlobFields(Value) else result := true; ForUpdate := false; // any exception shall unlock the record end; finally if ForUpdate then Model.UnLock(TableIndex,aID); end; end; function TSQLRestClient.Update(Value: TSQLRecord; const CustomFields: TSQLFieldBits; DoNotAutoComputeFields: boolean): boolean; begin result := BeforeUpdateEvent(Value) and inherited Update(Value,CustomFields,DoNotAutoComputeFields); if result then begin if (fForceBlobTransfert<>nil) and IsZero(CustomFields) and fForceBlobTransfert[Model.GetTableIndexExisting(PSQLRecordClass(Value)^)] then result := UpdateBlobFields(Value); if result and assigned(OnRecordUpdate) then OnRecordUpdate(Value); end; end; function TSQLRestClient.BeforeUpdateEvent(Value: TSQLRecord): Boolean; begin Result := true; // by default, just allow the update to proceed end; function TSQLRestClient.Refresh(aID: TID; Value: TSQLRecord; var Refreshed: boolean): boolean; var Resp, Original: RawUTF8; begin result := false; if (aID>0) and (self<>nil) and (Value<>nil) then if ClientRetrieve(Model.GetTableIndexExisting(PSQLRecordClass(Value)^),aID,False, Value.fInternalState,Resp) then begin Original := Value.GetJSONValues(IsNotAjaxJSON(pointer(Resp)),true,soSelect); Resp := trim(Resp); if (Resp<>'') and (Resp[1]='[') then // '[{....}]' -> '{...}' Resp := copy(Resp,2,length(Resp)-2); if Original<>Resp then begin // did the content really change? Refreshed := true; Value.FillFrom(Resp); end; result := true; end; end; procedure TSQLRestClient.Commit(SessionID: cardinal; RaiseException: boolean); begin inherited Commit(SessionID,RaiseException); end; function TSQLRestClient.TransactionBegin(aTable: TSQLRecordClass; SessionID: cardinal): boolean; begin result := inherited TransactionBegin(aTable,SessionID); end; procedure TSQLRestClient.RollBack(SessionID: cardinal); begin inherited; end; function TSQLRestClient.ListFmt(const Tables: array of TSQLRecordClass; const SQLSelect: RawUTF8; const SQLWhereFormat: RawUTF8; const Args: array of const): TSQLTableJSON; begin result := List(Tables,SQLSelect,FormatUTF8(SQLWhereFormat,Args)); end; function TSQLRestClient.ListFmt(const Tables: array of TSQLRecordClass; const SQLSelect: RawUTF8; const SQLWhereFormat: RawUTF8; const Args, Bounds: array of const): TSQLTableJSON; begin result := List(Tables,SQLSelect,FormatUTF8(SQLWhereFormat,Args,Bounds)); end; function TSQLRestClient.ServiceContainer: TServiceContainer; begin if fServices=nil then fServices := TServiceContainerClient.Create(self); result := fServices; end; { TSQLRecordLog } destructor TSQLRecordLog.Destroy; begin fLogTableWriter.Free; fLogTableStorage.Free; inherited; end; constructor TSQLRecordLog.CreateFrom(OneLog: TSQLRecord; const aJSON: RawUTF8); var L,FieldCount: integer; P: PUTF8Char; begin inherited Create; L := length(aJSON); if (L<10) or (Copy(aJSON,L-1,2)<>']}') then exit; fLogTableStorage := THeapMemoryStream.Create; fLogTableWriter := OneLog.RecordProps.CreateJSONWriter( fLogTableStorage,false,true,ALL_FIELDS,{knownrows=}0); fLogTableWriter.FlushToStream; P := pointer(aJSON); if not CompareMem(fLogTableStorage.Memory,P,fLogTableStorage.Position) or not IsNotExpandedBuffer(P,P+length(aJSON),FieldCount,fLogTableRowCount) or (fLogTableRowCount<0) then begin // field format changed or invalid FreeAndNil(fLogTableWriter); FreeAndNil(fLogTableStorage); exit; end; fLogTableStorage.Seek(0,soFromBeginning); fLogTableStorage.WriteBuffer(Pointer(aJSON)^,L-2); end; procedure TSQLRecordLog.Log(OneLog: TSQLRecord); begin if OneLog=nil then exit; // simulate adding a row: compute new ID inc(OneLog.fID); // adding a row, in not-expanded format if not Assigned(fLogTableStorage) then begin fLogTableStorage := THeapMemoryStream.Create; fLogTableWriter := OneLog.RecordProps.CreateJSONWriter( fLogTableStorage,false,true,ALL_FIELDS,{knownrows=}0); fLogTableRowCount := 1; end else begin fLogTableWriter.Add(','); if (fMaxLogTableRowCount<>0) and (fLogTableRowCount>=fMaxLogTableRowCount) then fLogTableWriter.TrimFirstRow else inc(fLogTableRowCount); end; OneLog.GetJSONValues(fLogTableWriter) end; function TSQLRecordLog.LogCurrentPosition: integer; begin if not Assigned(fLogTableStorage) then result := 0 else begin fLogTableWriter.FlushToStream; result := fLogTableStorage.Position; end; end; function TSQLRecordLog.LogTableJSON: RawUTF8; begin result := LogTableJSONFrom(0); end; function TSQLRecordLog.LogTableJSONFrom(StartPosition: integer): RawUTF8; var JSONStart: RawUTF8; Data: PAnsiChar; begin if not Assigned(fLogTableStorage) or (StartPosition<0) then result := '' else begin fLogTableWriter.FlushToStream; Data := fLogTableStorage.Memory; FastSetString(result,Data+StartPosition,fLogTableStorage.Position-StartPosition); // format as valid not expanded JSON table content: if StartPosition<>0 then begin FastSetString(JSONStart,Data,fLogTableWriter.StartDataPosition); result := JSONStart+result; end; result := result+']}'; end; end; { RecordRef } function RecordReference(Model: TSQLModel; aTable: TSQLRecordClass; aID: TID): TRecordReference; begin if aID=0 then result := 0 else begin result := Model.GetTableIndexExisting(aTable); if result>63 then // TRecordReference handle up to 64=1 shl 6 tables result := 0 else inc(result,aID shl 6); // 64=1 shl 6 end; end; function RecordReference(aTableIndex: cardinal; aID: TID): TRecordReference; begin if (aID=0) or (aTableIndex>63) then result := 0 else result := aTableIndex+aID shl 6; end; procedure RecordRefToID(var aArray: TInt64DynArray); var i: Integer; begin for i := 0 to high(aArray) do aArray[i] := aArray[i] shr 6; end; procedure RecordRef.From(Model: TSQLModel; aTable: TSQLRecordClass; aID: TID); begin Value := Model.GetTableIndexExisting(aTable); if Value>63 then // TRecordReference handle up to 64=1 shl 6 tables Value := 0 else inc(Value,aID shl 6); // 64=1 shl 6 end; function RecordRef.ID: TID; begin result := Value shr 6; // 64=1 shl 6 end; function RecordRef.Table(Model: TSQLModel): TSQLRecordClass; var V: integer; begin if (Model=nil) or (Value=0) then result := nil else begin V := Value and 63; if V>Model.TablesMax then result := nil else result := Model.Tables[V]; end; end; function RecordRef.TableIndex: integer; begin result := Value and 63; end; function RecordRef.Text(Model: TSQLModel): RawUTF8; var aTable: TSQLRecordClass; begin if ((Value shr 6)=0) then // Value=0 or no valid ID result := '' else begin aTable := Table(Model); if aTable=nil then result := '' else result := Model.TableProps[Value and 63].Props.SQLTableName+ ' '+Int64ToUtf8(Value shr 6); end; end; function RecordRef.Text(Rest: TSQLRest): RawUTF8; var T: TSQLRecordClass; aID: TID; begin result := ''; if ((Value shr 6)=0) or (Rest=nil) then exit; T := Table(Rest.Model); if T=nil then exit; aID := ID; with Rest.Model.TableProps[Value and 63].Props do if aID<=0 then result := SQLTableName else begin result := Rest.MainFieldValue(T,aID,true); if result='' then FormatUTF8('% %',[SQLTableName,aID],result) else result := FormatUTF8('% "%"',[SQLTableName,result]); end; end; { TSQLLocks } function TSQLLocks.isLocked(aID: TID): boolean; begin result := (@self<>nil) and (Count<>0) and (aID<>0) and Int64ScanExists(pointer(IDs),Count,aID); end; function TSQLLocks.Lock(aID: TID): boolean; var P: PInt64; begin if (@self=nil) or (aID=0) then // void or full result := false else begin P := Int64Scan(pointer(IDs),Count,aID); if P<>nil then // already locked result := false else begin // add to ID[] and Ticks[] P := Int64Scan(pointer(IDs),Count,0); if P=nil then begin // no free entry -> add at the end if Count>=length(IDs) then begin SetLength(IDs,Count+512); SetLength(Ticks64s,Count+512); end; IDs[Count] := aID; Ticks64s[Count] := GetTickCount64; inc(Count); end else begin // store at free entry P^ := aID; Ticks64s[(PtrUInt(P)-PtrUInt(IDs))shr 3] := GetTickCount64; end; result := true; end; end; end; procedure TSQLLocks.PurgeOlderThan(MinutesFromNow: cardinal); var LastOK64: Int64; i, LastEntry: integer; begin if (@self=nil) or (Count=0) then exit; // nothing to purge LastOK64 := GetTickCount64-MinutesFromNow*(1000*60); // GetTickCount64() unit is ms LastEntry := -1; for i := 0 to Count-1 do if IDs[i]<>0 then if Ticks64s[i]=PtrUInt(Count-1)) then dec(Count); // freed last entry -> decrease list length result := true; end; end; end; procedure CopyObject(aFrom, aTo: TObject); var P,P2: PPropInfo; i: integer; Cfrom,Cto,C: TClass; begin if (aFrom=nil) or (aTo=nil) then exit; {$ifndef LVCL} if aFrom.InheritsFrom(TCollection) then begin CopyCollection(TCollection(aFrom),TCollection(aTo)); exit; end; {$endif} if aFrom.InheritsFrom(TStrings) then begin if aTo.InheritsFrom(TStrings) then CopyStrings(TStrings(aFrom),TStrings(aTo)); exit; end; Cfrom := aFrom.ClassType; Cto := aTo.ClassType; if Cto.InheritsFrom(Cfrom) then // C = most common hierarchy level C := Cfrom else if Cfrom.InheritsFrom(Cto) then C := Cto else begin repeat // no common inheritance -> slower lookup by property name for i := 1 to InternalClassPropInfo(Cfrom,P) do begin P2 := ClassFieldPropWithParents(Cto,P^.Name); if P2<>nil then P^.CopyValue(aFrom,aTo,P2); P := P^.Next; end; Cfrom := GetClassParent(Cfrom); until Cfrom=TObject; exit; end; repeat // fast process of inherited PPropInfo for i := 1 to InternalClassPropInfo(C,P) do begin P^.CopyValue(aFrom,aTo); P := P^.Next; end; C := GetClassParent(C); until C=TObject; end; function CopyObject(aFrom: TObject): TObject; var DInst: TClassInstance; begin if aFrom=nil then begin result := nil; exit; end; DInst.Init(aFrom.ClassType); result := DInst.CreateNew; try CopyObject(aFrom,result); except FreeAndNil(result); // avoid memory leak if error during new instance copy end; end; {$ifndef LVCL} procedure CopyCollection(Source, Dest: TCollection); var i: integer; begin if (Source=nil) or (Dest=nil) or (Source.ClassType<>Dest.ClassType) then exit; Dest.BeginUpdate; try Dest.Clear; for i := 0 to Source.Count-1 do CopyObject(Source.Items[i],Dest.Add); // Assign() fails for most objects finally Dest.EndUpdate; end; end; {$endif} procedure CopyStrings(Source, Dest: TStrings); begin if (Source=nil) or (Dest=nil) then exit; {$ifdef LVCL} Dest.Clear; Dest.AddStrings(Source); {$else} Dest.Assign(Source); {$endif} end; procedure WriteObject(Value: TObject; var IniContent: RawUTF8; const Section: RawUTF8; const SubCompName: RawUTF8); var CT: TClass; P: PPropInfo; i, V: integer; Obj: TObject; tmp,field: RawUTF8; begin if Value=nil then exit; CT := Value.ClassType; repeat for i := 1 to InternalClassPropInfo(CT,P) do begin field := SubCompName+ToUTF8(P^.Name); case P^.PropType^.Kind of tkInt64{$ifdef FPC}, tkQWord{$endif}: UpdateIniEntry(IniContent,Section,field, Int64ToUtf8(P^.GetInt64Prop(Value))); {$ifdef FPC}tkBool,{$endif} tkEnumeration, tkSet, tkInteger: begin V := P^.GetOrdProp(Value); //if V<>P^.Default then NO DEFAULT: update INI -> must override previous UpdateIniEntry(IniContent,Section,field, Int32ToUtf8(V)); end; {$ifdef HASVARUSTRING}tkUString,{$endif} {$ifdef FPC}tkLStringOld,{$endif} tkLString, tkWString: begin P^.GetLongStrValue(Value,tmp); UpdateIniEntry(IniContent,Section,field,tmp); end; tkClass: if Section='' then begin // recursive call works only as plain object Obj := P^.GetObjProp(Value); if (Obj<>nil) and Obj.InheritsFrom(TPersistent) then WriteObject(Value,IniContent,Section,field+'.'); end; // tkString (shortstring) and tkInterface are not handled end; P := P^.Next; end; CT := GetClassParent(CT); until CT=TObject; end; function WriteObject(Value: TObject): RawUTF8; var temp: TTextWriterStackBuffer; begin if Value<>nil then with TIniWriter.CreateOwnedStream(temp) do try WriteObject(Value,''); SetText(result); finally Free; end else result := ''; end; function ObjectEquals(Value1,Value2: TObject; ignoreGetterFields: boolean): boolean; var i: integer; C1,C2: TClass; P1,P2: PPropInfo; begin if (Value1=nil) or (Value2=nil) or (Value1=Value2) then result := Value1=Value2 else if Value1.InheritsFrom(TSQLRecord) and Value2.InheritsFrom(TSQLRecord) then result := TSQLRecord(Value1).SameValues(TSQLRecord(Value2)) else begin result := false; C1 := Value1.ClassType; C2 := Value2.ClassType; repeat for i := 1 to InternalClassPropInfo(C1,P1) do begin if not ignoreGetterFields or P1^.GetterIsField then if C2<>C1 then begin P2 := ClassFieldPropWithParents(C2,P1^.Name); if (P2=nil) or not P1^.SameValue(Value1,P2,Value2) then exit; end else if not P1^.SameValue(Value1,P1,Value2) then exit; P1 := P1^.Next; end; C1 := GetClassParent(C1); until C1=TObject; result := true; end; end; function ObjectToJSONDebug(Value: TObject; Options: TTextWriterWriteObjectOptions): RawUTF8; begin if Value=nil then result := NULL_STR_VAR else if Value.InheritsFrom(Exception) and not Value.InheritsFrom(ESynException) then result := FormatUTF8('{"%":?}',[Value],[Exception(Value).Message],True) else result := ObjectToJSON(Value,Options); end; function ObjectToVariantDebug(Value: TObject): variant; var json: RawUTF8; begin VarClear(result); json := ObjectToJSONDebug(Value); PDocVariantData(@result)^.InitJSONInPlace(pointer(json),JSON_OPTIONS_FAST); end; procedure _ObjAddProps(Value: TObject; var Obj: variant); var v: variant; begin ObjectToVariant(Value,v,[woDontStoreDefault]); _ObjAddProps(v,Obj); end; function ObjectToVariantDebug(Value: TObject; const ContextFormat: RawUTF8; const ContextArgs: array of const; const ContextName: RawUTF8): variant; begin _Json(ObjectToJSONDebug(Value),result,JSON_OPTIONS_FAST); if ContextFormat<>'' then if ContextFormat[1]='{' then _ObjAddProps([ContextName,_JsonFastFmt(ContextFormat,[],ContextArgs)],result) else _ObjAddProps([ContextName,FormatUTF8(ContextFormat,ContextArgs)],result); end; type TJSONObject = (oNone, oObject, oException, oSynException, oList, oObjectList, {$ifndef LVCL}oCollection,{$endif} oUtfs, oStrings, oSQLRecord, oSQLMany, oPersistent, oSynAutoCreateFields, oSynPersistentWithPassword, oSynMonitor, oSQLTable, oCustomReaderWriter, oCustomPropName); TShort63 = string[63]; TShort63DynArray = array of TShort63; TJSONCustomParser = record Reader: TJSONSerializerCustomReader; Writer: TJSONSerializerCustomWriter; Props: PPropInfoDynArray; Fields: TShort63DynArray; // match Props[] order Kind: TJSONObject; end; TJSONCustomParsers = array of TJSONCustomParser; PJSONCustomParser = ^TJSONCustomParser; var JSONCustomParsers: TSynDictionary; // store TClass->TJSONCustomParser function JSONObjectFromClass(aClassType: TClass; out aParser: PJSONCustomParser): TJSONObject; const MAX = {$ifdef LVCL}15{$else}16{$endif}; TYP: array[0..MAX] of TClass = ( // all classes types gathered in CPU L1 cache TObject,Exception,ESynException,TList,TObjectList,TPersistent,TSynAutoCreateFields, TSynPersistentWithPassword,TSynPersistent,TInterfacedObjectWithCustomCreate, TSynMonitor,TSQLRecordMany,TSQLRecord,TStrings,TRawUTF8List,TSQLTable {$ifndef LVCL},TCollection{$endif}); OBJ: array[0..MAX] of TJSONObject = ( // oPersistent = has published properties oObject,oException,oSynException,oList,oObjectList,oPersistent,oSynAutoCreateFields, oSynPersistentWithPassword,oPersistent,oPersistent, oSynMonitor,oSQLMany,oSQLRecord,oStrings,oUtfs,oSQLTable {$ifndef LVCL},oCollection{$endif}); var P: PJSONCustomParser; added: boolean; i: integer; begin result := oNone; if aClassType<>nil then begin JSONCustomParsers.Safe.Lock; try aParser := JSONCustomParsers.FindValueOrAdd(aClassType,added); if not added then begin result := aParser^.Kind; if result<>oNone then // fast retrieval from cache exit; // =oNone if has been unsubscribed end; repeat // search from RTTI and parent custom registration i := PtrUIntScanIndex(@TYP,MAX+1,PtrUInt(aClassType)); if i>=0 then begin result := OBJ[i]; aParser^.Kind := result; // default serialization from RTTI exit; end; aClassType := GetClassParent(aClassType); P := JSONCustomParsers.FindValue(aClassType); if (P<>nil) and (P^.Kind in [oCustomReaderWriter,oCustomPropName]) then begin aParser^ := P^; // copy from parent result := P^.Kind; exit; end; until aClassType=nil; // should never happen, since TObject exists in TYP[] finally JSONCustomParsers.Safe.UnLock; end; end else aParser := nil; end; class procedure TJSONSerializer.RegisterCustomSerializer(aClass: TClass; aReader: TJSONSerializerCustomReader; aWriter: TJSONSerializerCustomWriter); var added: boolean; P: PJSONCustomParser; begin JSONCustomParsers.Safe.Lock; try P := JSONCustomParsers.FindValueOrAdd(aClass,added); P^.Writer := aWriter; P^.Reader := aReader; P^.Props := nil; // exclusive P^.Fields := nil; if Assigned(aWriter) or Assigned(aReader) then P^.Kind := oCustomReaderWriter else P^.Kind := oNone; finally JSONCustomParsers.Safe.UnLock; end; end; function JSONCustomParsersFieldProp(ClassType: TClass; Parser: PJSONCustomParser; PropName: PUTF8Char; PropNameLen: integer): PPropInfo; var i: integer; begin if Parser^.Props<>nil then begin // search from RegisterCustomSerializerFieldNames() for i := 0 to length(Parser^.Fields)-1 do if IdemPropName(Parser^.Fields[i],PropName,PropNameLen) then begin result := Parser^.Props[i]; exit; end; result := nil; end else // a previous RegisterCustomSerializerFieldNames() was unregistered result := ClassFieldPropWithParentsFromUTF8(ClassType,PropName,PropNameLen); end; class procedure TJSONSerializer.RegisterCustomSerializerFieldNames(aClass: TClass; const aClassFields, aJsonFields: array of ShortString); var prop: PPropInfoDynArray; field: TShort63DynArray; n,p,f: integer; found: boolean; parser: PJSONCustomParser; begin if high(aClassFields)<>high(aJsonFields) then raise EParsingException.CreateUTF8('RegisterCustomSerializerFieldNames(%,%,%)'+ ' fields count mismatch', [aClass,high(aClassFields),high(aJsonFields)]); if aClass.InheritsFrom(TSQLRecord) then raise EParsingException.CreateUTF8('RegisterCustomSerializerFieldNames(%)'+ ' not allowed on ORM class', [aClass]); prop := ClassFieldAllProps(aClass,[low(TTypeKind)..high(TTypeKind)]); SetLength(field,length(prop)); n := 0; for p := 0 to high(prop) do begin found := false; for f := 0 to high(aClassFields) do // check customized field name if IdemPropName(prop[p].Name,aClassFields[f]) then begin if aJsonFields[f]<>'' then begin // '' to ignore this property field[n] := aJsonFields[f]; prop[n] := prop[p]; inc(n); end; found := true; break; end; if not found then begin // default serialization of published property field[n] := prop[p].Name; prop[n] := prop[p]; inc(n); end; end; SetLength(prop,n); SetLength(field,n); JSONCustomParsers.Safe.Lock; try parser := JSONCustomParsers.FindValueOrAdd(aClass,found); @parser^.Writer := nil; // exclusive @parser^.Reader := nil; parser^.Props := prop; parser^.Fields := field; if Assigned(prop) then parser^.Kind := oCustomPropName else parser^.Kind := oNone; finally JSONCustomParsers.Safe.UnLock; end; end; constructor TJSONSerializerRegisteredClassAbstract.Create; begin inherited Create; fSafe.Init; end; destructor TJSONSerializerRegisteredClassAbstract.Destroy; begin inherited; fSafe.Done; end; function TJSONSerializerRegisteredClass.Find(JSON: PUTF8Char; AndRegisterClass: boolean): TClass; var token: shortstring; ClassNameValue: PUTF8Char; ClassNameLen: integer; begin // at input, JSON^='{' result := nil; if self=nil then exit; inc(JSON); GetJSONPropName(JSON,token); if (JSON=nil) or not IdemPropName('ClassName',token) then exit; // we expect woStoreClassName option to have been used if JSONRetrieveStringField(JSON,ClassNameValue,ClassNameLen,false)=nil then exit; //invalid JSON string value fSafe.Lock; try if (fLastClass<>nil) and IdemPropName(PShortString(PPointer(PtrInt(PtrUInt(fLastClass))+vmtClassName)^)^, ClassNameValue,ClassNameLen) then begin result := fLastClass; // for speed-up e.g. within a loop exit; end; result := Find(ClassNameValue,ClassNameLen); if result=nil then begin // not registered here -> try from Classes.pas {$ifndef LVCL} if AndRegisterClass then result := FindClass(UTF8DecodeToString(ClassNameValue,ClassNameLen)); if result=nil then {$endif} exit; // unknown type end; fLastClass := result; finally fSafe.UnLock; end; end; procedure TJSONSerializerRegisteredClass.AddOnce(aItemClass: TClass); begin fSafe.Lock; try if not PtrUIntScanExists(pointer(List),Count,PtrUInt(aItemClass)) then Add(aItemClass); finally fSafe.UnLock; end; end; function TJSONSerializerRegisteredClass.Find(aClassName: PUTF8Char; aClassNameLen: integer): TClass; var i: integer; begin result := nil; fSafe.Lock; try for i := 0 to Count-1 do // new TObject.ClassName is UnicodeString (since Delphi 20009) -> inline code // with vmtClassName = UTF-8 encoded text stored in a shortstring = -44 if IdemPropName(PShortString(PPointer(PtrInt(PtrUInt(List[i]))+vmtClassName)^)^, aClassName,aClassNameLen) then begin result := List[i]; exit; end; finally fSafe.UnLock; end; end; {$ifndef LVCL} type TJSONSerializerRegisteredCollection = class(TJSONSerializerRegisteredClassAbstract) protected public procedure AddOnce(aCollection: TCollectionClass; aItem: TCollectionItemClass); function Find(aCollClassName: PUTF8Char; aCollClassNameLen: integer): TCollectionItemClass; overload; function Find(aCollection: TCollectionClass): TCollectionItemClass; overload; end; function TJSONSerializerRegisteredCollection.Find(aCollection: TCollectionClass): TCollectionItemClass; var i: integer; begin result := nil; if self=nil then exit; fSafe.Lock; try for i := 0 to (Count shr 1)-1 do if TClass(List[i*2])=aCollection then begin result := List[i*2+1]; exit; end; finally fSafe.UnLock; end; end; procedure TJSONSerializerRegisteredCollection.AddOnce(aCollection: TCollectionClass; aItem: TCollectionItemClass); begin if (self=nil) or (Find(aCollection)<>nil) then exit; fSafe.Lock; try Add(aCollection); Add(aItem); finally fSafe.UnLock; end; end; function TJSONSerializerRegisteredCollection.Find(aCollClassName: PUTF8Char; aCollClassNameLen: integer): TCollectionItemClass; var i: integer; begin result := nil; fSafe.Lock; try for i := 0 to (Count shr 1)-1 do // new TObject.ClassName is UnicodeString (since Delphi 20009) -> inline code // with vmtClassName = UTF-8 encoded text stored in a shortstring = -44 if IdemPropName(PShortString(PPointer(PtrInt(PtrUInt(List[i*2]))+vmtClassName)^)^, aCollClassName,aCollClassNameLen) then begin result := List[i*2+1]; exit; end; finally fSafe.UnLock; end; end; var JSONSerializerRegisteredCollection: TJSONSerializerRegisteredCollection=nil; class procedure TJSONSerializer.RegisterCollectionForJSON(aCollection: TCollectionClass; aItem: TCollectionItemClass); begin if JSONSerializerRegisteredCollection=nil then GarbageCollectorFreeAndNil(JSONSerializerRegisteredCollection, TJSONSerializerRegisteredCollection.Create); JSONSerializerRegisteredCollection.AddOnce(aCollection,aItem); RegisterClassForJSON([aCollection,aItem]); end; {$endif LVCL} class procedure TJSONSerializer.RegisterClassForJSON(aItemClass: TClass); begin if JSONSerializerRegisteredClass=nil then GarbageCollectorFreeAndNil(JSONSerializerRegisteredClass, TJSONSerializerRegisteredClass.Create); JSONSerializerRegisteredClass.AddOnce(aItemClass); end; class procedure TJSONSerializer.RegisterClassForJSON(const aItemClass: array of TClass); var i: integer; begin for i := 0 to high(aItemClass) do RegisterClassForJSON(aItemClass[i]); end; class procedure TJSONSerializer.RegisterObjArrayForJSON(aDynArray: PTypeInfo; aItem: TClass; aReader: TDynArrayJSONCustomReader; aWriter: TDynArrayJSONCustomWriter); var serializer: ^TObjArraySerializer; begin if (aItem=nil) or (aDynArray^.DynArrayItemSize<>SizeOf(TObject)) then raise EModelException.CreateUTF8( 'Invalid %.RegisterObjArrayForJSON(TypeInfo(%),%)',[self,aDynArray^.Name,aItem]); if ObjArraySerializers=nil then GarbageCollectorFreeAndNil(ObjArraySerializers,TPointerClassHash.Create); serializer := pointer(ObjArraySerializers.TryAdd(aDynArray)); if serializer=nil then exit; // avoid duplicate serializer^ := TObjArraySerializer.Create(aDynArray,aItem,aReader,aWriter); TTextWriter.RegisterCustomJSONSerializer( aDynArray,serializer^.CustomReader,serializer^.CustomWriter); end; class function TJSONSerializer.RegisterObjArrayFindType(aDynArray: PTypeInfo): PClassInstance; var serializer: TPointerClassHashed; begin serializer := ObjArraySerializers.Find(aDynArray); if serializer=nil then result := nil else result := @TObjArraySerializer(serializer).Instance; end; class function TJSONSerializer.RegisterObjArrayFindTypeInfo(aClass: TClass): PTypeInfo; var i: integer; item: ^TObjArraySerializer; begin item := pointer(ObjArraySerializers.List); for i := 1 to ObjArraySerializers.Count do if item^.Instance.ItemClass=aClass then begin result := item^.fInfo; exit; end else inc(item); result := nil; end; class procedure TJSONSerializer.RegisterObjArrayForJSON( const aDynArrayClassPairs: array of const); var n,i: integer; begin n := length(aDynArrayClassPairs); if (n=0) or (n and 1=1) then exit; n := n shr 1; if n=0 then exit; for i := 0 to n-1 do if (aDynArrayClassPairs[i*2].VType<>vtPointer) or (aDynArrayClassPairs[i*2+1].VType<>vtClass) then raise EParsingException.Create('RegisterObjArrayForJSON[?]') else RegisterObjArrayForJSON( aDynArrayClassPairs[i*2].VPointer,aDynArrayClassPairs[i*2+1].VClass); end; function JSONToNewObject(var From: PUTF8Char; var Valid: boolean; Options: TJSONToObjectOptions): TObject; var ItemClass: TClass; ItemInstance: TClassInstance; begin Valid := false; result := nil; if From=nil then exit; while From^ in [#1..' '] do inc(From); if PInteger(From)^=NULL_LOW then begin Valid := true; exit; end; if From^<>'{' then exit; // input should be either null, either {"ClassName":"TMyClass",...} ItemClass := JSONSerializerRegisteredClass.Find(From,true); if ItemClass=nil then exit; // unknown type ItemInstance.Init(ItemClass); result := ItemInstance.CreateNew; From := JSONToObject(result,From,Valid,nil,Options); if not Valid then FreeAndNil(result); // avoid memory leak end; function PropIsIDTypeCastedField(Prop: PPropInfo; IsObj: TJSONObject; Value: TObject): boolean; begin // see [22ce911c715] if (Value<>nil) and (Prop^.PropType^.ClassSQLFieldType=sftID) then case IsObj of oSQLMany: if IdemPropName(Prop^.Name,'source') or IdemPropName(Prop^.Name,'dest') then result := true else result := not TSQLRecord(Value).fFill.JoinedFields; oSQLRecord: result := not TSQLRecord(Value).fFill.JoinedFields; else result := false; // real instance for regular classes end else result := false; // assume real instance by default end; type /// wrapper object to ease JSONToObject() maintainability TJSONToObject = object public // input parameters From: PUTF8Char; Value: TObject; Options: TJSONToObjectOptions; TObjectListItemClass: TClass; // output parameters Dest: PUTF8Char; Valid: boolean; procedure Parse; private ValueClass: TClass; parser: PJSONCustomParser; PropName, PropValue: PUTF8Char; PropNameLen, PropValueLen: integer; P: PPropInfo; IsObj: TJSONObject; Kind: TTypeKind; EndOfObject: AnsiChar; NestedValid, wasString: boolean; {$ifndef NOVARIANTS} procedure HandleVariant; {$endif} procedure HandleObjectList(Lst: TObjectList); {$ifndef LVCL} procedure HandleCollection(Coll: TCollection); {$endif} procedure HandleStrings(Str: TStrings); procedure HandleUtfs(utf: TRawUTF8List); procedure HandleProperty(var tmp: RawUTF8); end; function JSONToObject(var ObjectInstance; From: PUTF8Char; out Valid: boolean; TObjectListItemClass: TClass; Options: TJSONToObjectOptions): PUTF8Char; var parser: TJSONToObject; begin parser.From := From; parser.Options := Options; parser.TObjectListItemClass := TObjectListItemClass; parser.Value := TObject(ObjectInstance); parser.Parse; Valid := parser.Valid; TObject(ObjectInstance) := parser.Value; // e.g. 'null' -> FreeAndNil() result := parser.Dest; end; function JSONSettingsToObject(var InitialJsonContent: RawUTF8; Instance: TObject): boolean; var tmp: TSynTempBuffer; begin result := false; if InitialJsonContent='' then exit; tmp.Init(InitialJsonContent); try RemoveCommentsFromJSON(tmp.buf); JSONToObject(Instance,tmp.buf,result,nil,JSONTOOBJECT_TOLERANTOPTIONS); if not result then InitialJsonContent := ''; finally tmp.Done; end; end; function ObjectLoadJSON(var ObjectInstance; const JSON: RawUTF8; TObjectListItemClass: TClass; Options: TJSONToObjectOptions): boolean; var tmp: TSynTempBuffer; begin tmp.Init(JSON); if tmp.len<>0 then try JSONToObject(ObjectInstance,tmp.buf,result,TObjectListItemClass,Options); finally tmp.Done; end else result := false; end; procedure TJSONToObject.HandleObjectList(Lst: TObjectList); var Item: TObject; ItemClass: TClass; ItemInstance: TClassInstance; begin Lst.Clear; ItemInstance.ItemClass := nil; repeat while From^ in [#1..' '] do inc(From); case From^ of #0: exit; ']': begin inc(From); break; end; ',': inc(From); // valid delimiter between objects '{': begin Dest := From; if TObjectListItemClass=nil then begin // recognize "ClassName":... ItemClass := JSONSerializerRegisteredClass.Find(From,true); if ItemClass=nil then exit; // unknown "ClassName":.. type end else ItemClass := TObjectListItemClass; if ItemInstance.ItemClass<>ItemClass then ItemInstance.Init(ItemClass); Item := ItemInstance.CreateNew; From := JSONToObject(Item,From,NestedValid,nil,Options); if not NestedValid then begin Dest := From; exit; end else if From=nil then exit; Lst.Add(Item); end; else exit; end; until false; // only way of being here is to have an ending ] at expected place Valid := true; end; {$ifndef LVCL} procedure TJSONToObject.HandleCollection(Coll: TCollection); var CollItem: TObject; begin Coll.BeginUpdate; try Coll.Clear; repeat while From^ in [#1..' '] do inc(From); case From^ of #0: exit; ']': begin inc(From); break; end; ',': inc(From); // valid delimiter between objects '{': begin Dest := From; CollItem := Coll.Add; From := JSONToObject(CollItem,From,NestedValid,nil,Options); if not NestedValid then begin Dest := From; exit; end else if From=nil then exit; end; else exit; end; until false; // only way of being here is to have an ending ] at expected place Valid := true; finally Coll.EndUpdate; end; end; {$endif} procedure TJSONToObject.HandleStrings(Str: TStrings); var s: string; begin {$ifndef LVCL} Str.BeginUpdate; // Str: TStrings absolute Value try {$endif} Str.Clear; repeat while From^ in [#1..' '] do inc(From); case From^ of #0: exit; ']': begin inc(From); break; end; '"': begin Dest := From; PropValue := GetJSONField(From,From,@wasString,@EndOfObject,@PropValueLen); if (PropValue=nil) or not wasString then exit; UTF8DecodeToString(PropValue,PropValueLen,s); Str.Add(s); case EndOfObject of ']': break; ',': continue; else exit; end; end; else exit; end; until false; Valid := true; {$ifndef LVCL} finally Str.EndUpdate; end; {$endif} end; procedure TJSONToObject.HandleUtfs(utf: TRawUTF8List); var U: RawUTF8; begin utf.BeginUpdate; try utf.Clear; repeat while From^ in [#1..' '] do inc(From); case From^ of #0: exit; ']': begin inc(From); break; end; '"': begin Dest := From; PropValue := GetJSONField(From,From,@wasString,@EndOfObject,@PropValueLen); if (PropValue=nil) or not wasString then exit; FastSetString(U,PropValue,PropValueLen); utf.Add(U); case EndOfObject of ']': break; ',': if From=nil then exit else continue; else exit; end; end; else exit; end; until false; Valid := true; finally utf.EndUpdate; end; end; {$ifndef NOVARIANTS} procedure TJSONToObject.HandleVariant; var temp: variant; opt: TDocVariantOptions; begin if j2oHandleCustomVariants in Options then begin if j2oHandleCustomVariantsWithinString in Options then opt := [dvoValueCopiedByReference,dvoAllowDoubleValue,dvoJSONObjectParseWithinString] else opt := [dvoValueCopiedByReference,dvoAllowDoubleValue]; GetVariantFromJSON(PropValue,wasString,temp,@opt,false); end else GetVariantFromJSON(PropValue,wasString,temp,nil,false); P^.SetVariantProp(Value,temp); end; {$endif NOVARIANTS} procedure TJSONToObject.HandleProperty(var tmp: RawUTF8); var V: PtrInt; V64: Int64; D: double; err: integer; begin PropValue := GetJSONFieldOrObjectOrArray(From,@wasString,@EndOfObject, {$ifdef NOVARIANTS}false{$else}Kind=tkVariant{$endif},true,@PropValueLen); if (PropValue=nil) or not (EndOfObject in ['}',',']) then exit; // invalid JSON content (null has been handled above) case Kind of tkInt64{$ifdef FPC}, tkQWord{$endif}: if wasString then begin if PropValue^=#0 then V64 := 0 else if not (j2oAllowInt64Hex in Options) or not HexDisplayToBin(PAnsiChar(PropValue),@V64,SizeOf(V64)) then exit; P^.SetInt64Prop(Value,V64); end else begin if P^.PropType^.IsQWord then V64 := GetQWord(PropValue,err) else V64 := GetInt64(PropValue,err); if err<>0 then exit; P^.SetInt64Prop(Value,V64); end; tkClass: begin if wasString or (P^.PropType^.ClassSQLFieldType<>sftID) then exit; // should have been handled above V := GetInteger(PropValue,err); if err<>0 then exit; // invalid value P^.SetOrdProp(Value,V); end; tkEnumeration: begin if wasString then begin // in case enum stored as string V := P^.PropType^.EnumBaseType^.GetEnumNameValue(PropValue,PropValueLen); if V<0 then if j2oIgnoreUnknownEnum in Options then V := 0 else exit; end else begin V := GetInteger(PropValue,err); if err<>0 then if j2oIgnoreUnknownEnum in Options then V := 0 else exit; // invalid value end; P^.SetOrdProp(Value,V); end; {$ifdef FPC} tkBool, {$endif} tkInteger, tkSet: if wasString then exit else begin // From='true' or From='false' were converted into '1 or '0' V := GetInteger(PropValue,err); if err<>0 then exit; // invalid value P^.SetOrdProp(Value,V); end; {$ifdef FPC}tkLStringOld,{$endif}{$ifdef HASVARUSTRING}tkUString,{$endif} tkLString,tkWString: // handle all string types from temporary RawUTF8 if wasString or (j2oIgnoreStringType in Options) then begin FastSetString(tmp,PropValue,PropValueLen); P^.SetLongStrValue(Value,tmp); end else exit; {$ifdef PUBLISHRECORD} tkRecord{$ifdef FPC},tkObject{$endif}: RecordLoadJSON(P^.GetFieldAddr(Value)^,PropValue,P^.TypeInfo); {$endif} {$ifndef NOVARIANTS} tkVariant: HandleVariant; // dedicated method for a local temporary variant {$endif} tkFloat: if P^.TypeInfo=TypeInfo(TDateTime) then if wasString then begin if PInteger(PropValue)^ and $ffffff=JSON_SQLDATE_MAGIC then inc(PropValue,3); // ignore U+FFF1 pattern P^.SetDoubleProp(Value,Iso8601ToDateTimePUTF8Char(PropValue,PropValueLen)); end else exit else if wasString then exit else if P^.PropType^.FloatType=ftCurr then P^.SetCurrencyProp(Value,StrToCurrency(pointer(PropValue))) else begin D := GetExtended(pointer(PropValue),err); if err<>0 then exit else // invalid JSON content P^.SetFloatProp(Value,D); end; else exit; // unhandled type end; Dest := nil; // if we reached here, no error occured with this property end; procedure TJSONToObject.Parse; var V: PtrInt; DynArray: TDynArray; U: RawUTF8; Beg: PAnsiChar; begin Valid := false; Dest := From; if Value=nil then exit; ValueClass := PClass(Value)^; IsObj := JSONObjectFromClass(ValueClass,parser); if From=nil then begin case IsObj of // handle '' as Clear for arrays {$ifndef LVCL} oCollection: TCollection(Value).Clear; {$endif} oStrings: TStrings(Value).Clear; oUtfs: TRawUTF8List(Value).Clear; oObjectList: TObjectList(Value).Clear; end; exit; end; if PInteger(From)^=NULL_LOW then begin if (IsObj=oCustomReaderWriter) and Assigned(parser^.Reader) then // custom JSON reader expects to be executed even if value is null Dest := parser^.Reader(Value,From,Valid,Options) else begin FreeAndNil(Value); Dest := From+4; Valid := true; // null is a valid JSON object end; exit; end; while From^ in [#1..' '] do inc(From); if IsObj=oCustomReaderWriter then begin // may be from [array] or {object} if Assigned(parser^.Reader) then // leave Valid=false if Reader=nil Dest := parser^.Reader(Value,From,Valid,Options); exit; end; if From^='[' then begin // nested array = TObjectList, TCollection, TRawUTF8List or TStrings inc(From); case IsObj of oObjectList: // TList leaks memory, but TObjectList uses "ClassName":.. HandleObjectList(TObjectList(Value)); {$ifndef LVCL} oCollection: HandleCollection(TCollection(Value)); {$endif} oStrings: HandleStrings(TStrings(Value)); oUtfs: HandleUtfs(TRawUTF8List(Value)); end; // case IsObj of // Valid=false if not TCollection, TRawUTF8List nor TStrings if Valid and (From<>nil) then begin while From^ in [#1..' '] do inc(From); if From^=#0 then From := nil; end; Dest := From; exit; // a JSON array begin with [ end; if From^<>'{' then begin Dest := From; exit; // a JSON object MUST begin with { end; repeat inc(From) until (From^=#0) or (From^>' '); EndOfObject := #0; if From^='}' then begin // empty JSON object like {} (e.g. all properties having default values) EndOfObject := '}'; Inc(From); end else // for each fields repeat wasString := false; Dest := From; PropName := GetJSONPropName(From,@PropNameLen); // get property name if (From=nil) or (PropName=nil) then exit; // invalid JSON content if IdemPropName('ClassName',PropName,PropNameLen) then begin // WriteObject() was called with woStoreClassName option -> ignore it PropValue := GetJSONField(From,From,@wasString,@EndOfObject); if (PropValue=nil) or not wasString or not (EndOfObject in ['}',',']) then exit; // invalid JSON content continue; end; if IsObj in [oSQLRecord,oSQLMany] then if IsRowID(PropName) then begin // manual handling of TSQLRecord.ID property unserialization PropValue := GetJSONField(From,From,@wasString,@EndOfObject); if (PropValue=nil) or wasString or not (EndOfObject in ['}',',']) then exit; // invalid JSON content SetID(PropValue,TSQLRecord(Value).fID); continue; end; if IsObj=oCustomPropName then P := JSONCustomParsersFieldProp(ValueClass,parser,PropName,PropNameLen) else P := ClassFieldPropWithParentsFromUTF8(ValueClass,PropName,PropNameLen); if P=nil then // unknown property if j2oIgnoreUnknownProperty in Options then begin From := GotoNextJSONItem(From,1,@EndOfObject); continue; end else exit; // by default, abort Kind := P^.PropType^.Kind; while From^ in [#1..' '] do inc(From); Dest := From; if PInteger(From)^=NULL_LOW then begin // null value should set the default value, or free nested object if (Kind=tkClass) and (IsObj in [oSQLRecord,oSQLMany]) then exit; // null expects a plain TSynPersistent/TPersistent P^.SetDefaultValue(Value); // will set 0,'' or FreeAndNil(NestedObject) inc(From,4); while From^ in [#1..' '] do inc(From); EndOfObject := From^; if From^ in EndOfJSONField then inc(From); end else if (From^ in ['[','{']) {$ifndef NOVARIANTS}and (Kind<>tkVariant){$endif} then begin if Kind=tkDynArray then begin P^.GetDynArray(Value,DynArray); From := DynArray.LoadFromJSON(From); if From=nil then exit; // invalid '[dynamic array]' content end else if P^.TypeInfo=TypeInfo(RawJSON) then begin Beg := pointer(From); From := GotoNextJSONObjectOrArray(From); if From=nil then exit; FastSetString(U,Beg,From-Beg); P^.SetLongStrProp(Value,U); end else if (Kind=tkSet) and (From^='[') then begin // set as string array V := GetSetNameValue(P^.TypeInfo,From,EndOfObject); if From=nil then exit; // invalid '["setone","settwo"]' content P^.SetOrdProp(Value,V); if EndOfObject='}' then break else continue; end else if (Kind in tkRecordTypes) and (From^='{') then begin // from Delphi XE5+ From := RecordLoadJSON(P^.GetFieldAddr(Value)^,From,P^.TypeInfo,@EndOfObject); if From=nil then exit; // invalid '{record}' content if EndOfObject='}' then break else continue; end else begin if Kind<>tkClass then exit; // true nested object should begin with '[' or '{' if PropIsIDTypeCastedField(P,IsObj,Value) then exit; // only TSQLRecordMany/joined properties are true instances // will handle '[TCollection...' '[TStrings...' '{TObject...' From := P^.ClassFromJSON(Value,From,NestedValid,Options); if not NestedValid then begin Dest := From; exit; end else if From=nil then exit; // invalid JSON content: we expect at least a last '}' end; while From^ in [#1..' '] do inc(From); EndOfObject := From^; if From^ in EndOfJSONField then inc(From); end else begin HandleProperty(U); if Dest<>nil then exit; // an error occured end; until (From=nil) or (EndOfObject='}'); if IsObj=oSynAutoCreateFields then TSynAutoCreateFields(Value).AfterLoad; if From<>nil then begin while From^ in [#1..' '] do inc(From); if From^=#0 then From := nil; end; Valid := (EndOfObject='}'); // mark parsing success Dest := From; end; function UrlDecodeObject(U: PUTF8Char; Upper: PAnsiChar; var ObjectInstance; Next: PPUTF8Char; Options: TJSONToObjectOptions): boolean; var tmp: RawUTF8; begin result := UrlDecodeValue(U,Upper,tmp,Next); if result then JSONToObject(ObjectInstance,Pointer(tmp),result,nil,Options); end; function JSONFileToObject(const JSONFile: TFileName; var ObjectInstance; TObjectListItemClass: TClass; Options: TJSONToObjectOptions): boolean; var tmp: RawUTF8; begin tmp := AnyTextFileToRawUTF8(JSONFile,true); if tmp='' then result := false else begin RemoveCommentsFromJSON(pointer(tmp)); JSONToObject(ObjectInstance,pointer(tmp),result,TObjectListItemClass,Options); end; end; function ObjectToJSONFile(Value: TObject; const JSONFile: TFileName; Options: TTextWriterWriteObjectOptions): boolean; var humanread: boolean; json: RawUTF8; begin humanread := woHumanReadable in Options; if humanread and (woHumanReadableEnumSetAsComment in Options) then humanread := false else // JsonReformat() erases comments Exclude(Options,woHumanReadable); json := ObjectToJSON(Value,Options); if humanread then // woHumanReadable not working with custom JSON serializers, e.g. T*ObjArray result := JSONBufferReformatToFile(pointer(json),JSONFile) else result := FileFromString(json,JSONFile); end; procedure ReadObject(Value: TObject; From: PUTF8Char; const SubCompName: RawUTF8); var CT: TClass; P: PPropInfo; i: integer; Obj: TObject; U: RawUTF8; UpperName: array[byte] of AnsiChar; begin if Value=nil then // allow From=nil -> default values exit; CT := Value.ClassType; repeat for i := 1 to InternalClassPropInfo(CT,P) do begin PWord(UpperCopyShort(UpperCopy255(UpperName,SubCompName),P^.Name))^ := ord('='); U := FindIniNameValue(From,UpperName); if P^.PropType^.Kind=tkClass then begin // recursive unserialization Obj := P^.GetObjProp(Value); if (Obj<>nil) and ClassHasPublishedFields(PPointer(Obj)^) then ReadObject(Obj,From,SubCompName+ToUTF8(P^.Name)+'.'); end else P^.SetFromText(Value,U,@JSON_OPTIONS[true],{allowdouble=}true); P := P^.Next; end; CT := GetClassParent(CT); until CT=TObject; end; procedure ReadObject(Value: TObject; const FromContent,SubCompName: RawUTF8); var source: PUTF8Char; UpperSection: array[byte] of AnsiChar; begin if Value=nil then exit; // avoid GPF PWord(UpperCopyShort(UpperSection,ClassNameShort(Value)^))^ := ord(']'); source := pointer(FromContent); if FindSectionFirstLine(source,UpperSection) then ReadObject(Value,source,SubCompName); end; procedure SetDefaultValuesObject(Value: TObject); var p: PPropInfo; c: TClass; i: integer; begin if Value=nil then exit; c := Value.ClassType; repeat for i := 1 to InternalClassPropInfo(Value.ClassType,p) do begin case p^.PropType^.Kind of tkEnumeration,tkSet,tkInteger,tkChar,tkWChar{$ifdef FPC},tkBool{$endif}: if p^.Default<>NO_DEFAULT then p^.SetOrdProp(Value,p^.Default); tkClass: SetDefaultValuesObject(p^.GetObjProp(Value)); end; p := p^.Next; end; c := GetClassParent(c); until c=TObject; end; function IsObjectDefaultOrVoid(Value: TObject): boolean; var i: integer; C: TClass; P: PPropinfo; begin if Value<>nil then begin result := false; C := Value.ClassType; repeat for i := 1 to InternalClassPropInfo(C,P) do if P^.IsDefaultOrVoid(Value) then P := P^.Next else exit; C := GetClassParent(C); until C=TObject; end; result := true; end; procedure ClearObject(Value: TObject; FreeAndNilNestedObjects: boolean=false); var p: PPropInfo; c: TClass; i: integer; begin if Value=nil then exit; c := Value.ClassType; repeat for i := 1 to InternalClassPropInfo(c,p) do begin p^.SetDefaultValue(Value,FreeAndNilNestedObjects); p := p^.Next; end; c := GetClassParent(c); until c=TObject; end; { TClassInstance } procedure TClassInstance.Init(C: TClass); begin ItemClass := C; if C<>nil then repeat // this unrolled loop is faster than cascaded if C.InheritsFrom() if C<>TSQLRecord then if C<>TObjectList then if C<>TInterfacedObjectWithCustomCreate then if C<>TPersistentWithCustomCreate then if C<>TSynPersistent then if C<>TComponent then {$ifndef LVCL} if C<>TInterfacedCollection then if C<>TCollection then if C<>TCollectionItem then {$endif LVCL} begin {$ifdef FPC} C := GetClassParent(C); {$else} C := PPointer(PtrInt(C)+vmtParent)^; if C<>nil then C := PPointer(C)^; {$endif FPC} if C<>nil then continue else begin ItemCreate := cicTObject; exit; end; end else {$ifndef LVCL} begin ItemCreate := cicTCollectionItem; exit; end else begin // plain TCollection shall have been registered CollectionItemClass := JSONSerializerRegisteredCollection.Find(TCollectionClass(ItemClass)); if CollectionItemClass<>nil then begin ItemCreate := cicTCollection; exit; end else raise EParsingException.CreateUTF8('% shall inherit from TInterfacedCollection'+ ' or call TJSONSerializer.RegisterCollectionForJSON()',[ItemClass]); end else begin ItemCreate := cicTInterfacedCollection; exit; end else {$endif LVCL} begin ItemCreate := cicTComponent; exit; end else begin ItemCreate := cicTSynPersistent; exit; end else begin ItemCreate := cicTPersistentWithCustomCreate; exit; end else begin ItemCreate := cicTInterfacedObjectWithCustomCreate; exit; end else begin ItemCreate := cicTObjectList; exit; end else begin ItemCreate := cicTSQLRecord; exit; end; until false; ItemCreate := cicUnknown; end; function TClassInstance.CreateNew: TObject; begin if @self<>nil then case ItemCreate of cicUnknown: begin result := nil; exit; end; cicTSQLRecord: begin result := TSQLRecordClass(ItemClass).Create; exit; end; cicTObjectList: begin result := TObjectList.Create; exit; end; cicTPersistentWithCustomCreate: begin result := TPersistentWithCustomCreateClass(ItemClass).Create; exit; end; cicTComponent: begin result := TComponentClass(ItemClass).Create(nil); exit; end; cicTSynPersistent: begin result := TSynPersistentClass(ItemClass).Create; exit; end; cicTInterfacedObjectWithCustomCreate: begin result := TInterfacedObjectWithCustomCreateClass(ItemClass).Create; exit; end; {$ifndef LVCL} cicTInterfacedCollection: begin result := TInterfacedCollectionClass(ItemClass).Create; exit; end; cicTCollection: begin result := TCollectionClass(ItemClass).Create(CollectionItemClass); exit; end; cicTCollectionItem: begin result := TCollectionItemClass(ItemClass).Create(nil); exit; end; {$endif} cicTObject: begin result := ItemClass.Create; exit; end; else begin result := nil; exit; end; end else begin result := nil; exit; end; end; procedure TClassInstance.SetCustomComment(var CustomComment: RawUTF8); begin FormatUTF8('array of {%}',[ClassFieldNamesAllPropsAsText(ItemClass,true)], CustomComment); end; {$ifdef MSWINDOWS} { TSQLRestClientURIMessage } constructor TSQLRestClientURIMessage.Create(aModel: TSQLModel; const ServerWindowName: string; ClientWindow: HWND; TimeOutMS: cardinal); begin inherited Create(aModel); fClientWindow := ClientWindow; fServerWindowName := ServerWindowName; fTimeOutMS := TimeOutMS; end; constructor TSQLRestClientURIMessage.Create(aModel: TSQLModel; const ServerWindowName, ClientWindowName: string; TimeOutMS: cardinal); var H: HWND; begin H := CreateInternalWindow(ClientWindowName,self); if H=0 then raise ECommunicationException.CreateUTF8('%.Create(): CreateInternalWindow("%")', [self,ClientWindowName]); fClientWindowName := ClientWindowName; Create(aModel,ServerWindowName,H,TimeOutMS); end; destructor TSQLRestClientURIMessage.Destroy; begin try inherited Destroy; finally ReleaseInternalWindow(fClientWindowName,fClientWindow); end; end; procedure TSQLRestClientURIMessage.DefinitionTo(Definition: TSynConnectionDefinition); begin if Definition=nil then exit; inherited DefinitionTo(Definition); // save Kind + User/Password Definition.ServerName := StringToUTF8(fServerWindowName); Definition.DatabaseName := StringToUTF8(fClientWindowName); end; constructor TSQLRestClientURIMessage.RegisteredClassCreateFrom(aModel: TSQLModel; aDefinition: TSynConnectionDefinition); begin Create(aModel,UTF8ToString(aDefinition.ServerName), UTF8ToString(aDefinition.DatabaseName),10000); inherited RegisteredClassCreateFrom(aModel,aDefinition); // call SetUser() end; procedure TSQLRestClientURIMessage.InternalURI(var Call: TSQLRestURIParams); var Msg: RawUTF8; Data: TCopyDataStruct; Finished64: Int64; P: PUTF8Char; aMsg: TMsg; {$ifdef WITHLOG} log: ISynLog; // for Enter auto-leave to work with FPC {$endif} begin {$ifdef WITHLOG} log := fLogClass.Enter(self, 'InternalURI'); {$endif} if (fClientWindow=0) or not InternalCheckOpen then begin Call.OutStatus := HTTP_NOTIMPLEMENTED; // 501 InternalLog('InternalCheckOpen failure',sllClient); exit; end; // 1. send request // #1 is a field delimiter below, since Get*Item() functions return nil for #0 FastSetString(Msg,@MAGIC_SYN,4); Msg := Msg+Call.Url+#1+Call.Method+#1+Call.InHead+#1+Call.InBody; Data.dwData := fClientWindow; Data.cbData := length(Msg)*SizeOf(Msg[1]); Data.lpData := pointer(Msg); fSafe.Enter; try fCurrentResponse := #0; // mark expect some response Call.OutStatus := SendMessage(fServerWindow,WM_COPYDATA,fClientWindow,PtrInt(@Data)); if not StatusCodeIsSuccess(Call.OutStatus) then begin fCurrentResponse := ''; with Call do InternalLog('% % status=%',[Method,Url,OutStatus],sllError); exit; end; // 2. expect answer from server if fCurrentResponse=#0 then begin // in practice, we never reach here since SendMessage() did wait for the // message to be processed by the receiver, so the Server should have // already answered and fCurrentResponse field should have been set Finished64 := GetTickCount64+fTimeOutMS; repeat // incoming WM_COPYDATA will set fCurrentResponse in WMCopyData() method if not DoNotProcessMessages then while PeekMessage(aMsg,0,0,0,PM_REMOVE) do begin TranslateMessage(aMsg); DispatchMessage(aMsg); end; SleepHiRes(0); if GetTickCount64>Finished64 then begin Call.OutStatus := HTTP_TIMEOUT; // 408 Request Timeout Error exit; end; until fCurrentResponse<>#0; end; // 3. return answer to caller if length(fCurrentResponse)<=SizeOf(Int64) then Call.OutStatus := HTTP_NOTIMPLEMENTED else begin P := pointer(fCurrentResponse); if PCardinal(P)^<>MAGIC_SYN then // broadcasted WM_COPYDATA message? :( Call.OutStatus := 0 else begin Call.OutStatus := PIntegerArray(P)[1]; Call.OutInternalState := PIntegerArray(P)[2]; inc(P,SizeOf(integer)*3); end; if Call.OutStatus=0 then Call.OutStatus := HTTP_NOTFOUND else begin GetNextItem(P,#1,Call.OutHead); if P<>nil then SetString(Call.OutBody,P,length(fCurrentResponse)-(P-pointer(fCurrentResponse))); end; end; finally fSafe.Leave; end; with Call do InternalLog('% % status=% state=%',[Method,Url,OutStatus,OutInternalState],sllClient); end; procedure TSQLRestClientURIMessage.WMCopyData(var Msg: TWMCopyData); begin if (self=nil) or (Msg.From<>fServerWindow) or (PCopyDataStruct(Msg.CopyDataStruct)^.dwData<>fServerWindow) then exit; Msg.Result := HTTP_SUCCESS; // Send something back if fCurrentResponse=#0 then // expect some response? FastSetString(fCurrentResponse,PCopyDataStruct(Msg.CopyDataStruct)^.lpData, PCopyDataStruct(Msg.CopyDataStruct)^.cbData); end; function TSQLRestClientURIMessage.InternalCheckOpen: boolean; begin fSafe.Enter; try if fServerWindow<>0 then begin result := true; exit; // only reconnect if forced by InternalClose call or at first access end; fServerWindow := FindWindow(pointer(fServerWindowName),nil); result := fServerWindow<>0; finally fSafe.Leave; end; end; procedure TSQLRestClientURIMessage.InternalClose; begin fServerWindow := 0; end; {$endif} { TSQLRecordSigned } function TSQLRecordSigned.ComputeSignature(const UserName,Content: RawByteString): RawUTF8; var SHA: TSHA256; Digest: TSHA256Digest; begin SHA.Init; SHA.Update(TTimeLogBits(fSignatureTime).Text(false)); SHA.Update(ToText(ClassType)); SHA.Update(UserName); SHA.Update(Content); SHA.Final(Digest); result := SHA256DigestToString(Digest); end; function TSQLRecordSigned.CheckSignature(const Content: RawByteString): boolean; var i: integer; sign: RawUTF8; begin result := false; if self=nil then exit; i := PosExChar('/',fSignature); if i=0 then exit; sign := ComputeSignature(copy(fSignature,1,i-1),Content); if IdemPropNameU(sign,copy(fSignature,i+1,SizeOf(TSHA256Digest)*2)) then result := true; end; function TSQLRecordSigned.SetAndSignContent(const UserName: RawUTF8; const Content: RawByteString; ForcedSignatureTime: Int64): boolean; begin result := (fSignature='') and (fSignatureTime=0); if not result then exit; // sign is allowed only once if ForcedSignatureTime<>0 then fSignatureTime := ForcedSignatureTime else fSignatureTime := TimeLogNow; fSignature := UserName+'/'+ComputeSignature(UserName,Content); end; function TSQLRecordSigned.SignedBy: RawUTF8; var i: integer; begin if self=nil then i := 0 else i := PosExChar('/',fSignature); if i=0 then result := '' else result := copy(fSignature,1,i-1); end; procedure TSQLRecordSigned.UnSign; begin fSignature := ''; fSignatureTime := 0; end; { TSQLRecordInterfaced } class function TSQLRecordInterfaced.NewInstance: TObject; begin result := inherited NewInstance; TSQLRecordInterfaced(result).fRefCount := 1; end; procedure TSQLRecordInterfaced.AfterConstruction; {$ifdef PUREPASCAL} begin InterlockedDecrement(fRefCount); // fRefCount=1 in NewInstance end; {$else} {$ifdef FPC} nostackframe; assembler; {$endif} asm lock dec [eax].TInterfacedObject.fRefCount end; {$endif} procedure TSQLRecordInterfaced.BeforeDestruction; begin if fRefCount<>0 then System.Error(reInvalidPtr); end; {$ifdef FPC} function TSQLRecordInterfaced.QueryInterface( {$IFDEF FPC_HAS_CONSTREF}constref{$ELSE}const{$ENDIF} IID: TGUID; out Obj): longint; {$else} function TSQLRecordInterfaced.QueryInterface(const IID: TGUID; out Obj): HResult; {$endif} begin if GetInterface(IID,Obj) then result := 0 else result := {$ifdef FPC}longint{$endif}(E_NOINTERFACE); end; function TSQLRecordInterfaced._AddRef: {$ifdef FPC}longint{$else}integer{$endif}; begin result := InterlockedIncrement(fRefCount); end; function TSQLRecordInterfaced._Release: {$ifdef FPC}longint{$else}integer{$endif}; begin result := InterlockedDecrement(fRefCount); if result=0 then Destroy; end; { TSQLRecordFTS3 } class function TSQLRecordFTS3.OptimizeFTS3Index(Server: TSQLRestServer): boolean; begin if (self=nil) or (Server=nil) then Result:= false else with RecordProps do Result := Server.ExecuteFmt('INSERT INTO %(%) VALUES(''optimize'');', [SQLTableName,SQLTableName]); end; { TSQLRecordFTS4 } class procedure TSQLRecordFTS4.InitializeTable(Server: TSQLRestServer; const FieldName: RawUTF8; Options: TSQLInitializeTableOptions); var Props: TSQLModelRecordProperties; main,fts,ftsfields: RawUTF8; begin inherited; if FieldName<>'' then exit; Props := Server.Model.Props[self]; if (Props=nil) or (Props.fFTSWithoutContentFields='') then exit; main := Server.Model.Tables[Props.fFTSWithoutContentTableIndex].SQLTableName; if not Server.IsInternalSQLite3Table(Props.fFTSWithoutContentTableIndex) then begin Server.InternalLog('% is an external content FTS4/5 table but source % is not '+ 'a local SQLite3 table: FTS search will be unavailable',[self,main],sllWarning); exit; end; fts := Props.Props.SQLTableName; ftsfields := Props.Props.SQLTableSimpleFieldsNoRowID; // see http://www.sqlite.org/fts3.html#*fts4content if Props.Kind=rFTS5 then begin // In fts 5 we can't use docid only rowid, also use insert() values('delete',) to delete record Server.ExecuteFmt('CREATE TRIGGER %_bu BEFORE UPDATE ON % '+ 'BEGIN INSERT INTO %(%,rowid,%) VALUES(''delete'',old.rowid%); END;', [main,main,fts,fts,ftsfields, StringReplaceAll(Props.fFTSWithoutContentFields, 'new.', 'old.')]); Server.ExecuteFmt('CREATE TRIGGER %_bd BEFORE DELETE ON % '+ 'BEGIN INSERT INTO %(%,rowid,%) VALUES(''delete'',old.rowid%); END;', [main,main,fts,fts,ftsfields, StringReplaceAll(Props.fFTSWithoutContentFields, 'new.', 'old.')]); Server.ExecuteFmt('CREATE TRIGGER %_au AFTER UPDATE ON % '+ 'BEGIN INSERT INTO %(rowid,%) VALUES(new.rowid%); END;', [main,main,fts,ftsfields,Props.fFTSWithoutContentFields]); Server.ExecuteFmt('CREATE TRIGGER %_ai AFTER INSERT ON % '+ 'BEGIN INSERT INTO %(rowid,%) VALUES(new.rowid%); END;', [main,main,fts,ftsfields,Props.fFTSWithoutContentFields]); end else begin Server.ExecuteFmt('CREATE TRIGGER %_bu BEFORE UPDATE ON % '+ 'BEGIN DELETE FROM % WHERE docid=old.rowid; END;', [main,main,fts]); Server.ExecuteFmt('CREATE TRIGGER %_bd BEFORE DELETE ON % '+ 'BEGIN DELETE FROM % WHERE docid=old.rowid; END;', [main,main,fts]); Server.ExecuteFmt('CREATE TRIGGER %_au AFTER UPDATE ON % '+ 'BEGIN INSERT INTO %(docid,%) VALUES(new.rowid%); END;', [main,main,fts,ftsfields,Props.fFTSWithoutContentFields]); Server.ExecuteFmt('CREATE TRIGGER %_ai AFTER INSERT ON % '+ 'BEGIN INSERT INTO %(docid,%) VALUES(new.rowid%); END;', [main,main,fts,ftsfields,Props.fFTSWithoutContentFields]); end; end; { TSQLRecordRTreeAbstract } class function TSQLRecordRTreeAbstract.RTreeSQLFunctionName: RawUTF8; begin result := RecordProps.SQLTableName+'_in'; end; { TSQLRecordRTree } class procedure TSQLRecordRTree.BlobToCoord(const InBlob; var OutCoord: TSQLRecordTreeCoords); begin // direct memory copy with no memory check MoveFast(InBlob,OutCoord,(RecordProps.RTreeCoordBoundaryFields shr 1)*SizeOf(double)); end; class function TSQLRecordRTree.ContainedIn(const BlobA,BlobB): boolean; var A, B: TSQLRecordTreeCoords; i: integer; begin BlobToCoord(BlobA,A); BlobToCoord(BlobB,B); result := false; for i := 0 to (RecordProps.RTreeCoordBoundaryFields shr 1)-1 do if (A[i].maxB[i].max) then exit; // no match result := true; // box match end; { TSQLRecordRTreeInteger } class procedure TSQLRecordRTreeInteger.BlobToCoord(const InBlob; var OutCoord: TSQLRecordTreeCoordsInteger); begin // direct memory copy with no memory check MoveFast(InBlob,OutCoord,(RecordProps.RTreeCoordBoundaryFields shr 1)*SizeOf(integer)); end; class function TSQLRecordRTreeInteger.ContainedIn(const BlobA,BlobB): boolean; var A,B: TSQLRecordTreeCoordsInteger; i: integer; begin BlobToCoord(BlobA,A); BlobToCoord(BlobB,B); result := false; for i := 0 to (RecordProps.RTreeCoordBoundaryFields shr 1)-1 do if (A[i].maxB[i].max) then exit; // no match result := true; // box match end; { TSQLRecordMany } constructor TSQLRecordMany.Create; begin inherited Create; with RecordProps do if (fRecordManySourceProp<>nil) and (fRecordManyDestProp<>nil) then begin fSourceID := fRecordManySourceProp.GetFieldAddr(Self); fDestID := fRecordManyDestProp.GetFieldAddr(Self); end; end; function TSQLRecordMany.ManyAdd(aClient: TSQLRest; aSourceID, aDestID: TID; NoDuplicates: boolean; aUseBatch: TSQLRestBatch): boolean; begin result := false; if (self=nil) or (aClient=nil) or (aSourceID=0) or (aDestID=0) or (fSourceID=nil) or (fDestID=nil) then exit; // invalid parameters if NoDuplicates and (InternalIDFromSourceDest(aClient,aSourceID,aDestID)<>0) then exit; // this TRecordReference pair already exists fSourceID^ := aSourceID; fDestID^ := aDestID; if aUseBatch<>nil then result := aUseBatch.Add(self,true)>=0 else result := aClient.Add(self,true)<>0; end; function TSQLRecordMany.ManyAdd(aClient: TSQLRest; aDestID: TID; NoDuplicates: boolean): boolean; begin if (self=nil) or (fSourceID=nil) then result := false else // avoid GPF result := ManyAdd(aClient,fSourceID^,aDestID,NoDuplicates); end; function TSQLRecordMany.DestGet(aClient: TSQLRest; aSourceID: TID; out DestIDs: TIDDynArray): Boolean; var Where: RawUTF8; begin Where := IDWhereSQL(aClient,aSourceID,False); if Where='' then result := False else result := aClient.OneFieldValues(RecordClass,'Dest',Where,TInt64DynArray(DestIDs)); end; function TSQLRecordMany.DestGetJoined(aClient: TSQLRest; const aDestWhereSQL: RawUTF8; aSourceID: TID; out DestIDs: TIDDynArray): boolean; var aTable: TSQLTable; begin aTable := DestGetJoinedTable(aClient,aDestWhereSQL,aSourceID,jkDestID); if aTable=nil then Result := False else try aTable.GetRowValues(0,TInt64DynArray(DestIDs)); Result := true; finally aTable.Free; end; end; function TSQLRecordMany.DestGetJoined(aClient: TSQLRest; const aDestWhereSQL: RawUTF8; aSourceID: TID): TSQLRecord; var aTable: TSQLTable; begin aTable := DestGetJoinedTable(aClient,aDestWhereSQL,aSourceID,jkDestFields); if aTable=nil then Result := nil else begin Result := TSQLRecordClass(RecordProps.fRecordManyDestProp.ObjectClass).Create; aTable.OwnerMustFree := true; Result.FillPrepare(aTable,ctnTrimExisting); end; end; function TSQLRecordMany.DestGetJoinedTable(aClient: TSQLRest; const aDestWhereSQL: RawUTF8; aSourceID: TID; JoinKind: TSQLRecordManyJoinKind; const aCustomFieldsCSV: RawUTF8): TSQLTable; var Select, SQL: RawUTF8; SelfProps, DestProps: TSQLModelRecordProperties; procedure SelectFields(const Classes: array of TSQLModelRecordProperties); var i: integer; begin for i := 0 to high(Classes) do begin Select := Select+Classes[i].SQL.TableSimpleFields[True,True]; if inil then aSourceID := fSourceID^; if aSourceID=0 then exit; SelfProps := aClient.Model.Props[PSQLRecordClass(self)^]; DestProps := aClient.Model.Props[TSQLRecordClass(SelfProps.Props.fRecordManyDestProp.ObjectClass)]; case JoinKind of jkDestID: Select := DestProps.Props.SQLTableName+'.RowID'; jkPivotID: Select := SelfProps.Props.SQLTableName+'.RowID'; jkDestFields: if aCustomFieldsCSV='' then SelectFields([DestProps]) else Select := AddPrefixToCSV(pointer(aCustomFieldsCSV),DestProps.Props.SQLTableName+'.'); jkPivotFields: if aCustomFieldsCSV='' then SelectFields([SelfProps]) else Select := AddPrefixToCSV(pointer(aCustomFieldsCSV),SelfProps.Props.SQLTableName+'.'); jkPivotAndDestFields: if aCustomFieldsCSV='' then SelectFields([SelfProps,DestProps]) else Select := aCustomFieldsCSV; end; if aDestWhereSQL='' then // fast inlined prepared statement SQL := 'SELECT % FROM %,% WHERE %.Source=:(%): AND %.Dest=%.RowID' else if PosEx(RawUTF8(':('),aDestWhereSQL,1)>0 then // statement is globaly inlined -> cache prepared statement SQL := 'SELECT % FROM %,% WHERE %.Source=:(%): AND %.Dest=%.RowID AND %' else // statement is not globaly inlined -> no caching of prepared statement SQL := 'SELECT % FROM %,% WHERE %.Source=% AND %.Dest=%.RowID AND %'; result := aClient.ExecuteList([PSQLRecordClass(Self)^, TSQLRecordClass(SelfProps.Props.fRecordManyDestProp.ObjectClass)], FormatUTF8(SQL, [Select, DestProps.Props.SQLTableName,SelfProps.Props.SQLTableName, SelfProps.Props.SQLTableName,aSourceID, SelfProps.Props.SQLTableName, DestProps.Props.SQLTableName, aDestWhereSQL])); end; function TSQLRecordMany.DestGet(aClient: TSQLRest; out DestIDs: TIDDynArray): boolean; begin if fSourceID=nil then result := false else // avoid GPF result := DestGet(aClient,fSourceID^,DestIDs); // fSourceID has been set by TSQLRecord.Create end; function TSQLRecordMany.ManyDelete(aClient: TSQLRest; aSourceID, aDestID: TID; aUseBatch: TSQLRestBatch): boolean; var aID: TID; begin result := false; if (self=nil) or (aClient=nil) or (aSourceID=0) or (aDestID=0) then exit; aID := InternalIDFromSourceDest(aClient,aSourceID,aDestID); if aID<>0 then if aUseBatch<>nil then result := aUseBatch.Delete(RecordClass,aID)>=0 else result := aClient.Delete(RecordClass,aID); end; function TSQLRecordMany.ManyDelete(aClient: TSQLRest; aDestID: TID): boolean; begin if fSourceID=nil then result := false else // avoid GPF result := ManyDelete(aClient,fSourceID^,aDestID,nil); end; function TSQLRecordMany.ManySelect(aClient: TSQLRest; aSourceID, aDestID: TID): boolean; begin if (self=nil) or (aClient=nil) or (aSourceID=0) or (aDestID=0) then result := false else // invalid parameters result := aClient.Retrieve(FormatUTF8('Source=:(%): AND Dest=:(%):', [aSourceID,aDestID]),Self); end; function TSQLRecordMany.ManySelect(aClient: TSQLRest; aDestID: TID): boolean; begin if (self=nil) or (fSourceID=nil) then result := false else // avoid GPF result := ManySelect(aClient,fSourceID^,aDestID); end; function TSQLRecordMany.InternalFillMany(aClient: TSQLRest; aID: TID; const aAndWhereSQL: RawUTF8; isDest: boolean): integer; var aTable: TSQLTable; Where: RawUTF8; begin result := 0; if self=nil then exit; if not isDest and (aID=0)then if fSourceID<>nil then aID := fSourceID^; // has been set by TSQLRecord.Create Where := IDWhereSQL(aClient,aID,isDest,aAndWhereSQL); if Where='' then exit; aTable := aClient.MultiFieldValues(RecordClass,'',Where); if aTable=nil then exit; aTable.OwnerMustFree := true; FillPrepare(aTable); // temporary storage for FillRow, FillOne and FillRewind result := aTable.fRowCount; end; function TSQLRecordMany.FillMany(aClient: TSQLRest; aSourceID: TID; const aAndWhereSQL: RawUTF8): integer; begin result := InternalFillMany(aclient,aSourceID,aAndWhereSQL,false); end; function TSQLRecordMany.FillManyFromDest(aClient: TSQLRest; aDestID: TID; const aAndWhereSQL: RawUTF8): integer; begin result := InternalFillMany(aclient,aDestID,aAndWhereSQL,true); end; function TSQLRecordMany.IDWhereSQL(aClient: TSQLRest; aID: TID; isDest: boolean; const aAndWhereSQL: RawUTF8): RawUTF8; const FieldName: array[boolean] of RawUTF8 = ('Source=','Dest='); begin if (self=nil) or (aID=0) or (fSourceID=nil) or (fDestID=nil) or (aClient=nil) then Result := '' else begin if aAndWhereSQL<>'' then if PosEx(RawUTF8(':('),aAndWhereSQL,1)>0 then Result := '%:(%): AND %' else // inlined parameters Result := '%% AND %' else // no inlined parameters -> not cached Result := '%:(%):'; // no additional where clause -> inline ID Result := FormatUTF8(result,[FieldName[isDest],aID,aAndWhereSQL]); end; end; function TSQLRecordMany.SourceGet(aClient: TSQLRest; aDestID: TID; out SourceIDs: TIDDynArray): boolean; var Where: RawUTF8; begin Where := IDWhereSQL(aClient,aDestID,True); if Where='' then Result := false else Result := aClient.OneFieldValues(RecordClass,'Source',Where,TInt64DynArray(SourceIDs)); end; function TSQLRecordMany.InternalIDFromSourceDest(aClient: TSQLRest; aSourceID, aDestID: TID): TID; begin SetID(aClient.OneFieldValue(RecordClass,'RowID', FormatUTF8('Source=:(%): AND Dest=:(%):',[aSourceID,aDestID])),result); end; { TSQLRestTempStorage } constructor TSQLRestTempStorage.Create(aClass: TSQLRecordClass); begin inherited Create; fStoredClass := aClass; fStoredClassRecordProps := aClass.RecordProps; fItems.InitSpecific( TypeInfo(TSQLRestTempStorageItemDynArray),fItem,djInt64,@fCount); fItems.Sorted := true; // space for 524287 fake items (our sorted array will not like bigger extent) fLastFakeID := $100000000000; end; destructor TSQLRestTempStorage.Destroy; var i: integer; begin for i := 0 to fCount-1 do fItem[i].Value.Free; inherited; end; procedure TSQLRestTempStorage.InternalAddItem(const item: TSQLRestTempStorageItem); begin fItems.Add(item); if (fCount>1) and (fItem[fCount-2].ID>item.ID) then fItems.Sort else // ensure IDs are in increasing order fItems.Sorted := true; // pessimistic fItems.Add() did reset to false end; function TSQLRestTempStorage.InternalSetFields(const FieldNames: RawUTF8; out Fields: TSQLFieldBits): Boolean; begin if FieldNames='' then Fields := fStoredClassRecordProps.SimpleFieldsBits[soUpdate] else if FieldNames='*' then FillCharFast(Fields,SizeOf(Fields),255) else if not fStoredClassRecordProps.FieldBitsFromCSV(FieldNames,Fields) then begin result := false; // invalid FieldNames content exit; end; result := True; end; function TSQLRestTempStorage.AddCopy(Value: TSQLRecord; ForceID: boolean; const FieldNames: RawUTF8): TID; begin if (self=nil) or (Value=nil) then result := 0 else result := AddOwned(Value.CreateCopy,ForceID,FieldNames); end; function TSQLRestTempStorage.AddOwned(Value: TSQLRecord; ForceID: boolean; const Fields: TSQLFieldBits): TID; var item: TSQLRestTempStorageItem; begin result := 0; if (self=nil) or (Value=nil) or (ForceID and (Value.IDValue=0)) or IsZero(Fields) then exit; item.ValueFields := Fields; fSafe.Lock; try if ForceID then begin item.ID := Value.IDValue; if fItems.Find(item)>=0 then begin Value.Free; // avoid memory leak exit; // this forced ID is already existing! end; item.Kind := [itemInsert]; end else begin inc(fLastFakeID); item.ID := fLastFakeID; Value.IDValue := fLastFakeID; item.Kind := [itemInsert,itemFakeID]; end; item.Value := Value; // instance will be owned by the list InternalAddItem(item); finally Safe.UnLock; end; result := item.ID; end; function TSQLRestTempStorage.AddOwned(Value: TSQLRecord; ForceID: boolean; const FieldNames: RawUTF8): TID; var fields: TSQLFieldBits; begin if (self=nil) or not InternalSetFields(FieldNames,fields) then result := 0 else result := AddOwned(Value,ForceID,fields); end; procedure TSQLRestTempStorage.Delete(const ID: TID); var i: integer; item: TSQLRestTempStorageItem; begin if (self=nil) or (ID=0) then exit; fSafe.Lock; try i := fItems.Find(ID); if i>=0 then with fItem[i] do begin FreeAndNil(Value); // Value=nil indicates deleted reord if itemInsert in Kind then fItems.Delete(i); // Add + Delete in place -> ignore this entry exit; end; item.ID := ID; item.Value := nil; // Value=nil indicates deleted record FillZero(item.ValueFields); InternalAddItem(item); finally Safe.UnLock; end; end; function TSQLRestTempStorage.Update(Value: TSQLRecord; const Fields: TSQLFieldBits): boolean; var i,f: integer; item: TSQLRestTempStorageItem; existing: ^TSQLRestTempStorageItem; begin result := false; if (self=nil) or (Value=nil) or (Value.IDValue=0) or IsZero(fields) then exit; item.ID := Value.IDValue; item.ValueFields := Fields; fSafe.Lock; try i := fItems.Find(item); if i>=0 then begin existing := @fItem[i]; if existing.Value=nil then exit; // impossible to update a deleted record existing^.ValueFields := existing^.ValueFields+item.ValueFields; for f := 0 to fStoredClassRecordProps.Fields.Count-1 do if f in item.ValueFields then fStoredClassRecordProps.Fields.List[f].CopyValue(Value,existing^.Value); end else begin item.Value := Value.CreateCopy; FillZero(item.ValueFields); InternalAddItem(item); end; result := true; finally Safe.UnLock; end; end; function TSQLRestTempStorage.Update(Value: TSQLRecord; const FieldNames: RawUTF8): boolean; var fields: TSQLFieldBits; begin if (self<>nil) and InternalSetFields(FieldNames,fields) then result := Update(Value,fields) else result := false; end; function TSQLRestTempStorage.FlushAsBatch(Rest: TSQLRest; AutomaticTransactionPerRow: cardinal): TSQLRestBatch; var i: integer; begin if (self=nil) or (fCount=0) then begin result := nil; exit; end; result := TSQLRestBatch.Create(Rest,fStoredClass,AutomaticTransactionPerRow,[]); fSafe.Lock; try for i := 0 to fCount-1 do with fItem[i] do if Value=nil then result.Delete(ID) else begin if itemInsert in Kind then result.Add(Value,true,not(itemFakeID in Kind),ValueFields) else result.Update(Value,ValueFields); FreeAndNil(Value); end; fItems.Clear; finally Safe.UnLock; end; end; function TSQLRestTempStorage.FromEvent(Event: TSQLEvent; ID: TID; const JSON: RawUTF8): boolean; var Value: TSQLRecord; fields: TSQLFieldBits; begin if (self=nil) or (ID=0) then begin result := false; exit; end; if Event=seDelete then begin Delete(ID); result := true; exit; end; Value := fStoredClass.Create; try Value.FillFrom(JSON,@fields); Value.IDValue := ID; case Event of seAdd: begin result := AddOwned(Value,True,fields)<>0; Value := nil; // owned by the list end; seUpdate,seUpdateBlob: result := Update(Value,fields); else result := false; end; finally Value.Free; end; end; { TSQLRecordProperties } procedure TSQLRecordProperties.InternalRegisterModel(aModel: TSQLModel; aTableIndex: integer; aProperties: TSQLModelRecordProperties); var i: integer; begin //assert(aTableIndex>=0); EnterCriticalSection(fLock); // may be called from several threads at once try for i := 0 to fModelMax do if fModel[i].Model=aModel then exit; // already registered inc(fModelMax); if fModelMax>=length(fModel) then SetLength(fModel,fModelMax+4); with fModel[fModelMax] do begin Model := aModel; Properties := aProperties; TableIndex := aTableIndex; end; finally LeaveCriticalSection(fLock); end; end; const // the most ambigous keywords - others may be used as column names SQLITE3_KEYWORDS = ' from where group in as '; constructor TSQLRecordProperties.Create(aTable: TSQLRecordClass); var i,j, nProps: integer; nMany, nSQLRecord, nSimple, nDynArray, nBlob, nBlobCustom, nCopiableFields: integer; isTSQLRecordMany: boolean; F: TSQLPropInfo; label Simple, Small, Copiabl; begin InitializeCriticalSection(fLock); if aTable=nil then raise EModelException.Create('TSQLRecordProperties.Create(nil)'); // register for JSONToObject() and for TSQLPropInfoRTTITID.Create() // (should have been done before in TSQLModel.Create/AddTable) TJSONSerializer.RegisterClassForJSON(aTable); // initialize internal structures fModelMax := -1; fTable := aTable; fSQLTableName := GetDisplayNameFromClass(aTable); fSQLTableNameUpperWithDot := SynCommons.UpperCase(SQLTableName)+'.'; isTSQLRecordMany := aTable.InheritsFrom(TSQLRecordMany); // add properties to internal Fields list fClassType := PTypeInfo(aTable.ClassInfo)^.ClassType; fClassProp := InternalClassProp(aTable); nProps := ClassFieldCountWithParents(aTable); if nProps>MAX_SQLFIELDS_INCLUDINGID then raise EModelException.CreateUTF8('% has too many fields: %>=%', [Table,nProps,MAX_SQLFIELDS]); fFields := TSQLPropInfoList.Create(aTable,[pilRaiseEORMExceptionIfNotHandled]); aTable.InternalRegisterCustomProperties(self); if Fields.Count>MAX_SQLFIELDS_INCLUDINGID then raise EModelException.CreateUTF8( '% has too many fields after InternalRegisterCustomProperties(%): %>=%', [Table,self,Fields.Count,MAX_SQLFIELDS]); SetLength(Fields.fList,Fields.Count); // generate some internal lookup information fSQLTableRetrieveAllFields := 'ID'; SetLength(fManyFields,MAX_SQLFIELDS); SetLength(fSimpleFields,MAX_SQLFIELDS); SetLength(fJoinedFields,MAX_SQLFIELDS); SetLength(fCopiableFields,MAX_SQLFIELDS); SetLength(fDynArrayFields,MAX_SQLFIELDS); SetLength(fBlobCustomFields,MAX_SQLFIELDS); SetLength(fBlobFields,MAX_SQLFIELDS); MainField[false] := -1; MainField[true] := -1; nMany := 0; nSimple := 0; nSQLRecord := 0; nCopiableFields := 0; nDynArray := 0; nBlob := 0; nBlobCustom := 0; for i := 0 to Fields.Count-1 do begin F := Fields.List[i]; // check field name if IsRowID(pointer(F.Name)) then raise EModelException.CreateUTF8('ID is already defined in TSQLRecord: '+ '%.% field name is not allowed as published property',[Table,F.Name]); if PosEx(' '+LowerCase(F.Name)+' ',SQLITE3_KEYWORDS)>0 then raise EModelException.CreateUTF8('%.% field name conflicts with a SQL keyword',[Table,F.Name]); // handle unique fields, i.e. if marked as "stored false" if aIsUnique in F.Attributes then begin include(IsUniqueFieldsBits,i); // must trim() text value before storage, and validate for unicity if F.SQLFieldType in [sftUTF8Text,sftAnsiText] then AddFilterOrValidate(i,TSynFilterTrim.Create); AddFilterOrValidate(i,TSynValidateUniqueField.Create); end; // get corresponding properties content include(fHasTypeFields,F.SQLFieldType); include(FieldBits[F.SQLFieldType],i); case F.SQLFieldType of sftUnknown: ; sftUTF8Text: begin if aIsUnique in F.Attributes then if MainField[false]<0 then MainField[false] := i; if MainField[true]<0 then MainField[true] := i; goto Small; end; sftBlob: begin BlobFields[nBlob] := F as TSQLPropInfoRTTI; inc(nBlob); fSQLTableUpdateBlobFields := fSQLTableUpdateBlobFields+F.Name+'=?,'; fSQLTableRetrieveBlobFields := fSQLTableRetrieveBlobFields+F.Name+','; fSQLTableRetrieveAllFields := fSQLTableRetrieveAllFields+','+F.Name; goto Copiabl; end; sftID: // = TSQLRecord(aID) if isTSQLRecordMany and (IdemPropNameU(F.Name,'Source') or IdemPropNameU(F.Name,'Dest')) then goto Small else begin JoinedFields[nSQLRecord] := F as TSQLPropInfoRTTIID; inc(nSQLRecord); goto Small; end; sftMany: begin ManyFields[nMany] := F as TSQLPropInfoRTTIMany; inc(nMany); end; sftBlobDynArray: with F as TSQLPropInfoRTTIDynArray do begin if DynArrayIndex>0 then for j := 0 to nDynArray-1 do if DynArrayFields[j].DynArrayIndex=DynArrayIndex then raise EModelException.CreateUTF8('dup index % for %.% and %.% properties', [DynArrayIndex,Table,Name,Table,DynArrayFields[j].Name]); DynArrayFields[nDynArray] := TSQLPropInfoRTTIDynArray(F); if TSQLPropInfoRTTIDynArray(F).ObjArray<>nil then fDynArrayFieldsHasObjArray := true; inc(nDynArray); goto Simple; end; sftBlobCustom, sftUTF8Custom: begin BlobCustomFields[nBlobCustom] := F; inc(nBlobCustom); goto Simple; end; sftCreateTime: begin include(ComputeBeforeAddFieldsBits,i); goto Small; end; sftModTime, sftSessionUserID: begin include(ComputeBeforeAddFieldsBits,i); include(ComputeBeforeUpdateFieldsBits,i); goto Small; end; sftRecordVersion: begin if fRecordVersionField<>nil then raise EModelException.CreateUTF8('%: only a single TRecordVersion '+ 'field is allowed per class',[Table]); fRecordVersionField := F as TSQLPropInfoRTTIRecordVersion; fSQLTableRetrieveAllFields := fSQLTableRetrieveAllFields+','+F.Name; goto Copiabl; end; // TRecordVersion is a copiable but not a simple field! sftVariant: // sftNullable are included in SmallfieldsBits goto Simple; else begin Small: include(SmallFieldsBits,i); // this code follows NOT_SIMPLE_FIELDS/COPIABLE_FIELDS constants Simple: SimpleFields[nSimple] := F; inc(nSimple); include(SimpleFieldsBits[soSelect],i); fSQLTableSimpleFieldsNoRowID := fSQLTableSimpleFieldsNoRowID+F.Name+','; fSQLTableRetrieveAllFields := fSQLTableRetrieveAllFields+','+F.Name; Copiabl:include(CopiableFieldsBits,i); CopiableFields[nCopiableFields] := F; inc(nCopiableFields); end; end; end; if fSQLTableSimpleFieldsNoRowID<>'' then SetLength(fSQLTableSimpleFieldsNoRowID,length(fSQLTableSimpleFieldsNoRowID)-1); if fSQLTableUpdateBlobFields<>'' then SetLength(fSQLTableUpdateBlobFields,length(fSQLTableUpdateBlobFields)-1); if fSQLTableRetrieveBlobFields<>'' then SetLength(fSQLTableRetrieveBlobFields,length(fSQLTableRetrieveBlobFields)-1); SetLength(fManyFields,nMany); SetLength(fSimpleFields,nSimple); SetLength(fJoinedFields,nSQLRecord); if nSQLRecord>0 then begin SetLength(fJoinedFieldsTable,nSQLRecord+1); fJoinedFieldsTable[0] := aTable; for i := 0 to nSQLRecord-1 do fJoinedFieldsTable[i+1] := TSQLRecordClass(JoinedFields[i].ObjectClass); end; SetLength(fCopiableFields,nCopiableFields); SetLength(fDynArrayFields,nDynArray); SetLength(fBlobCustomFields,nBlobCustom); SetLength(fBlobFields,nBlob); SimpleFieldsBits[soInsert] := SimpleFieldsBits[soSelect]; SimpleFieldsBits[soUpdate] := SimpleFieldsBits[soSelect]; SimpleFieldsBits[soDelete] := SimpleFieldsBits[soSelect]; SimpleFieldsCount[soInsert] := nSimple; SimpleFieldsCount[soUpdate] := nSimple; SimpleFieldsCount[soDelete] := nSimple; fHasNotSimpleFields := nSimple<>Fields.Count; for i := 0 to Fields.Count-1 do if Fields.List[i].SQLFieldType=sftCreateTime then begin exclude(SimpleFieldsBits[soUpdate],i); dec(SimpleFieldsCount[soUpdate]); end; if SmallFieldsBits<>SimpleFieldsBits[soSelect]-FieldBits[sftVariant]- FieldBits[sftBlobDynArray]-FieldBits[sftBlobCustom]-FieldBits[sftUTF8Custom] then raise EModelException.CreateUTF8('TSQLRecordProperties.Create(%) Bits?',[Table]); if isTSQLRecordMany then begin fRecordManySourceProp := Fields.ByRawUTF8Name('Source') as TSQLPropInfoRTTIInstance; if fRecordManySourceProp=nil then raise EModelException.CreateUTF8('% expects a SOURCE field',[Table]) else fRecordManyDestProp := Fields.ByRawUTF8Name('Dest') as TSQLPropInfoRTTIInstance; if fRecordManyDestProp=nil then raise EModelException.CreateUTF8('% expects a DEST field',[Table]); end; end; function TSQLRecordProperties.BlobFieldPropFromRawUTF8(const PropName: RawUTF8): PPropInfo; var i: integer; begin if (self<>nil) and (PropName<>'') then for i := 0 to high(BlobFields) do if IdemPropNameU(BlobFields[i].Name,PropName) then begin result := BlobFields[i].PropInfo; exit; end; result := nil; end; function TSQLRecordProperties.BlobFieldPropFromUTF8(PropName: PUTF8Char; PropNameLen: integer): PPropInfo; var i: integer; begin if (self<>nil) and (PropName<>'') then for i := 0 to high(BlobFields) do if IdemPropName(BlobFields[i].PropInfo^.Name,PropName,PropNameLen) then begin result := BlobFields[i].PropInfo; exit; end; result := nil; end; function TSQLRecordProperties.SQLFieldTypeToSQL(FieldIndex: integer): RawUTF8; const /// simple wrapper from each SQL used type into SQLite3 field datatype // - set to '' for fields with no column created in the database DEFAULT_SQLFIELDTYPETOSQL: array[TSQLFieldType] of RawUTF8 = ('', // sftUnknown ' TEXT COLLATE NOCASE, ', // sftAnsiText ' TEXT COLLATE SYSTEMNOCASE, ', // sftUTF8Text ' INTEGER, ', // sftEnumerate ' INTEGER, ', // sftSet ' INTEGER, ', // sftInteger ' INTEGER, ', // sftID = TSQLRecord(aID) ' INTEGER, ', // sftRecord = TRecordReference ' INTEGER, ', // sftBoolean ' FLOAT, ', // sftFloat ' TEXT COLLATE ISO8601, ', // sftDateTime ' INTEGER, ', // sftTimeLog ' FLOAT, ', // sftCurrency ' TEXT COLLATE BINARY, ', // sftObject {$ifndef NOVARIANTS} ' TEXT COLLATE BINARY, ', // sftVariant ' TEXT COLLATE NOCASE, ', // sftNullable (from SQLFieldTypeStored) {$endif NOVARIANTS} ' BLOB, ', // sftBlob ' BLOB, ', // sftBlobDynArray ' BLOB, ', // sftBlobCustom ' TEXT COLLATE NOCASE, ', // sftUTF8Custom '', // sftMany ' INTEGER, ', // sftModTime ' INTEGER, ', // sftCreateTime ' INTEGER, ', // sftTID ' INTEGER, ', // sftRecordVersion ' INTEGER, ', // sftSessionUserID ' TEXT COLLATE ISO8601, ', // sftDateTimeMS ' INTEGER, ', // sftUnixTime ' INTEGER, '); // sftUnixMSTime begin if (self=nil) or (cardinal(FieldIndex)>=cardinal(Fields.Count)) then result := '' else if (FieldIndex'') then result := ' TEXT COLLATE '+fCustomCollation[FieldIndex]+', ' else result := DEFAULT_SQLFIELDTYPETOSQL[Fields.List[FieldIndex].SQLFieldTypeStored]; end; function TSQLRecordProperties.SetCustomCollation(FieldIndex: integer; const aCollationName: RawUTF8): boolean; begin result := (self<>nil) and (cardinal(FieldIndex)length(fCustomCollation) then SetLength(fCustomCollation,Fields.Count); fCustomCollation[FieldIndex] := aCollationName; with Fields.List[FieldIndex] do if IdemPropNameU(aCollationName,'BINARY') then include(fAttributes,aBinaryCollation) else exclude(fAttributes,aBinaryCollation); end; end; function TSQLRecordProperties.SetCustomCollation(const aFieldName, aCollationName: RawUTF8): boolean; begin result := SetCustomCollation(Fields.IndexByNameOrExcept(aFieldName),aCollationName); end; procedure TSQLRecordProperties.SetCustomCollationForAll(aFieldType: TSQLFieldType; const aCollationName: RawUTF8); var i: integer; begin if (self<>nil) and not(aFieldType in [sftUnknown,sftMany]) then for i := 0 to Fields.Count-1 do if Fields.List[i].SQLFieldTypeStored=aFieldType then SetCustomCollation(i,aCollationName); end; procedure TSQLRecordProperties.SetMaxLengthValidatorForTextFields(IndexIsUTF8Length: boolean); var i: integer; begin if self<>nil then for i := 0 to Fields.Count-1 do with Fields.List[i] do if (SQLDBFieldType in TEXT_DBFIELDS) and (cardinal(FieldWidth-1)<262144) then AddFilterOrValidate(i,TSynValidateText.CreateUTF8('{maxLength:%,UTF8Length:%}', [FieldWidth,IndexIsUTF8Length],[])); end; procedure TSQLRecordProperties.SetMaxLengthFilterForTextFields(IndexIsUTF8Length: boolean); var i: integer; begin if self<>nil then for i := 0 to Fields.Count-1 do with Fields.List[i] do if (SQLDBFieldType in TEXT_DBFIELDS) and (cardinal(FieldWidth-1)<262144) then AddFilterOrValidate(i,TSynFilterTruncate.CreateUTF8('{maxLength:%,UTF8Length:%}', [FieldWidth,IndexIsUTF8Length],[])); end; {$ifndef NOVARIANTS} procedure TSQLRecordProperties.SetVariantFieldsDocVariantOptions(const Options: TDocVariantOptions); var i: integer; begin if self<>nil then for i := 0 to Fields.Count-1 do if (Fields.List[i].SQLFieldType=sftVariant) and Fields.List[i].InheritsFrom(TSQLPropInfoRTTIVariant) then TSQLPropInfoRTTIVariant(Fields.List[i]).DocVariantOptions := Options; end; {$endif} function TSQLRecordProperties.SQLAddField(FieldIndex: integer): RawUTF8; begin result := SQLFieldTypeToSQL(FieldIndex); if result='' then exit; // some fields won't have any column created in the database result := FormatUTF8('ALTER TABLE % ADD COLUMN %%', [SQLTableName,Fields.List[FieldIndex].Name,result]); if FieldIndex in IsUniqueFieldsBits then insert(' UNIQUE',result,length(result)-1); result[length(result)-1] := ';' // SQLFieldTypeToSQL[] ends with ',' end; procedure TSQLRecordProperties.SetJSONWriterColumnNames(W: TJSONSerializer; KnownRowsCount: integer); var i,n,nf: integer; begin // get col count overhead if W.withID then n := 1 else n := 0; // set col names nf := Length(W.Fields); SetLength(W.ColNames,nf+n); if W.withID then W.ColNames[0] := 'RowID'; // works for both normal and FTS3 records for i := 0 to nf-1 do begin W.ColNames[n] := Fields.List[W.Fields[i]].Name; inc(n); end; // write or init field names for appropriate JSON Expand W.AddColumns(KnownRowsCount); end; function TSQLRecordProperties.CreateJSONWriter(JSON: TStream; Expand, withID: boolean; const aFields: TSQLFieldBits; KnownRowsCount,aBufSize: integer): TJSONSerializer; begin result := CreateJSONWriter(JSON,Expand,withID, FieldBitsToIndex(aFields,Fields.Count),KnownRowsCount,aBufSize); end; function TSQLRecordProperties.CreateJSONWriter(JSON: TStream; Expand, withID: boolean; const aFields: TSQLFieldIndexDynArray; KnownRowsCount,aBufSize: integer): TJSONSerializer; begin if (self=nil) or ((Fields=nil) and not withID) then // no data result := nil else begin result := TJSONSerializer.Create(JSON,Expand,withID,aFields,aBufSize); SetJSONWriterColumnNames(result,KnownRowsCount); end; end; function TSQLRecordProperties.CreateJSONWriter(JSON: TStream; Expand: boolean; const aFieldsCSV: RawUTF8; KnownRowsCount,aBufSize: integer): TJSONSerializer; var withID: boolean; bits: TSQLFieldBits; begin FieldBitsFromCSV(aFieldsCSV,bits,withID); result := CreateJSONWriter(JSON,Expand,withID,bits,KnownRowsCount,aBufSize); end; function TSQLRecordProperties.SaveSimpleFieldsFromJsonArray(var P: PUTF8Char; var EndOfObject: AnsiChar; ExtendedJSON: boolean): RawUTF8; var i: integer; W: TJSONSerializer; Start: PUTF8Char; temp: TTextWriterStackBuffer; begin result := ''; if P=nil then exit; while (P^<=' ') and (P^<>#0) do inc(P); if P^<>'[' then exit; repeat inc(P) until (P^>' ') or (P^=#0); W := TJSONSerializer.CreateOwnedStream(temp); try W.Add('{'); for i := 0 to length(SimpleFields)-1 do begin if ExtendedJSON then begin W.AddString(SimpleFields[i].Name); W.Add(':'); end else W.AddFieldName(SimpleFields[i].Name); Start := P; P := GotoEndJSONItem(P); if (P=nil) or not(P^ in [',',']']) then exit; W.AddNoJSONEscape(Start,P-Start); W.Add(','); repeat inc(P) until (P^>' ') or (P^=#0); end; W.CancelLastComma; W.Add('}'); W.SetText(result); finally W.Free; end; EndOfObject := P^; if P^<>#0 then repeat inc(P) until (P^>' ') or (P^=#0); end; procedure TSQLRecordProperties.SaveBinaryHeader(W: TFileBufferWriter); var i: integer; FieldNames: TRawUTF8DynArray; begin W.Write(SQLTableName); SetLength(FieldNames,Fields.Count); for i := 0 to Fields.Count-1 do FieldNames[i] := Fields.List[i].Name; W.WriteRawUTF8DynArray(FieldNames,Fields.Count); for i := 0 to Fields.Count-1 do W.Write(@Fields.List[i].fSQLFieldType,SizeOf(TSQLFieldType)); end; function TSQLRecordProperties.CheckBinaryHeader(var R: TFileBufferReader): boolean; var n,i: integer; FieldNames: TRawUTF8DynArray; FieldTypes: array[0..MAX_SQLFIELDS-1] of TSQLFieldType; begin result := false; if (R.ReadRawUTF8<>SQLTableName) or (R.ReadVarRawUTF8DynArray(FieldNames)<>Fields.Count) then exit; n := SizeOf(TSQLFieldType)*Fields.Count; if R.Read(@FieldTypes,n)<>n then exit; for i := 0 to Fields.Count-1 do with Fields.List[i] do if (Name<>FieldNames[i]) or (SQLFieldType<>FieldTypes[i]) then exit; result := true; end; function TSQLRecordProperties.IsFieldName(const PropName: RawUTF8): boolean; begin result := (PropName<>'') and (isRowID(pointer(PropName)) or (Fields.IndexByName(PropName)>=0)); end; function TSQLRecordProperties.IsFieldNameOrFunction(const PropName: RawUTF8): boolean; var L: integer; begin L := length(PropName); if (L=0) or (self=nil) then result := false else if PropName[L]=')' then case IdemPCharArray(pointer(PropName),['MAX(','MIN(','AVG(','SUM(', 'JSONGET(','JSONHAS(']) of 0..3: result := IsFieldName(copy(PropName,5,L-5)); 4..5: result := IsFieldName(copy(PropName,9,PosExChar(',',PropName)-9)); else result := IsFieldName(PropName); end else result := IsFieldName(PropName); end; function TSQLRecordProperties.AddFilterOrValidate(aFieldIndex: integer; aFilter: TSynFilterOrValidate): boolean; begin if (self=nil) or (cardinal(aFieldIndex)>=cardinal(Fields.Count)) or (aFilter=nil) then result := false else begin if Filters=nil then SetLength(fFilters,Fields.Count); aFilter.AddOnce(Filters[aFieldIndex]); result := true; end; end; procedure TSQLRecordProperties.AddFilterOrValidate(const aFieldName: RawUTF8; aFilter: TSynFilterOrValidate); begin AddFilterOrValidate(Fields.IndexByNameOrExcept(aFieldName),aFilter); end; destructor TSQLRecordProperties.Destroy; var f: integer; begin for f := 0 to high(Filters) do ObjArrayClear(Filters[f]); // will free any created TSynFilter instances inherited; DeleteCriticalSection(fLock); Fields.Free; end; function TSQLRecordProperties.FieldBitsFromBlobField(aBlobField: PPropInfo; var Bits: TSQLFieldBits): boolean; var f: integer; begin FillZero(Bits); if self<>nil then for f := 0 to high(BlobFields) do if BlobFields[f].fPropInfo=aBlobField then begin Include(Bits,BlobFields[f].PropertyIndex); result := true; exit; end; result := false; end; function TSQLRecordProperties.FieldBitsFromCSV(const aFieldsCSV: RawUTF8; var Bits: TSQLFieldBits): boolean; var ndx: integer; P: PUTF8Char; FieldName: ShortString; begin FillZero(Bits); result := false; if self=nil then exit; P := pointer(aFieldsCSV); while P<>nil do begin GetNextItemShortString(P,FieldName); FieldName[ord(FieldName[0])+1] := #0; // make PUTF8Char ndx := Fields.IndexByName(@FieldName[1]); if ndx<0 then exit; // invalid field name include(Bits,ndx); end; result := true; end; function TSQLRecordProperties.FieldBitsFromCSV(const aFieldsCSV: RawUTF8; var Bits: TSQLFieldBits; out withID: boolean): boolean; var ndx: integer; P: PUTF8Char; FieldName: ShortString; begin if (aFieldsCSV='*') and (self<>nil) then begin Bits := SimpleFieldsBits[soSelect]; withID := true; result := true; exit; end; FillZero(Bits); withID := false; result := false; if self=nil then exit; P := pointer(aFieldsCSV); while P<>nil do begin GetNextItemShortString(P,FieldName); if IsRowIDShort(FieldName) then begin withID := true; continue; end; FieldName[ord(FieldName[0])+1] := #0; // make PUTF8Char ndx := Fields.IndexByName(@FieldName[1]); if ndx<0 then exit; // invalid field name include(Bits,ndx); end; result := true; end; function TSQLRecordProperties.FieldBitsFromCSV(const aFieldsCSV: RawUTF8): TSQLFieldBits; begin if not FieldBitsFromCSV(aFieldsCSV,Result) then FillZero(result); end; function TSQLRecordProperties.FieldBitsFromExcludingCSV( const aFieldsCSV: RawUTF8; aOccasion: TSQLOccasion): TSQLFieldBits; var excluded: TSQLFieldBits; begin result := SimpleFieldsBits[aOccasion]; if FieldBitsFromCSV(aFieldsCSV,excluded) then result := result-excluded; end; function TSQLRecordProperties.FieldBitsFromRawUTF8(const aFields: array of RawUTF8; var Bits: TSQLFieldBits): boolean; var f,ndx: integer; begin FillZero(Bits); result := false; if self=nil then exit; for f := 0 to high(aFields) do begin ndx := Fields.IndexByName(aFields[f]); if ndx<0 then exit; // invalid field name include(Bits,ndx); end; result := true; end; function TSQLRecordProperties.FieldBitsFromRawUTF8(const aFields: array of RawUTF8): TSQLFieldBits; begin if not FieldBitsFromRawUTF8(aFields,Result) then FillZero(result); end; function TSQLRecordProperties.CSVFromFieldBits(const Bits: TSQLFieldBits): RawUTF8; var f: integer; W: TTextWriter; temp: TTextWriterStackBuffer; begin W := TTextWriter.CreateOwnedStream(temp); try for f := 0 to Fields.Count-1 do if f in Bits then begin W.AddString(Fields.List[f].Name); W.Add(','); end; W.CancelLastComma; W.SetText(result); finally W.Free; end; end; function TSQLRecordProperties.FieldIndexDynArrayFromRawUTF8( const aFields: array of RawUTF8; var Indexes: TSQLFieldIndexDynArray): boolean; var f,ndx: integer; begin result := false; if self=nil then exit; for f := 0 to high(aFields) do begin ndx := Fields.IndexByName(aFields[f]); if ndx<0 then exit; // invalid field name AddFieldIndex(Indexes,ndx); end; result := true; end; function TSQLRecordProperties.FieldIndexDynArrayFromRawUTF8(const aFields: array of RawUTF8): TSQLFieldIndexDynArray; begin if not FieldIndexDynArrayFromRawUTF8(aFields,result) then result := nil; end; function TSQLRecordProperties.FieldIndexDynArrayFromCSV(const aFieldsCSV: RawUTF8; var Indexes: TSQLFieldIndexDynArray): boolean; var ndx: integer; P: PUTF8Char; FieldName: ShortString; begin result := false; if self=nil then exit; P := pointer(aFieldsCSV); while P<>nil do begin GetNextItemShortString(P,FieldName); FieldName[ord(FieldName[0])+1] := #0; // make PUTF8Char ndx := Fields.IndexByName(@FieldName[1]); if ndx<0 then exit; // invalid field name AddFieldIndex(Indexes,ndx); end; result := true; end; function TSQLRecordProperties.FieldIndexDynArrayFromCSV(const aFieldsCSV: RawUTF8): TSQLFieldIndexDynArray; begin if not FieldIndexDynArrayFromCSV(aFieldsCSV,result) then result := nil; end; function TSQLRecordProperties.FieldIndexDynArrayFromBlobField(aBlobField: PPropInfo; var Indexes: TSQLFieldIndexDynArray): boolean; var f: integer; begin if self<>nil then for f := 0 to high(BlobFields) do if BlobFields[f].fPropInfo=aBlobField then begin AddFieldIndex(Indexes,BlobFields[f].PropertyIndex); result := true; exit; end; result := false; end; function TSQLRecordProperties.AppendFieldName(FieldIndex: Integer; var Text: RawUTF8; ForceNoRowID: boolean): boolean; begin result := false; // success if FieldIndex=VIRTUAL_TABLE_ROWID_COLUMN then if ForceNoRowID then Text := Text+'ID' else Text := Text+'RowID' else if (self=nil) or (cardinal(FieldIndex)>=cardinal(Fields.Count)) then result := true else Text := Text+Fields.List[FieldIndex].Name; end; function TSQLRecordProperties.MainFieldName(ReturnFirstIfNoUnique: boolean=false): RawUTF8; begin if (self=nil) or (Table=nil) or (MainField[ReturnFirstIfNoUnique]<0) then result := '' else result := Fields.List[MainField[ReturnFirstIfNoUnique]].Name; end; procedure TSQLRecordProperties.RegisterCustomFixedSizeRecordProperty( aTable: TClass; aRecordSize: cardinal; const aName: RawUTF8; aPropertyPointer: pointer; aAttributes: TSQLPropInfoAttributes; aFieldWidth: integer; aData2Text: TOnSQLPropInfoRecord2Text; aText2Data: TOnSQLPropInfoRecord2Data); begin Fields.Add(TSQLPropInfoRecordFixedSize.Create(aRecordSize,aName,Fields.Count, aPropertyPointer,aAttributes,aFieldWidth,aData2Text,aText2Data)); end; procedure TSQLRecordProperties.RegisterCustomRTTIRecordProperty(aTable: TClass; aRecordInfo: PTypeInfo; const aName: RawUTF8; aPropertyPointer: pointer; aAttributes: TSQLPropInfoAttributes; aFieldWidth: integer; aData2Text: TOnSQLPropInfoRecord2Text; aText2Data: TOnSQLPropInfoRecord2Data); begin Fields.Add(TSQLPropInfoRecordRTTI.Create(aRecordInfo,aName,Fields.Count, aPropertyPointer,aAttributes,aFieldWidth,aData2Text,aText2Data)); end; procedure TSQLRecordProperties.RegisterCustomPropertyFromRTTI(aTable: TClass; aTypeInfo: PTypeInfo; const aName: RawUTF8; aPropertyPointer: pointer; aAttributes: TSQLPropInfoAttributes; aFieldWidth: integer); begin Fields.Add(TSQLPropInfoCustomJSON.Create(aTypeInfo,aName,Fields.Count, aPropertyPointer,aAttributes,aFieldWidth)); end; procedure TSQLRecordProperties.RegisterCustomPropertyFromTypeName(aTable: TClass; const aTypeName, aName: RawUTF8; aPropertyPointer: pointer; aAttributes: TSQLPropInfoAttributes; aFieldWidth: integer); begin Fields.Add(TSQLPropInfoCustomJSON.Create(aTypeName,aName,Fields.Count, aPropertyPointer,aAttributes,aFieldWidth)); end; { TSynValidateRest } function TSynValidateRest.Process(aFieldIndex: integer; const Value: RawUTF8; var ErrorMsg: string): boolean; begin Result := DoValidate(aFieldIndex, Value, ErrorMsg, fProcessRest, fProcessRec); end; function TSynValidateRest.Validate(aFieldIndex: integer; const Value: RawUTF8; var ErrorMsg: string; aProcessRest: TSQLRest; aProcessRec: TSQLRecord): boolean; begin try fProcessRest := aProcessRest; fProcessRec := aProcessRec; result := DoValidate(aFieldIndex,Value,ErrorMsg,aProcessRest,aProcessRec); finally fProcessRest := nil; fProcessRec := nil; end; end; { TSynValidateUniqueField } function TSynValidateUniqueField.DoValidate(aFieldIndex: integer; const Value: RawUTF8; var ErrorMsg: string; aProcessRest: TSQLRest; aProcessRec: TSQLRecord): boolean; var aID: TID; begin result := false; if Value='' then ErrorMsg := sValidationFieldVoid else if (aProcessRest=nil) or (aProcessRec=nil) then result := true else with aProcessRec.RecordProps do if cardinal(aFieldIndex)>=cardinal(Fields.Count) then result := true else begin SetID(aProcessRest.OneFieldValue(Table,'RowID', Fields.List[aFieldIndex].Name+'=:('+QuotedStr(Value,'''')+'):'),aID); if (aID>0) and (aID<>aProcessRec.fID) then ErrorMsg := sValidationFieldDuplicate else result := true; end; end; { TSynValidateUniqueFields } procedure TSynValidateUniqueFields.SetParameters(const Value: RawUTF8); var V: array[0..0] of TValuePUTF8Char; tmp: TSynTempBuffer; begin tmp.Init(Value); try JSONDecode(tmp.buf,['FieldNames'],@V,True); CSVToRawUTF8DynArray(V[0].Value,fFieldNames); finally tmp.Done; end; end; function TSynValidateUniqueFields.DoValidate(aFieldIndex: integer; const Value: RawUTF8; var ErrorMsg: string; aProcessRest: TSQLRest; aProcessRec: TSQLRecord): boolean; var where: RawUTF8; i: integer; aID: TID; begin if (aProcessRest=nil) or (aProcessRec=nil) or (fFieldNames=nil) then result := true else begin for i := 0 to high(fFieldNames) do begin if where<>'' then where := where+' AND '; where := where+fFieldNames[i]+'=:('+ QuotedStr(aProcessRec.GetFieldValue(fFieldNames[i]),'''')+'):'; end; SetID(aProcessRest.OneFieldValue(aProcessRec.RecordClass,'ID',where),aID); if (aID>0) and (aID<>aProcessRec.fID) then begin ErrorMsg := sValidationFieldDuplicate; result := false; end else result := true; end; end; { TJSONSerializer } procedure TJSONSerializer.WriteObject(Value: TObject; Options: TTextWriterWriteObjectOptions); var Added: boolean; CustomComment: RawUTF8; CustomPropName: PShortString; IsObj: TJSONObject; WS: WideString; {$ifdef HASVARUSTRING} US: UnicodeString; {$endif} tmp: RawByteString; {$ifndef NOVARIANTS} VV: variant; {$endif} procedure HR(P: PPropInfo=nil); begin if woHumanReadable in Options then begin if CustomComment<>'' then begin AddShort(' // '); AddString(CustomComment); CustomComment := ''; end; AddCRAndIndent; end; if P=nil then exit; if CustomPropName<>nil then AddPropName(CustomPropName^) else AddPropName(P^.Name); // handle twoForceJSONExtended in CustomOptions if woHumanReadable in Options then Add(' '); Added := true; end; procedure WriteProp(P: PPropInfo); var D64: double; V64: Int64; Obj: TObject; V, c, codepage: integer; po: PObject; Kind: TTypeKind; PS: PShortString; dyn: TDynArray; dynObjArray: PClassInstance; begin if Assigned(OnWriteObject) and OnWriteObject(self,Value,P,Options) then exit; if IsObj in [oSQLRecord,oSQLMany] then begin // ignore "stored AS_UNIQUE" if IsRowIDShort(P^.Name) then exit; // should not happen end else if not (woStoreStoredFalse in Options) and not P^.IsStored(Value) then exit; // ignore regular "stored false" attribute Added := false; // HR(P) will write field name and set Added := true Kind := P^.PropType^.Kind; case Kind of tkInt64{$ifdef FPC}, tkQWord{$endif}: begin V64 := P^.GetInt64Prop(Value); if not ((woDontStoreDefault in Options) and (V64=Int64(P^.Default))) and not ((V64=0) and (woDontStore0 in Options)) then begin HR(P); if woTimeLogAsText in Options then case P^.PropType^.GetSQLFieldType of sftTimeLog,sftModTime,sftCreateTime: begin Add('"'); AddTimeLog(@V64); Add('"',','); exit; end; sftUnixTime: begin Add('"'); AddUnixTime(@V64); Add('"',','); exit; end; sftUnixMSTime: begin Add('"'); AddUnixMSTime(@V64); Add('"',','); exit; end; end; if woInt64AsHex in Options then begin Add('"'); if V64 <> 0 then AddBinToHexDisplay(@V64,SizeOf(V64)); Add('"'); end else if {$ifdef FPC}Kind=tkQWord{$else}P^.PropType^.IsQWord{$endif} then AddQ(V64) else Add(V64); end; end; {$ifdef FPC} tkBool, {$endif} tkEnumeration, tkInteger, tkSet: begin V := P^.GetOrdProp(Value); if not ((woDontStoreDefault in Options) and (V=P^.Default)) and not ((V=0) and (woDontStore0 in Options)) then begin HR(P); if {$ifdef FPC}(Kind=tkBool) or{$endif} ((Kind=tkEnumeration) and (P^.TypeInfo=TypeInfo(boolean))) then Add(boolean(V)) else if (woFullExpand in Options) or (woHumanReadable in Options) or (woEnumSetsAsText in Options) or (twoEnumSetsAsTextInRecord in CustomOptions) then case Kind of tkEnumeration: with P^.PropType^.EnumBaseType^ do begin Add('"'); PS := GetEnumNameOrd(V); if twoTrimLeftEnumSets in CustomOptions then AddTrimLeftLowerCase(PS) else AddShort(PS^); Add('"'); if woHumanReadableEnumSetAsComment in Options then GetEnumNameAll(CustomComment,'',true); end; tkSet: with P^.PropType^.SetEnumType^ do begin GetSetNameCSV(self,V,',',woHumanReadableFullSetsAsStar in Options); if woHumanReadableEnumSetAsComment in Options then GetEnumNameAll(CustomComment,'"*" or a set of ',true); end; else Add(V); end else Add(V); // typecast enums and sets as plain integer by default end; end; {$ifdef FPC}tkSString{$else}tkString{$endif}: begin P^.GetShortStrProp(Value,tmp); if (tmp<>'') or not (woDontStoreEmptyString in Options) then begin HR(P); Add('"'); AddJSONEscape(pointer(tmp)); Add('"'); end; end; // no shortstring deserialization by now {$ifdef FPC}tkLStringOld,{$endif} tkLString: if P^.TypeInfo=TypeInfo(RawJSON) then begin P^.GetLongStrProp(Value,tmp); // assume shortstring field is UTF-8 encoded if tmp<>'' then begin HR(P); AddString(tmp); end; end else begin codepage := P^.PropType^.AnsiStringCodePage; if (codepage=CP_SQLRAWBLOB) and not (woSQLRawBlobAsBase64 in Options) then begin if not (woDontStoreEmptyString in Options) then begin HR(P); AddShort('""'); end; end else begin P^.GetLongStrProp(Value,tmp); if (tmp<>'') or not (woDontStoreEmptyString in Options) then begin HR(P); Add('"'); if (IsObj=oSynPersistentWithPassword) and (codepage=CP_UTF8) and ((woHideSynPersistentPassword in Options) or (woFullExpand in Options)) and P^.GetterIsField and (P^.GetterAddr(Value)= TSynPersistentWithPassword(Value).GetPasswordFieldAddress) then begin if tmp<>'' then AddShort('***'); end else AddAnyAnsiString(tmp,twJSONEscape,codepage); Add('"'); end; end; end; tkFloat: begin if (P^.TypeInfo=TypeInfo(Currency)) and P^.GetterIsField then begin V64 := PInt64(P^.GetterAddr(Value))^; if not ((V64=0) and (woDontStore0 in Options)) then begin HR(P); AddCurr64(V64); end; end else if P^.TypeInfo=TypeInfo(TDateTime) then begin D64 := P^.GetDoubleProp(Value); if not ((D64=0) and (woDontStore0 in Options)) then begin HR(P); if woDateTimeWithMagic in Options then AddNoJSONEscape(@JSON_SQLDATE_MAGIC_QUOTE_VAR,4) else Add('"'); AddDateTime(D64); if woDateTimeWithZSuffix in Options then if frac(D64)=0 then // FireFox can't decode short form "2017-01-01Z" AddShort('T00:00Z') else Add('Z'); Add('"'); end; end else begin D64 := P^.GetFloatProp(Value); if not ((woDontStore0 in Options) and (D64=0)) then begin HR(P); AddDouble(D64); end; end; end; {$ifdef HASVARUSTRING} tkUString: begin // write converted to UTF-8 P^.GetUnicodeStrProp(Value,US); if (US<>'') or not (woDontStoreEmptyString in Options) then begin HR(P); Add('"'); AddJSONEscapeW(pointer(US)); Add('"'); end; end; {$endif} tkWString: begin // write converted to UTF-8 P^.GetWideStrProp(Value,WS); if (WS<>'') or not (woDontStoreEmptyString in Options) then begin HR(P); Add('"'); AddJSONEscapeW(pointer(WS)); Add('"'); end; end; tkDynArray: begin P^.GetDynArray(Value,dyn); if not ((woDontStore0 in Options) and (dyn.Count=0)) then begin HR(P); dynObjArray := P^.DynArrayIsObjArrayInstance; if (dynObjArray<>nil) and HasDefaultObjArrayWriter(dyn) then begin if dyn.Count=0 then begin if woHumanReadableEnumSetAsComment in Options then dynObjArray^.SetCustomComment(CustomComment); Add('[',']'); end else begin // do not use AddDynArrayJSON to support HR inc(fHumanReadableLevel); Add('['); po := dyn.Value^; for c := 1 to dyn.Count do begin WriteObject(po^,Options); Add(','); inc(po); end; CancelLastComma; dec(fHumanReadableLevel); HR; Add(']'); end; end else AddDynArrayJSON(dyn); // not an ObjArray: record-based serialization end; end; {$ifdef PUBLISHRECORD} tkRecord{$ifdef FPC},tkObject{$endif}: begin HR(P); AddRecordJSON(P^.GetFieldAddr(Value)^,P^.PropType^); end; {$endif} {$ifndef NOVARIANTS} tkVariant: begin P^.GetVariantProp(Value,VV); if not ((TVarData(VV).VType<=varNull) and (woDontStore0 in Options)) then begin HR(P); AddVariant(VV,twJSONEscape); // stored as JSON, e.g. '1.234' or '"text"' end; end; {$endif NOVARIANTS} tkClass: begin Obj := P^.GetObjProp(Value); if not(woDontStore0 in Options) or not IsObjectDefaultOrVoid(Obj) then if PropIsIDTypeCastedField(P,IsObj,Value) then begin HR(P); Add(PtrInt(Obj)); // not true instances, but ID end else if Obj<>nil then begin HR(P); // TPersistent or any class defined with $M+ WriteObject(Obj,Options); end; end; // tkString (shortstring) and tkInterface is not handled end; if Added then Add(','); end; procedure WritePropsFromRTTI(aClassType: TClass); var i: integer; P: PPropInfo; begin repeat for i := 1 to InternalClassPropInfo(aClassType,P) do begin WriteProp(P); P := P^.Next; end; if woDontStoreInherited in Options then break; aClassType := GetClassParent(aClassType); until aClassType=TObject; end; var i, c: integer; List: TList absolute Value; {$ifndef LVCL} Coll: TCollection absolute Value; {$endif} Str: TStrings absolute Value; Utf: TRawUTF8List absolute Value; Table: TSQLTable absolute Value; parser: PJSONCustomParser; aClassType: TClass; UtfP: PPUtf8CharArray; begin if not (woHumanReadable in Options) or (fHumanReadableLevel<0) then fHumanReadableLevel := 0; if (self=nil) or (Value=nil) then begin AddShort('null'); // return void object exit; end; CustomPropName := nil; aClassType := PClass(Value)^; IsObj := JSONObjectFromClass(aClassType,parser); if woFullExpand in Options then if IsObj=oSynMonitor then begin // nested values do not need extended info exclude(Options,woFullExpand); include(Options,woEnumSetsAsText); // only needed info is textual enums end else begin Add('{'); AddInstanceName(Value,':'); end; case IsObj of // handle custom class serialization oCustomReaderWriter: begin if Assigned(parser^.Writer) then parser^.Writer(self,Value,Options); exit; end; // handle JSON arrays oSQLTable: Table.GetJSONValues(Stream,true); oList, oObjectList, {$ifndef LVCL}oCollection,{$endif} oUtfs, oStrings: begin HR; Add('['); // write as JSON array of JSON objects inc(fHumanReadableLevel); case IsObj of oList: // TList for c := 0 to List.Count-1 do begin WriteObject(List.List[c],Options); Add(','); end; oObjectList: begin if not (woObjectListWontStoreClassName in Options) then // TObjectList will include "ClassName":"TMyObject" field Options := Options+[woStoreClassName]; for c := 0 to List.Count-1 do begin WriteObject(List.List[c],Options); Add(','); end; end; {$ifndef LVCL} oCollection: for c := 0 to Coll.Count-1 do begin WriteObject(Coll.Items[c],Options); Add(','); end; {$endif} oUtfs: begin UtfP := Utf.TextPtr; for c := 0 to Utf.Count-1 do begin HR; Add('"'); AddJSONEscape(UtfP^[c]); Add('"',','); end; end; oStrings: for c := 0 to Str.Count-1 do begin HR; Add('"'); AddJSONEscapeString(Str[c]); Add('"',','); end; end; CancelLastComma; dec(fHumanReadableLevel); HR; Add(']'); if woFullExpand in Options then Add('}'); exit; end; end; // handle JSON object Add('{'); inc(fHumanReadableLevel); if woStoreClassName in Options then begin // optional "ClassName":"TObjectClass" HR; AddPropName('ClassName'); Add('"'); AddShort(PShortString(PPointer(PPtrInt(Value)^+vmtClassName)^)^); Add('"',','); end; if IsObj in [oSQLRecord,oSQLMany] then begin // add TSQLRecord.ID property HR; AddPropName('ID'); if woHumanReadable in Options then Add(' '); Add(TSQLRecord(Value).fID); Add(','); if woIDAsIDstr in Options then begin AddPropName('ID_str'); Add('"'); Add(TSQLRecord(Value).fID); Add('"',','); end; end else begin if woStorePointer in Options then begin HR; AddPropName('Address'); // "Address":"0431298a" field Add('"'); if (IsObj=oSynException) and (ESynException(Value).RaisedAt<>nil) then TSynMapFile.Log(self,PtrUInt(ESynException(Value).RaisedAt),false) else AddPointer(PtrUInt(Value)); Add('"',','); end; case IsObj of oException: begin HR; AddPropName('Message'); // not published property -> manual serialization Add('"'); AddJSONEscapeString(Exception(Value).Message); Add('"',','); end; end; end; if IsObj=oCustomPropName then begin if parser^.Props=nil then // has been unregistered WritePropsFromRTTI(aClassType) else if woDontStoreInherited in Options then raise EParsingException.CreateUTF8('%.WriteObject woDontStoreInherited '+ 'after RegisterCustomSerializerFieldNames(%)', [self,aClassType]) else for i := 0 to length(parser^.Props)-1 do begin CustomPropName := @parser^.Fields[i]; WriteProp(parser^.Props[i]); end; end else WritePropsFromRTTI(aClassType); CancelLastComma; dec(fHumanReadableLevel); HR; Add('}'); if woFullExpand in Options then Add('}'); end; procedure TJSONSerializer.SetSQLRecordOptions(Value: TJSONSerializerSQLRecordOptions); begin fSQLRecordOptions := Value; if Value*[jwoAsJsonNotAsString,jwoID_str]<>[] then if (ColNames<>nil) and (ColNames[0]='"RowID":') then ColNames[0] := '"ID":'; // as expected by AJAX end; { TSQLVirtualTableModule } constructor TSQLVirtualTableModule.Create(aTableClass: TSQLVirtualTableClass; aServer: TSQLRestServer); begin fTableClass := aTableClass; fServer := aServer; fTableClass.GetTableModuleProperties(fFeatures); fModuleName := fTableClass.ModuleName; if fFeatures.FileExtension='' then // default extension is the module name fFeatures.FileExtension := UTF8ToString(LowerCase(fModuleName)); end; function TSQLVirtualTableModule.FileName(const aTableName: RawUTF8): TFileName; begin result := UTF8ToString(aTableName)+'.'+FileExtension;; if fFilePath='' then result := ExeVersion.ProgramFilePath+result else result := IncludeTrailingPathDelimiter(fFilePath)+result; end; { TSQLVirtualTable } constructor TSQLVirtualTable.Create(aModule: TSQLVirtualTableModule; const aTableName: RawUTF8; FieldCount: integer; Fields: PPUTF8CharArray); var aClass: TSQLRestStorageClass; aServer: TSQLRestServer; begin if (aModule=nil) or (aTableName='') then raise EModelException.CreateUTF8('Invalid %.Create(%,"%")',[self,aModule,aTableName]); fModule := aModule; fTableName := aTableName; if fModule.fFeatures.StaticClass<>nil then begin // create new fStatic instance e.g. for TSQLVirtualTableLog aServer := fModule.Server; if aServer=nil then raise EModelException.CreateUTF8('%.Server=nil for %.Create',[Module,self]) else fStaticTableIndex := aServer.Model.GetTableIndex(aTableName); if fStaticTableIndex>=0 then begin fStaticTable := aServer.Model.Tables[fStaticTableIndex]; aClass := fModule.fFeatures.StaticClass; if aClass.InheritsFrom(TSQLRestStorageInMemory) then fStatic := TSQLRestStorageInMemoryClass(aClass).Create(fStaticTable, fModule.Server,fModule.FileName(aTableName), self.InheritsFrom(TSQLVirtualTableBinary)) else fStatic := aClass.Create(fStaticTable,fModule.Server); if length(aServer.fStaticVirtualTable)<>length(aServer.Model.Tables) then SetLength(aServer.fStaticVirtualTable,length(aServer.Model.Tables)); aServer.fStaticVirtualTable[fStaticTableIndex] := fStatic; fStaticStorage := TSQLRestStorage(fStatic); fStaticStorage.fStorageVirtual := self; end; end; end; destructor TSQLVirtualTable.Destroy; var t,n: cardinal; begin if fStatic<>nil then begin if (Module<>nil) and (Module.Server<>nil) then with Module.Server do begin // temporary release (e.g. backup) t := Model.GetTableIndex(TableName); n := length(fStaticVirtualTable); if tnil; if result then if (vtWhereIDPrepared in fModule.Features) and Prepared.IsWhereIDEquals(true) then with Prepared.Where[0] do begin // check ID=? Value.VType := ftNull; // mark TSQLVirtualTableCursorJSON expects it OmitCheck := true; Prepared.EstimatedCost := costPrimaryIndex; Prepared.EstimatedRows := 1; end else begin Prepared.EstimatedCost := costFullScan; Prepared.EstimatedRows := 1000000; end; end; function TSQLVirtualTable.Drop: boolean; begin result := false; // no DROP TABLE to be implemented here end; function TSQLVirtualTable.Delete(aRowID: Int64): boolean; begin result := false; // no DELETE to be implemented here end; function TSQLVirtualTable.Insert(aRowID: Int64; var Values: TSQLVarDynArray; out insertedRowID: Int64): boolean; begin result := false; // no INSERT to be implemented here end; function TSQLVirtualTable.Update(oldRowID, newRowID: Int64; var Values: TSQLVarDynArray): boolean; begin result := false; // no UPDATE to be implemented here end; function TSQLVirtualTable.Transaction(aState: TSQLVirtualTableTransaction; aSavePoint: integer): boolean; begin result := (Module<>nil) and (vtWrite in Module.Features) and (aState in [vttBegin, vttSync, vttCommit, vttSavePoint, vttRelease]); end; function TSQLVirtualTable.Rename(const NewName: RawUTF8): boolean; begin result := false; end; class function TSQLVirtualTable.ModuleName: RawUTF8; const LEN: array[-1..2] of byte = (1,16,11,4); begin if self=nil then result := '' else begin ToText(self,result); system.delete(result,1,LEN[IdemPCharArray(pointer(result), ['TSQLVIRTUALTABLE','TSQLVIRTUAL','TSQL'])]); end; end; class function TSQLVirtualTable.StructureFromClass(aClass: TSQLRecordClass; const aTableName: RawUTF8): RawUTF8; begin FormatUTF8('CREATE TABLE % (%',[aTableName, GetVirtualTableSQLCreate(aClass.RecordProps)],result); end; function TSQLVirtualTable.Structure: RawUTF8; begin result := ''; if Self<>nil then if (Static<>nil) then // e.g. for TSQLVirtualTableJSON or TSQLVirtualTableExternal Result := StructureFromClass(StaticTable,TableName) else if (Module<>nil) and (Module.RecordClass<>nil) then // e.g. for TSQLVirtualTableLog Result := StructureFromClass(Module.RecordClass,TableName); end; { TSQLVirtualTableCursor } constructor TSQLVirtualTableCursor.Create(aTable: TSQLVirtualTable); begin fTable := aTable; end; procedure TSQLVirtualTableCursor.SetColumn(var aResult: TSQLVar; aValue: Int64); begin aResult.Options := []; aResult.VType := ftInt64; aResult.VInt64 := aValue; end; procedure TSQLVirtualTableCursor.SetColumn(var aResult: TSQLVar; const aValue: double); begin aResult.Options := []; aResult.VType := ftDouble; aResult.VDouble := aValue; end; procedure TSQLVirtualTableCursor.SetColumn(var aResult: TSQLVar; const aValue: RawUTF8); begin aResult.Options := []; aResult.VType := ftUTF8; fColumnTemp := aValue; // temporary copy available until next Column() call aResult.VText := pointer(fColumnTemp); end; procedure TSQLVirtualTableCursor.SetColumn(var aResult: TSQLVar; aValue: PUTF8Char; aValueLength: integer); begin aResult.Options := []; aResult.VType := ftUTF8; FastSetString(RawUTF8(fColumnTemp),aValue,aValueLength); // temporary copy aResult.VText := pointer(fColumnTemp); end; procedure TSQLVirtualTableCursor.SetColumnBlob(var aResult: TSQLVar; aValue: pointer; aValueLength: integer); begin aResult.Options := []; aResult.VType := ftBlob; SetString(fColumnTemp,PAnsiChar(aValue),aValueLength); // temporary copy aResult.VBlob := pointer(fColumnTemp); aResult.VBlobLen := aValueLength; end; procedure TSQLVirtualTableCursor.SetColumnDate(var aResult: TSQLVar; const aValue: TDateTime; aWithMS: boolean); begin if aWithMS then aResult.Options := [svoDateWithMS] else aResult.Options := []; aResult.VType := ftDate; aResult.VDateTime := aValue; end; procedure TSQLVirtualTableCursor.SetColumnCurr64(var aResult: TSQLVar; aValue64: PInt64); begin aResult.Options := []; aResult.VType := ftCurrency; PInt64(@aResult.VCurrency)^ := aValue64^; end; { TSQLVirtualTableCursorIndex } function TSQLVirtualTableCursorIndex.HasData: boolean; begin result := (self<>nil) and (fCurrent<=fMax); end; function TSQLVirtualTableCursorIndex.Next: boolean; begin if self=nil then result := false else begin if fCurrent<=fMax then inc(fCurrent); result := true; end; end; function TSQLVirtualTableCursorIndex.Search(const Prepared: TSQLVirtualTablePrepared): boolean; begin fCurrent := 0; // mark EOF by default fMax := -1; result := true; end; { TSQLVirtualTablePrepared } function TSQLVirtualTablePrepared.IsWhereIDEquals(CalledFromPrepare: Boolean): boolean; begin result := (WhereCount=1) and (Where[0].Column=VIRTUAL_TABLE_ROWID_COLUMN) and (CalledFromPrepare or (Where[0].Value.VType=ftInt64)) and (Where[0].Operation=soEqualTo); end; function TSQLVirtualTablePrepared.IsWhereOneFieldEquals: boolean; begin result := (WhereCount=1) and (Where[0].Column>=0) and (Where[0].Operation=soEqualTo); end; { TSQLVirtualTableJSON } constructor TSQLVirtualTableJSON.Create(aModule: TSQLVirtualTableModule; const aTableName: RawUTF8; FieldCount: integer; Fields: PPUTF8CharArray); begin inherited Create(aModule,aTableName,FieldCount,Fields); fStaticInMemory := fStatic as TSQLRestStorageInMemory; end; function TSQLVirtualTableJSON.Delete(aRowID: Int64): boolean; begin result := (Static<>nil) and Static.Delete(StaticTable,aRowID); if result and (StaticStorage<>nil) and (StaticStorage.Owner<>nil) then StaticStorage.Owner.fCache.NotifyDeletion(StaticTable,aRowID); end; function TSQLVirtualTableJSON.Drop: boolean; begin if (self<>nil) and (Static<>nil) then begin fStaticInMemory.RollBack(0); // close any pending transaction fStaticInMemory.DropValues({andupdatefile=}true); result := true; end else result := false; end; class procedure TSQLVirtualTableJSON.GetTableModuleProperties( var aProperties: TVirtualTableModuleProperties); begin aProperties.Features := [vtWrite,vtWhereIDPrepared]; aProperties.CursorClass := TSQLVirtualTableCursorJSON; aProperties.StaticClass := TSQLRestStorageInMemoryExternal; // will flush Cache if InheritsFrom(TSQLVirtualTableBinary) then aProperties.FileExtension := 'data'; // default will follow the class name, e.g. '.json' for TSQLVirtualTableJSON end; function TSQLVirtualTableJSON.Insert(aRowID: Int64; var Values: TSQLVarDynArray; out insertedRowID: Int64): boolean; var aRecord: TSQLRecord; begin result := false; if (self=nil) or (Static=nil) then exit; aRecord := StaticTable.Create; try if aRecord.SetFieldSQLVars(Values) then begin if aRowID>0 then aRecord.fID := aRowID; insertedRowID := fStaticInMemory.AddOne(aRecord,aRowID>0, aRecord.GetJSONValues(true,false,soInsert)); if insertedRowID>0 then begin if fStaticInMemory.Owner<>nil then fStaticInMemory.Owner.fCache.Notify(aRecord,soInsert); result := true; end; end; finally if not result then aRecord.Free; // on success, aRecord will stay in Values[] end; end; function TSQLVirtualTableJSON.Prepare(var Prepared: TSQLVirtualTablePrepared): boolean; begin result := inherited Prepare(Prepared); // optimize ID=? WHERE clause if result and (Static<>nil) then begin if Prepared.IsWhereOneFieldEquals then with Prepared.Where[0] do if (Column>=0) and (Column in fStaticInMemory.fIsUnique) then begin Value.VType := ftNull; // mark TSQLVirtualTableCursorJSON expects it OmitCheck := true; Prepared.EstimatedCost := costSecondaryIndex; Prepared.EstimatedRows := 10; end else if Prepared.EstimatedCost in [costFullScan,costScanWhere] then Prepared.EstimatedRows := fStaticInMemory.Count; end; end; function TSQLVirtualTableJSON.Update(oldRowID, newRowID: Int64; var Values: TSQLVarDynArray): boolean; var i: PtrInt; begin result := false; if (self=nil) or (Static=nil) or (oldRowID<>newRowID) or (newRowID<=0) then // don't allow ID change exit; if fStaticInMemory.UpdateOne(newRowID,Values) then begin if (fStaticInMemory.Owner<>nil) then begin i := fStaticInMemory.IDToIndex(newRowID); if i>=0 then fStaticInMemory.Owner.fCache.Notify(fStaticInMemory.fValue[i],soUpdate); end; result := true; end; end; { TSQLVirtualTableCursorJSON } function TSQLVirtualTableCursorJSON.Column(aColumn: integer; var aResult: TSQLVar): boolean; var store: TSQLRestStorageInMemory; begin if (self=nil) or (fCurrent>fMax) or (TSQLVirtualTableJSON(Table).Static=nil) then begin result := false; exit; end; store := TSQLVirtualTableJSON(Table).fStaticInMemory; if Cardinal(fCurrent)>=Cardinal(store.fCount) then result := false else begin if aColumn=VIRTUAL_TABLE_ROWID_COLUMN then begin aResult.VType := ftInt64; aResult.VInt64 := store.fValue[fCurrent].fID; end else with store.fStoredClassRecordProps.Fields do if cardinal(aColumn)>=cardinal(Count) then aResult.VType := ftNull else List[aColumn].GetFieldSQLVar(store.fValue[fCurrent],aResult,fColumnTemp); result := true; end; end; function TSQLVirtualTableCursorJSON.Search(const Prepared: TSQLVirtualTablePrepared): boolean; var store: TSQLRestStorageInMemory; begin result := false; inherited Search(Prepared); // mark EOF by default if not Table.InheritsFrom(TSQLVirtualTableJSON) then exit; store := TSQLVirtualTableJSON(Table).fStaticInMemory; if store=nil then exit; if store.fCount>0 then // if something to search in if Prepared.IsWhereIDEquals(false) then begin // ID=? fMax := store.IDToIndex(Prepared.Where[0].Value.VInt64); // binary search if fMax>=0 then fCurrent := fMax; // ID found end else begin fMax := store.fCount-1; // loop all records in ID order by default if Prepared.IsWhereOneFieldEquals then with Prepared.Where[0] do if Column in store.fIsUnique then begin store.fStoredClassRecordProps.Fields.List[Column]. SetFieldSQLVar(store.fSearchRec,Value); fMax := store.fUnique[Column].Find(store.fSearchRec); if fMax>=0 then fCurrent := fMax; // value found with O(1) search end; end; result := true; // no DB error end; { TSQLVirtualTableLog } type /// Record associated to Virtual Table implemented in Delphi, for Read/Only // access to a .log file, as created by TSynLog // - not used as real instances, but only used by the TSQLVirtualTableLog module // to provide the field layout needed to create the column layout for the // CREATE TABLE statement TSQLRecordLogFile = class(TSQLRecordVirtualTableAutoID) protected fContent: RawUTF8; fDateTime: TDateTime; fLevel: TSynLogInfo; published /// the log event time stamp property DateTime: TDateTime read fDateTime; /// the log event level property Level: TSynLogInfo read fLevel; /// the textual message associated to the log event property Content: RawUTF8 read fContent; end; constructor TSQLVirtualTableLog.Create(aModule: TSQLVirtualTableModule; const aTableName: RawUTF8; FieldCount: integer; Fields: PPUTF8CharArray); var aFileName: TFileName; begin inherited Create(aModule,aTableName,Fieldcount,Fields); if (FieldCount=1) then aFileName := UTF8ToString(Fields[0]) else aFileName := aModule.FileName(aTableName); fLogFile := TSynLogFile.Create(aFileName); end; destructor TSQLVirtualTableLog.Destroy; begin fLogFile.Free; inherited; end; class procedure TSQLVirtualTableLog.GetTableModuleProperties( var aProperties: TVirtualTableModuleProperties); begin aProperties.Features := [vtWhereIDPrepared]; aProperties.CursorClass := TSQLVirtualTableCursorLog; aProperties.RecordClass := TSQLRecordLogFile; end; { TSQLVirtualTableCursorLog } function TSQLVirtualTableCursorLog.Column(aColumn: integer; var aResult: TSQLVar): boolean; var LogFile: TSynLogFile; begin result := false; if (self=nil) or (fCurrent>fMax) then exit; LogFile := TSQLVirtualTableLog(Table).fLogFile; if LogFile=nil then exit; case aColumn of -1: SetColumn(aResult,fCurrent+1); // ID = row index + 1 0: SetColumnDate(aResult,LogFile.EventDateTime(fCurrent),true); 1: SetColumn(aResult,ord(LogFile.EventLevel[fCurrent])); 2: SetColumn(aResult,LogFile.LinePointers[fCurrent],LogFile.LineSize(fCurrent)); else exit; end; result := true; end; function TSQLVirtualTableCursorLog.Search( const Prepared: TSQLVirtualTablePrepared): boolean; begin result := inherited Search(Prepared); // mark EOF by default if result then begin fMax := TSQLVirtualTableLog(Table).fLogFile.Count-1; // search all range if Prepared.IsWhereIDEquals(false) then begin fCurrent := Prepared.Where[0].Value.VInt64-1; // ID=? -> index := ID-1 if cardinal(fCurrent)<=cardinal(fMax) then fMax := fCurrent else // found one fMax := fCurrent-1; // out of range ID end; end; end; { TAuthSession } procedure TAuthSession.ComputeProtectedValues; begin // here User.GroupRights and fPrivateKey should have been set fTimeOutShr10 := (QWord(User.GroupRights.SessionTimeout)*(1000*60))shr 10; fTimeOutTix := GetTickCount64 shr 10+fTimeOutShr10; fAccessRights := User.GroupRights.SQLAccessRights; fPrivateSalt := fID+'+'+fPrivateKey; fPrivateSaltHash := crc32(crc32(0,pointer(fPrivateSalt),length(fPrivateSalt)), pointer(User.PasswordHashHexa),length(User.PasswordHashHexa)); end; constructor TAuthSession.Create(aCtxt: TSQLRestServerURIContext; aUser: TSQLAuthUser); var GID: TSQLAuthGroup; rnd: THash256; begin fUser := aUser; if (aCtxt<>nil) and (User<>nil) and (User.fID<>0) then begin GID := User.GroupRights; // save pseudo TSQLAuthGroup = ID User.GroupRights := aCtxt.Server.fSQLAuthGroupClass.Create(aCtxt.Server,GID); if User.GroupRights.fID<>0 then begin // compute the next Session ID with aCtxt.Server do begin if fSessionCounter>=cardinal(maxInt) then fSessionCounter := 10 else if fSessionCounter=75 then // avoid IDCardinal=0 (77) or 1 (76) fSessionCounter := 78 else inc(fSessionCounter); fIDCardinal := fSessionCounter xor 77; UInt32ToUtf8(fIDCardinal,fID); end; // set session parameters TAESPRNG.Main.Fill(@rnd,SizeOf(rnd)); fPrivateKey := BinToHex(@rnd,SizeOf(rnd)); if not (rsoGetUserRetrieveNoBlobData in aCtxt.Server.Options) then aCtxt.Server.RetrieveBlob(aCtxt.Server.fSQLAuthUserClass,User.fID,'Data',User.fData); if (aCtxt.Call<>nil) and (aCtxt.Call.InHead<>'') then fSentHeaders := aCtxt.Call.InHead; ComputeProtectedValues; fRemoteIP := aCtxt.RemoteIP; {$ifdef WITHLOG} aCtxt.Log.Log(sllUserAuth, 'New [%] session %/% created at %/% running %', [User.GroupRights.Ident,User.LogonName,fIDCardinal,fRemoteIP, aCtxt.Call^.LowLevelConnectionID,aCtxt.GetUserAgent],self); {$endif} exit; // create successfull end; // on error: set GroupRights back to a pseudo TSQLAuthGroup = ID User.GroupRights.Free; User.GroupRights := GID; end; raise ESecurityException.CreateUTF8('Invalid %.Create(%,%)',[self,aCtxt,aUser]); end; destructor TAuthSession.Destroy; begin if User<>nil then begin User.GroupRights.Free; fUser.Free; end; ObjArrayClear(fMethods); ObjArrayClear(fInterfaces); inherited; end; function TAuthSession.GetUserName: RawUTF8; begin if User=nil then result := '' else result := User.LogonName; end; function TAuthSession.GetUserID: TID; begin if User=nil then result := 0 else result := User.fID; end; function TAuthSession.GetGroupID: TID; begin if User=nil then result := 0 else result := User.GroupRights.ID; end; const TAUTHSESSION_MAGIC = 1; procedure TAuthSession.SaveTo(W: TFileBufferWriter); begin W.Write1(TAUTHSESSION_MAGIC); W.WriteVarUInt32(IDCardinal); W.WriteVarUInt32(fUser.fID); fUser.GetBinaryValues(W); // User.fGroup is a pointer, but will be overriden W.WriteVarUInt32(fUser.GroupRights.fID); fUser.GroupRights.GetBinaryValues(W); W.Write(fPrivateKey); W.Write(fSentHeaders); end; // TODO: persist ORM/SOA stats? -> rather integrate them before saving constructor TAuthSession.CreateFrom(var P: PAnsiChar; PEnd: PAnsiChar; Server: TSQLRestServer); procedure RaiseError; begin raise ESynException.CreateUTF8('%.CreateFrom() with invalid format',[self]); end; var PB: PByte absolute P; i32: cardinal; begin if PB^=TAUTHSESSION_MAGIC then inc(PB) else RaiseError; PB := FromVarUInt32Safe(PB,pointer(PEnd),fIDCardinal); if PB=nil then RaiseError; UInt32ToUtf8(fIDCardinal,fID); fUser := Server.SQLAuthUserClass.Create; PB := FromVarUInt32Safe(PB,pointer(PEnd),i32); if PB=nil then RaiseError; fUser.fID := i32; fUser.SetBinaryValues(P,PEnd); // fUser.fGroup will be overriden by true instance fUser.fGroup := Server.SQLAuthGroupClass.Create; PB := FromVarUInt32Safe(PB,pointer(PEnd),i32); if PB=nil then RaiseError; fUser.fGroup.fID := i32; fUser.fGroup.SetBinaryValues(P,PEnd); fPrivateKey := FromVarString(PB,pointer(PEnd)); fSentHeaders := FromVarString(PB,pointer(PEnd)); if PB=nil then RaiseError; ComputeProtectedValues; FindNameValue(fSentHeaders,HEADER_REMOTEIP_UPPER,fRemoteIP); end; { TSQLAccessRights } procedure TSQLAccessRights.Edit(aTableIndex: integer; C, R, U, D: Boolean); begin if C then Include(POST,aTableIndex) else Exclude(POST,aTableindex); if R then Include(GET,aTableIndex) else Exclude(GET,aTableindex); if U then Include(PUT,aTableIndex) else Exclude(PUT,aTableindex); if D then Include(DELETE,aTableIndex) else Exclude(DELETE,aTableindex); end; procedure TSQLAccessRights.Edit(aTableIndex: integer; aRights: TSQLOccasions); begin if soInsert in aRights then Include(POST,aTableIndex) else Exclude(POST,aTableindex); if soSelect in aRights then Include(GET,aTableIndex) else Exclude(GET,aTableindex); if soUpdate in aRights then Include(PUT,aTableIndex) else Exclude(PUT,aTableindex); if soDelete in aRights then Include(DELETE,aTableIndex) else Exclude(DELETE,aTableindex); end; procedure TSQLAccessRights.Edit(aModel: TSQLModel; aTable: TSQLRecordClass; C, R, U, D: Boolean); begin Edit(aModel.GetTableIndexExisting(aTable),C,R,U,D); end; procedure TSQLAccessRights.Edit(aModel: TSQLModel; aTable: TSQLRecordClass; aRights: TSQLOccasions); begin Edit(aModel.GetTableIndexExisting(aTable),aRights); end; procedure TSQLAccessRights.FromString(P: PUTF8Char); begin FillCharFast(self,SizeOf(self),0); if P=nil then exit; AllowRemoteExecute := TSQLAllowRemoteExecute(byte(GetNextItemCardinal(P))); SetBitCSV(GET,MAX_SQLTABLES,P); SetBitCSV(POST,MAX_SQLTABLES,P); SetBitCSV(PUT,MAX_SQLTABLES,P); SetBitCSV(DELETE,MAX_SQLTABLES,P); end; function TSQLAccessRights.ToString: RawUTF8; begin FormatUTF8('%,%,%,%,%', [Byte(AllowRemoteExecute), GetBitCSV(GET,MAX_SQLTABLES), GetBitCSV(POST,MAX_SQLTABLES), GetBitCSV(PUT,MAX_SQLTABLES), GetBitCSV(DELETE,MAX_SQLTABLES)],result); end; function TSQLAccessRights.CanExecuteORMWrite(Method: TSQLURIMethod; Table: TSQLRecordClass; TableIndex: integer; const TableID: TID; Context: TSQLRestServerURIContext): boolean; begin result := true; case Method of mPOST: // POST=ADD=INSERT if Table<>nil then // ExecuteORMWrite will check reSQL access right result := (TableIndex in POST); mPUT: // PUT=UPDATE result := (Table<>nil) and ((TableIndex in PUT) or ((TableID>0) and (Context.Session>CONST_AUTHENTICATION_NOT_USED) and (Table=Context.Server.fSQLAuthUserClass) and (TableID=Context.SessionUser) and (reUserCanChangeOwnPassword in AllowRemoteExecute))); mDelete: result := (Table<>nil) and (TableIndex in DELETE) and ((TableID>0) or (reUrlEncodedDelete in AllowRemoteExecute)); end; end; { TSQLAuthGroup } function TSQLAuthGroup.GetSQLAccessRights: TSQLAccessRights; begin if self=nil then FillCharFast(result,SizeOf(result),0) else result.FromString(pointer(AccessRights)); end; class procedure TSQLAuthGroup.InitializeTable(Server: TSQLRestServer; const FieldName: RawUTF8; Options: TSQLInitializeTableOptions); var G: TSQLAuthGroup; A: TSQLAccessRights; U: TSQLAuthUser; AuthUserIndex, AuthGroupIndex: integer; AdminID, SupervisorID, UserID: PtrInt; begin inherited; // will create any needed index if (Server<>nil) and (FieldName='') then if Server.HandleAuthentication then begin // create default Groups and Users (we are already in a Transaction) AuthGroupIndex := Server.Model.GetTableIndexExisting(Server.fSQLAuthUserClass); AuthUserIndex := Server.Model.GetTableIndexExisting(Server.fSQLAuthGroupClass); if not (itoNoAutoCreateGroups in Options) then begin G := Server.fSQLAuthGroupClass.Create; try // POSTSQL SELECTSQL Service AuthR AuthW TablesR TablesW // Admin Yes Yes Yes Yes Yes Yes Yes // Supervisor No Yes Yes Yes No Yes Yes // User No No Yes No No Yes Yes // Guest No No No No No Yes No A := FULL_ACCESS_RIGHTS; G.Ident := 'Admin'; G.SQLAccessRights := A; G.SessionTimeout := AuthAdminGroupDefaultTimeout; AdminID := Server.Add(G,true); G.Ident := 'Supervisor'; A.AllowRemoteExecute := SUPERVISOR_ACCESS_RIGHTS.AllowRemoteExecute; A.Edit(AuthUserIndex,[soSelect]); // AuthUser R/O A.Edit(AuthGroupIndex,[soSelect]); // AuthGroup R/O G.SQLAccessRights := A; G.SessionTimeout := AuthSupervisorGroupDefaultTimeout; SupervisorID := Server.Add(G,true); G.Ident := 'User'; Exclude(A.AllowRemoteExecute,reSQLSelectWithoutTable); Exclude(A.GET,AuthUserIndex); // no Auth R Exclude(A.GET,AuthGroupIndex); G.SQLAccessRights := A; G.SessionTimeout := AuthUserGroupDefaultTimeout; UserID := Server.Add(G,true); G.Ident := 'Guest'; A.AllowRemoteExecute := []; FillcharFast(A.POST,SizeOf(TSQLFieldTables),0); // R/O access FillcharFast(A.PUT,SizeOf(TSQLFieldTables),0); FillcharFast(A.DELETE,SizeOf(TSQLFieldTables),0); G.SQLAccessRights := A; G.SessionTimeout := AuthGuestGroupDefaultTimeout; Server.Add(G,true); finally G.Free; end; if not (itoNoAutoCreateUsers in Options) and (Server.TableRowCount(Server.fSQLAuthUserClass)=0) then begin U := Server.fSQLAuthUserClass.Create; try U.LogonName := 'Admin'; U.PasswordHashHexa := AuthAdminDefaultPassword; U.DisplayName := U.LogonName; U.GroupRights := TSQLAuthGroup(AdminID); Server.Add(U,true); U.LogonName := 'Supervisor'; U.PasswordHashHexa := AuthSupervisorDefaultPassword; U.DisplayName := U.LogonName; U.GroupRights := TSQLAuthGroup(SupervisorID); Server.Add(U,true); U.LogonName := 'User'; U.PasswordHashHexa := AuthUserDefaultPassword; U.DisplayName := U.LogonName; U.GroupRights := TSQLAuthGroup(UserID); Server.Add(U,true); finally U.Free; end; end; end; end; end; procedure TSQLAuthGroup.SetSQLAccessRights(const Value: TSQLAccessRights); begin if self<>nil then AccessRights := Value.ToString; end; { TSQLAuthUser } class function TSQLAuthUser.ComputeHashedPassword( const aPasswordPlain, aHashSalt: RawUTF8; aHashRound: integer): RawUTF8; const TSQLAUTHUSER_SALT = 'salt'; var dig: TSHA256Digest; begin if aHashSalt='' then result := SHA256(TSQLAUTHUSER_SALT+aPasswordPlain) else begin PBKDF2_HMAC_SHA256(aPasswordPlain,aHashSalt,aHashRound,dig); result := SHA256DigestToString(dig); FillCharFast(dig,SizeOf(dig),0); end; end; procedure TSQLAuthUser.SetPasswordPlain(const Value: RawUTF8); begin if self<>nil then PasswordHashHexa := ComputeHashedPassword(Value); end; procedure TSQLAuthUser.SetPassword(const aPasswordPlain, aHashSalt: RawUTF8; aHashRound: integer); begin if self<>nil then PasswordHashHexa := ComputeHashedPassword(aPasswordPlain,aHashSalt,aHashRound); end; function TSQLAuthUser.CanUserLog(Ctxt: TSQLRestServerURIContext): boolean; begin result := true; // any existing TSQLAuthUser is allowed by default end; { TSQLRestServerAuthentication } constructor TSQLRestServerAuthentication.Create(aServer: TSQLRestServer); begin fServer := aServer; fOptions := [saoUserByLogonOrID]; end; function TSQLRestServerAuthentication.AuthSessionRelease( Ctxt: TSQLRestServerURIContext): boolean; var aUserName: RawUTF8; aSessionID: cardinal; i: integer; begin result := false; if fServer.fSessions=nil then exit; aUserName := Ctxt.InputUTF8OrVoid['UserName']; if aUserName='' then exit; aSessionID := Ctxt.InputIntOrVoid['Session']; if aSessionID=0 then aSessionID := Ctxt.InputHexaOrVoid['SessionHex']; if aSessionID=0 then exit; result := true; // recognized GET ModelRoot/auth?UserName=...&Session=... // allow only to delete its own session - ticket [7723fa7ebd] if aSessionID=Ctxt.Session then for i := 0 to fServer.fSessions.Count-1 do with TAuthSession(fServer.fSessions.List[i]) do if (fIDCardinal=aSessionID) and (fUser.LogonName=aUserName) then begin Ctxt.fSession := nil; // avoid GPF fServer.SessionDelete(i,Ctxt); Ctxt.Success; break; end; end; function TSQLRestServerAuthentication.GetUser(Ctxt: TSQLRestServerURIContext; const aUserName: RawUTF8): TSQLAuthUser; var UserID: TID; err: integer; begin UserID := GetInt64(pointer(aUserName),err); if (err<>0) or (UserID<=0) or not (saoUserByLogonOrID in fOptions) then UserID := 0; if Assigned(fServer.OnAuthenticationUserRetrieve) then result := fServer.OnAuthenticationUserRetrieve(self,Ctxt,UserID,aUserName) else begin if UserID<>0 then begin // try if TSQLAuthUser.ID was transmitted result := fServer.fSQLAuthUserClass.Create(fServer,UserID); // may use ORM cache :) if result.fID=0 then FreeAndNil(result); end else result := nil; if result=nil then result := fServer.fSQLAuthUserClass.Create(fServer,'LogonName=?',[aUserName]); if (result.fID=0) and (saoHandleUnknownLogonAsStar in fOptions) then if fServer.Retrieve('LogonName=?',[],['*'],result) then begin result.LogonName := aUserName; result.DisplayName := aUserName; end; end; if (result=nil) or (result.fID=0) then begin fServer.InternalLog('%.LogonName=% not found',[fServer.fSQLAuthUserClass,aUserName],sllUserAuth); FreeAndNil(result); end else if not result.CanUserLog(Ctxt) then begin fServer.InternalLog('%.CanUserLog(%) returned FALSE -> rejected',[result,aUserName],sllUserAuth); FreeAndNil(result); end; end; procedure TSQLRestServerAuthentication.SessionCreate(Ctxt: TSQLRestServerURIContext; var User: TSQLAuthUser); var Session: TAuthSession; begin if User<>nil then try // now client is authenticated -> create a session fServer.SessionCreate(User,Ctxt,Session); // call Ctxt.AuthenticationFailed on error if Session<>nil then SessionCreateReturns(Ctxt,Session,Session.fPrivateSalt,'',''); finally User.Free; end; end; procedure TSQLRestServerAuthentication.SessionCreateReturns( Ctxt: TSQLRestServerURIContext; Session: TAuthSession; const result, data, header: RawUTF8); var body: TDocVariantData; begin body.InitFast(10,dvObject); if result='' then body.AddValue('result',Session.IDCardinal) else body.AddValue('result',RawUTF8ToVariant(result)); if data<>'' then body.AddValue('data',RawUTF8ToVariant(data)); if fAlgoName<>'' then // match e.g. TSQLRestServerAuthenticationSignedURIAlgo body.AddValue('algo',RawUTF8ToVariant(fAlgoName)); with Session.User do body.AddNameValuesToObject(['logonid',IDValue,'logonname',LogonName, 'logondisplay',DisplayName,'logongroup',GroupRights.IDValue, 'timeout',GroupRights.SessionTimeout, 'server',ExeVersion.ProgramName,'version',ExeVersion.Version.DetailedOrVoid]); Ctxt.ReturnsJson(variant(body),HTTP_SUCCESS,false,twJSONEscape,false,header); end; class function TSQLRestServerAuthentication.ClientGetSessionKey( Sender: TSQLRestClientURI; User: TSQLAuthUser; const aNameValueParameters: array of const): RawUTF8; var resp: RawUTF8; values: array[0..9] of TValuePUTF8Char; a: integer; algo: TSQLRestServerAuthenticationSignedURIAlgo absolute a; begin if (Sender.CallBackGet('Auth',aNameValueParameters,resp)<>HTTP_SUCCESS) or (JSONDecode(pointer(resp),['result','data','server','version','logonid', 'logonname','logondisplay','logongroup','timeout','algo'],@values)=nil) then begin Sender.fSessionData := ''; // reset temporary 'data' field result := ''; end else begin values[0].ToUTF8(result); Base64ToBin(PAnsiChar(values[1].Value),values[1].ValueLen,Sender.fSessionData); values[2].ToUTF8(Sender.fSessionServer); values[3].ToUTF8(Sender.fSessionVersion); SetID(values[4].Value,User.fID); values[5].ToUTF8(User.fLogonName); // set/fix using values from server values[6].ToUTF8(User.fDisplayName); User.GroupRights := pointer(values[7].ToInteger); Sender.fSessionServerTimeout := values[8].ToInteger; if Sender.fSessionServerTimeout<=0 then Sender.fSessionServerTimeout := 60; // default 1 hour if not suppplied a := GetEnumNameValueTrimmed(TypeInfo(TSQLRestServerAuthenticationSignedURIAlgo), values[9].Value,values[9].ValueLen); if a>=0 then Sender.fComputeSignature := TSQLRestServerAuthenticationSignedURI.GetComputeSignature(algo); end; end; class function TSQLRestServerAuthentication.ClientSetUser(Sender: TSQLRestClientURI; const aUserName, aPassword: RawUTF8; aPasswordKind: TSQLRestServerAuthenticationClientSetUserPassword; const aHashSalt: RawUTF8; aHashRound: integer): boolean; var U: TSQLAuthUser; key: RawUTF8; begin result := false; if Sender=nil then exit; try Sender.SessionClose; // ensure Sender.SessionUser=nil U := TSQLAuthUser(Sender.Model.GetTableInherited(TSQLAuthUser).Create); try U.LogonName := trim(aUserName); U.DisplayName := U.LogonName; if aPasswordKind<>passClear then U.PasswordHashHexa := aPassword else if aHashSalt='' then U.PasswordPlain := aPassword else // compute SHA256('salt'+aPassword) U.SetPassword(aPassword,aHashSalt,aHashRound); key := ClientComputeSessionKey(Sender,U); result := Sender.SessionCreate(self,U,key); finally U.Free; end; finally if Assigned(Sender.OnSetUser) then Sender.OnSetUser(Sender); // always notify of user change, even if failed end; end; { TSQLRestServerAuthenticationURI } function TSQLRestServerAuthenticationURI.RetrieveSession( Ctxt: TSQLRestServerURIContext): TAuthSession; begin result := nil; if (Ctxt=nil) or (Ctxt.URISessionSignaturePos=0) then exit; // expected format is 'session_signature='Hexa8(SessionID)'... if (Ctxt.URISessionSignaturePos>0) and (Ctxt.URISessionSignaturePos+(18+8)<=length(Ctxt.Call^.Url)) and HexDisplayToCardinal(PAnsiChar(pointer(Ctxt.Call^.url))+Ctxt.URISessionSignaturePos+18,Ctxt.Session) then result := fServer.SessionAccess(Ctxt); end; class procedure TSQLRestServerAuthenticationURI.ClientSessionSign( Sender: TSQLRestClientURI; var Call: TSQLRestURIParams); begin if (Sender<>nil) and (Sender.fSessionID<>0) and (Sender.fSessionUser<>nil) then if PosExChar('?',Call.url)=0 then Call.url := Call.url+'?session_signature='+Sender.fSessionIDHexa8 else Call.url := Call.url+'&session_signature='+Sender.fSessionIDHexa8; end; { TSQLRestServerAuthenticationSignedURI } // expected format is session_signature= // Hexa8(SessionID)+ // Hexa8(Timestamp)+ // Hexa8(crc32('SessionID+HexaSessionPrivateKey'+Sha256('salt'+PassWord)+ // Hexa8(Timestamp)+url)) constructor TSQLRestServerAuthenticationSignedURI.Create(aServer: TSQLRestServer); begin inherited Create(aServer); fComputeSignature := TSQLRestServerAuthenticationSignedURI.ComputeSignatureCrc32; TimestampCoherencySeconds := 5; end; procedure TSQLRestServerAuthenticationSignedURI.SetNoTimestampCoherencyCheck( value: boolean); begin if self<>nil then fNoTimestampCoherencyCheck := value; end; procedure TSQLRestServerAuthenticationSignedURI.SetTimestampCoherencySeconds( value: cardinal); begin if self=nil then exit; fTimestampCoherencySeconds := value; fTimestampCoherencyTicks := round(value*(1000/256)); // 256 ms resolution end; procedure TSQLRestServerAuthenticationSignedURI.SetAlgorithm( value: TSQLRestServerAuthenticationSignedURIAlgo); begin fComputeSignature := GetComputeSignature(value); if value=suaCRC32 then fAlgoName := '' else fAlgoName := SynCommons.LowerCase(TrimLeftLowerCaseShort(ToText(value))); end; class function TSQLRestServerAuthenticationSignedURI.GetComputeSignature( algo: TSQLRestServerAuthenticationSignedURIAlgo): TSQLRestServerAuthenticationSignedURIComputeSignature; begin // FPC doesn't allow to use constants for procedure of object case algo of suaCRC32C: result := ComputeSignatureCrc32c; suaXXHASH: result := ComputeSignaturexxHash; suaMD5: result := ComputeSignatureMD5; suaSHA1: result := ComputeSignatureSHA1; suaSHA256: result := ComputeSignatureSHA256; suaSHA512: result := ComputeSignatureSHA512; else result := ComputeSignatureCrc32; // default/fallback end; end; class function TSQLRestServerAuthenticationSignedURI.ComputeSignatureCrc32( privatesalt: cardinal; timestamp, url: PAnsiChar; urllen: integer): cardinal; begin // historical algorithm result := crc32(crc32(privatesalt,timestamp,8),url,urllen); end; class function TSQLRestServerAuthenticationSignedURI.ComputeSignatureCrc32c( privatesalt: cardinal; timestamp, url: PAnsiChar; urllen: integer): cardinal; begin // faster on SSE4.2 CPU, and slightly more secure if not cascaded result := crc32c(privatesalt,timestamp,8) xor crc32c(privatesalt,url,urllen); end; class function TSQLRestServerAuthenticationSignedURI.ComputeSignaturexxHash( privatesalt: cardinal; timestamp, url: PAnsiChar; urllen: integer): cardinal; begin // xxHash32 has no immediate reverse function result := xxHash32(privatesalt,timestamp,8) xor xxHash32(privatesalt,url,urllen); end; class function TSQLRestServerAuthenticationSignedURI.ComputeSignatureMD5( privatesalt: cardinal; timestamp, url: PAnsiChar; urllen: integer): cardinal; var digest: THash128Rec; MD5: TMD5; i: integer; begin MD5.Init; MD5.Update(privatesalt,4); MD5.Update(timestamp^,8); MD5.Update(url^,urllen); MD5.Final(digest.b); result := digest.c[0]; for i := 1 to high(digest.c) do result := result xor digest.c[i]; end; class function TSQLRestServerAuthenticationSignedURI.ComputeSignatureSHA1( privatesalt: cardinal; timestamp, url: PAnsiChar; urllen: integer): cardinal; var digest: array[0..(SizeOf(TSHA1Digest)div 4)-1] of cardinal; SHA1: TSHA1; i: integer; begin SHA1.Init; SHA1.Update(@privatesalt,4); SHA1.Update(timestamp,8); SHA1.Update(url,urllen); SHA1.Final(TSHA1Digest(digest)); result := digest[0]; for i := 1 to high(digest) do result := result xor digest[i]; end; class function TSQLRestServerAuthenticationSignedURI.ComputeSignatureSHA256( privatesalt: cardinal; timestamp, url: PAnsiChar; urllen: integer): cardinal; var digest: THash256Rec; SHA256: TSHA256; i: integer; begin SHA256.Init; SHA256.Update(@privatesalt,4); SHA256.Update(timestamp,8); SHA256.Update(url,urllen); SHA256.Final(digest.b); result := digest.c[0]; for i := 1 to high(digest.c) do result := result xor digest.c[i]; end; class function TSQLRestServerAuthenticationSignedURI.ComputeSignatureSHA512( privatesalt: cardinal; timestamp, url: PAnsiChar; urllen: integer): cardinal; var digest: THash512Rec; SHA512: TSHA512; i: integer; begin SHA512.Init; SHA512.Update(@privatesalt,4); SHA512.Update(timestamp,8); SHA512.Update(url,urllen); SHA512.Final(digest.b); result := digest.c[0]; for i := 1 to high(digest.c) do result := result xor digest.c[i]; end; function TSQLRestServerAuthenticationSignedURI.RetrieveSession( Ctxt: TSQLRestServerURIContext): TAuthSession; var aTimestamp, aSignature, aMinimalTimestamp, aExpectedSignature: cardinal; PTimestamp: PAnsiChar; aURLlength: Integer; begin result := inherited RetrieveSession(Ctxt); if result=nil then exit; // no valid session ID in session_signature if Ctxt.URISessionSignaturePos+(18+8+8+8)>length(Ctxt.Call^.url) then begin result := nil; exit; end; aURLlength := Ctxt.URISessionSignaturePos-1; PTimestamp := @Ctxt.Call^.url[aURLLength+(20+8)]; // points to Hexa8(Timestamp) aMinimalTimestamp := result.fLastTimestamp-fTimestampCoherencyTicks; if HexDisplayToCardinal(PTimestamp,aTimestamp) and (fNoTimestampCoherencyCheck or (integer(aMinimalTimestamp)<0) or // <0 just after login (aTimestamp>=aMinimalTimestamp)) then begin aExpectedSignature := fComputeSignature(result.fPrivateSaltHash,PTimestamp, pointer(Ctxt.Call^.url),aURLlength); if HexDisplayToCardinal(PTimestamp+8,aSignature) and (aSignature=aExpectedSignature) then begin if aTimestamp>result.fLastTimestamp then result.fLastTimestamp := aTimestamp; exit; end else begin {$ifdef WITHLOG} Ctxt.Log.Log(sllUserAuth,'Invalid Signature: expected %, got %', [Int64(aExpectedSignature),Int64(aSignature)],self); {$endif} end; end else begin {$ifdef WITHLOG} Ctxt.Log.Log(sllUserAuth,'Invalid Timestamp: expected >=%, got %', [aMinimalTimestamp,Int64(aTimestamp)],self); {$endif} end; result := nil; // indicates invalid signature end; class procedure TSQLRestServerAuthenticationSignedURI.ClientSessionSign( Sender: TSQLRestClientURI; var Call: TSQLRestURIParams); var nonce, blankURI: RawUTF8; begin if (Sender=nil) or (Sender.fSessionID=0) or (Sender.fSessionUser=nil) then exit; blankURI := Call.Url; if PosExChar('?',Call.url)=0 then Call.url := Call.Url+'?session_signature=' else Call.url := Call.Url+'&session_signature='; with Sender do begin fSessionLastTick64 := GetTickCount64; nonce := CardinalToHexLower(fSessionLastTick64 shr 8); // 256 ms resolution Call.url := Call.url+fSessionIDHexa8+nonce+CardinalToHexLower( Sender.fComputeSignature(fSessionPrivateKey,Pointer(nonce), Pointer(blankURI),length(blankURI))); end; end; { TSQLRestServerAuthenticationDefault } function TSQLRestServerAuthenticationDefault.Auth(Ctxt: TSQLRestServerURIContext): boolean; var aUserName, aPassWord, aClientNonce: RawUTF8; User: TSQLAuthUser; begin result := true; if AuthSessionRelease(Ctxt) then exit; aUserName := Ctxt.InputUTF8OrVoid['UserName']; aPassWord := Ctxt.InputUTF8OrVoid['Password']; aClientNonce := Ctxt.InputUTF8OrVoid['ClientNonce']; if (aUserName<>'') and (length(aClientNonce)>32) then begin // GET ModelRoot/auth?UserName=...&PassWord=...&ClientNonce=... -> handshaking User := GetUser(Ctxt,aUserName); if User<>nil then try // check if match TSQLRestClientURI.SetUser() algorithm if CheckPassword(Ctxt,User,aClientNonce,aPassWord) then SessionCreate(Ctxt,User) else // will call Ctxt.AuthenticationFailed on error Ctxt.AuthenticationFailed(afInvalidPassword); finally User.Free; end else Ctxt.AuthenticationFailed(afUnknownUser); end else if aUserName<>'' then // only UserName=... -> return hexadecimal nonce content valid for 5 minutes Ctxt.Results([CurrentServerNonce]) else // parameters does not match any expected layout -> try next authentication result := false; end; function TSQLRestServerAuthenticationDefault.CheckPassword(Ctxt: TSQLRestServerURIContext; User: TSQLAuthUser; const aClientNonce, aPassWord: RawUTF8): boolean; var aSalt: RawUTF8; begin aSalt := aClientNonce+User.LogonName+User.PasswordHashHexa; result := IsHex(aPassword,SizeOf(THash256)) and (IdemPropNameU(aPassWord,SHA256(fServer.Model.Root+CurrentServerNonce(false)+aSalt)) or // if current nonce failed, tries with previous 5 minutes' nonce IdemPropNameU(aPassWord,SHA256(fServer.Model.Root+CurrentServerNonce(true)+aSalt))); end; class function TSQLRestServerAuthenticationDefault.ClientComputeSessionKey( Sender: TSQLRestClientURI; User: TSQLAuthUser): RawUTF8; var aServerNonce, aClientNonce: RawUTF8; rnd: THash256; begin result := ''; if User.LogonName='' then exit; aServerNonce := Sender.CallBackGetResult('Auth',['UserName',User.LogonName]); if aServerNonce='' then exit; TAESPRNG.Main.FillRandom(@rnd,SizeOf(rnd)); aClientNonce := BinToHexLower(@rnd,SizeOf(rnd)); result := ClientGetSessionKey(Sender,User,['UserName',User.LogonName,'Password', Sha256(Sender.Model.Root+aServerNonce+aClientNonce+User.LogonName+User.PasswordHashHexa), 'ClientNonce',aClientNonce]); end; { TSQLRestServerAuthenticationNone } function TSQLRestServerAuthenticationNone.Auth(Ctxt: TSQLRestServerURIContext): boolean; var aUserName: RawUTF8; U: TSQLAuthUser; begin aUserName := Ctxt.InputUTF8OrVoid['UserName']; if aUserName='' then begin result := false; // let's try another TSQLRestServerAuthentication class exit; end; result := true; // this kind of weak authentication avoid stronger ones if AuthSessionRelease(Ctxt) then exit; U := GetUser(Ctxt,aUserName); if U=nil then Ctxt.AuthenticationFailed(afUnknownUser) else SessionCreate(Ctxt,U); // call Ctxt.AuthenticationFailed on error end; class function TSQLRestServerAuthenticationNone.ClientComputeSessionKey( Sender: TSQLRestClientURI; User: TSQLAuthUser): RawUTF8; begin result := ClientGetSessionKey(Sender,User,['UserName',User.LogonName]); end; { TSQLRestServerAuthenticationHttpAbstract } const COOKIE_SESSION = 'mORMot_session_signature'; class procedure TSQLRestServerAuthenticationHttpAbstract.ClientSessionSign( Sender: TSQLRestClientURI; var Call: TSQLRestURIParams); begin if (Sender<>nil) and (Sender.fSessionID<>0) and (Sender.fSessionUser<>nil) then Call.InHead := Trim(Call.InHead+ // session ID transmitted as HTTP cookie (#13#10'Cookie: '+COOKIE_SESSION+'=')+Sender.fSessionIDHexa8); end; class function TSQLRestServerAuthenticationHttpAbstract.ClientSetUser( Sender: TSQLRestClientURI; const aUserName, aPassword: RawUTF8; aPasswordKind: TSQLRestServerAuthenticationClientSetUserPassword; const aHashSalt: RawUTF8; aHashRound: integer): boolean; var res: RawUTF8; U: TSQLAuthUser; begin result := false; if (aUserName='') or (Sender=nil) then exit; if aPasswordKind<>passClear then raise ESecurityException.CreateUTF8('%.ClientSetUser(%) expects passClear', [self,Sender]); Sender.SessionClose; // ensure Sender.SessionUser=nil try // inherited ClientSetUser() won't fit with Auth() method below ClientSetUserHttpOnly(Sender,aUserName,aPassword); Sender.fSessionAuthentication := self; // to enable ClientSessionSign() U := TSQLAuthUser(Sender.Model.GetTableInherited(TSQLAuthUser).Create); try U.LogonName := trim(aUserName); res := ClientGetSessionKey(Sender,U,[]); if res<>'' then result := Sender.SessionCreate(self,U,res); finally U.Free; end; finally if not result then begin // on error, reverse all values Sender.fSessionAuthentication := nil; Sender.fSessionHttpHeader := ''; end; if Assigned(Sender.OnSetUser) then Sender.OnSetUser(Sender); // always notify of user change, even if failed end; end; function TSQLRestServerAuthenticationHttpAbstract.RetrieveSession( Ctxt: TSQLRestServerURIContext): TAuthSession; var cookie: RawUTF8; begin cookie := Ctxt.InCookie[COOKIE_SESSION]; if (length(cookie)=8) and HexDisplayToCardinal(pointer(cookie),Ctxt.Session) then result := fServer.SessionAccess(Ctxt) else result := nil; end; class procedure TSQLRestServerAuthenticationHttpAbstract.ClientSetUserHttpOnly( Sender: TSQLRestClientURI; const aUserName, aPasswordClear: RawUTF8); begin Sender.fSessionHttpHeader := ComputeAuthenticateHeader(aUserName,aPasswordClear); end; { TSQLRestServerAuthenticationHttpBasic } class function TSQLRestServerAuthenticationHttpBasic.GetUserPassFromInHead( Ctxt: TSQLRestServerURIContext; out userPass,user,pass: RawUTF8): boolean; begin userPass := Ctxt.InHeader['Authorization']; if IdemPChar(pointer(userPass),'BASIC ') then begin delete(userPass,1,6); Split(Base64ToBin(userPass),':',user,pass); result := user<>''; end else result := false; end; function TSQLRestServerAuthenticationHttpBasic.RetrieveSession( Ctxt: TSQLRestServerURIContext): TAuthSession; var userPass,user,pass: RawUTF8; begin result := inherited RetrieveSession(Ctxt); if result=nil then exit; // not a valid 'Cookie: mORMot_session_signature=...' header if (result.fExpectedHttpAuthentication<>'') and (result.fExpectedHttpAuthentication=Ctxt.InHeader['Authorization']) then exit; // already previously authenticated for this session if GetUserPassFromInHead(Ctxt,userPass,user,pass) then if user=Result.User.LogonName then with Ctxt.Server.SQLAuthUserClass.Create do try PasswordPlain := pass; // compute SHA-256 hash of the supplied password if PasswordHashHexa=result.User.PasswordHashHexa then begin // match -> store header in result (locked by fSessions.fSafe.Lock) result.fExpectedHttpAuthentication := userPass; exit; end; finally Free; end; result := nil; // identicates authentication error end; class function TSQLRestServerAuthenticationHttpBasic.ComputeAuthenticateHeader( const aUserName,aPasswordClear: RawUTF8): RawUTF8; begin result := 'Authorization: Basic '+BinToBase64(aUsername+':'+aPasswordClear); end; function TSQLRestServerAuthenticationHttpBasic.CheckPassword(Ctxt: TSQLRestServerURIContext; User: TSQLAuthUser; const aPassWord: RawUTF8): boolean; var expectedPass: RawUTF8; begin expectedPass := User.PasswordHashHexa; User.PasswordPlain := aPassWord; // override with SHA-256 hash from HTTP header result := IdemPropNameU(User.PasswordHashHexa,expectedPass); end; function TSQLRestServerAuthenticationHttpBasic.Auth(Ctxt: TSQLRestServerURIContext): boolean; var userPass,user,pass: RawUTF8; U: TSQLAuthUser; Session: TAuthSession; begin if Ctxt.InputExists['UserName'] then begin result := false; // allow other schemes to check this request exit; end; result := true; // this authentication method is exclusive to any other if GetUserPassFromInHead(Ctxt,userPass,user,pass) then begin U := GetUser(Ctxt,user); if U<>nil then try if CheckPassword(Ctxt,U,pass) then begin fServer.SessionCreate(U,Ctxt,Session); // call Ctxt.AuthenticationFailed on error if Session<>nil then begin // see TSQLRestServerAuthenticationHttpAbstract.ClientSessionSign() Ctxt.SetOutSetCookie((COOKIE_SESSION+'=')+CardinalToHexLower(Session.IDCardinal)); if (rsoRedirectForbiddenToAuth in fServer.Options) and (Ctxt.ClientKind=ckAjax) then Ctxt.Redirect(fServer.Model.Root) else SessionCreateReturns(Ctxt,Session,'','',''); exit; // success end; end else Ctxt.AuthenticationFailed(afInvalidPassword); finally U.Free; end else Ctxt.AuthenticationFailed(afUnknownUser); end else begin Ctxt.Call.OutHead := 'WWW-Authenticate: Basic realm="mORMot Server"';; Ctxt.Error('',HTTP_UNAUTHORIZED); // 401 will popup for credentials in browser end; end; {$ifdef DOMAINAUTH} { TSQLRestServerAuthenticationSSPI } const /// maximum number of Windows Authentication context to be handled at once // - 64 should be big enough MAXSSPIAUTHCONTEXTS = 64; function TSQLRestServerAuthenticationSSPI.Auth( Ctxt: TSQLRestServerURIContext): boolean; var i: integer; UserName, InDataEnc: RawUTF8; ticks,ConnectionID: Int64; BrowserAuth: Boolean; CtxArr: TDynArray; SecCtxIdx: Integer; OutData: RawByteString; User: TSQLAuthUser; Session: TAuthSession; begin result := AuthSessionRelease(Ctxt); if result or not Ctxt.InputExists['UserName'] or not Ctxt.InputExists['Data'] then exit; // use ConnectionID to find authentication session ConnectionID := Ctxt.Call^.LowLevelConnectionID; // GET ModelRoot/auth?UserName=&data=... -> windows SSPI auth InDataEnc := Ctxt.InputUTF8['Data']; if InDataEnc='' then begin // client is browser and used HTTP headers to send auth data FindNameValue(Ctxt.Call.InHead,SECPKGNAMEHTTPAUTHORIZATION,InDataEnc); if InDataEnc = '' then begin // no auth data sent, reply with supported auth methods Ctxt.Call.OutHead := SECPKGNAMEHTTPWWWAUTHENTICATE; Ctxt.Call.OutStatus := HTTP_UNAUTHORIZED; // (401) StatusCodeToErrorMessage(Ctxt.Call.OutStatus, Ctxt.Call.OutBody); exit; end; BrowserAuth := True; end else BrowserAuth := False; CtxArr.InitSpecific(TypeInfo(TSecContextDynArray),fSSPIAuthContexts,djInt64); // check for outdated auth context ticks := GetTickCount64-30000; for i := High(fSSPIAuthContexts) downto 0 do if ticks>fSSPIAuthContexts[i].CreatedTick64 then begin FreeSecContext(fSSPIAuthContexts[i]); CtxArr.Delete(i); end; // if no auth context specified, create a new one result := true; SecCtxIdx := CtxArr.Find(ConnectionID); if SecCtxIdx<0 then begin // 1st call: create SecCtxId if High(fSSPIAuthContexts)>MAXSSPIAUTHCONTEXTS then begin fServer.InternalLog( 'Too many Windows Authenticated session in pending state: MAXSSPIAUTHCONTEXTS=%', [MAXSSPIAUTHCONTEXTS],sllUserAuth); exit; end; SecCtxIdx := CtxArr.New; // add a new entry to fSSPIAuthContexts[] InvalidateSecContext(fSSPIAuthContexts[SecCtxIdx],ConnectionID); end; // call SSPI provider if ServerSSPIAuth(fSSPIAuthContexts[SecCtxIdx], Base64ToBin(InDataEnc), OutData) then begin if BrowserAuth then begin Ctxt.Call.OutHead := (SECPKGNAMEHTTPWWWAUTHENTICATE+' ')+BinToBase64(OutData); Ctxt.Call.OutStatus := HTTP_UNAUTHORIZED; // (401) StatusCodeToErrorMessage(Ctxt.Call.OutStatus, Ctxt.Call.OutBody); end else Ctxt.Returns(['result','','data',BinToBase64(OutData)]); exit; // 1st call: send back OutData to the client end; // 2nd call: user was authenticated -> release used context ServerSSPIAuthUser(fSSPIAuthContexts[SecCtxIdx],UserName); {$ifdef WITHLOG} if sllUserAuth in fServer.fLogFamily.Level then fServer.fLogFamily.SynLog.Log(sllUserAuth,'% Authentication success for %', [SecPackageName(fSSPIAuthContexts[SecCtxIdx]),UserName],self); {$endif} // now client is authenticated -> create a session for aUserName // and send back OutData try if UserName='' then exit; User := GetUser(Ctxt,UserName); if User<>nil then try User.PasswordHashHexa := ''; // override with context fServer.SessionCreate(User,Ctxt,Session); // call Ctxt.AuthenticationFailed on error if Session<>nil then with Session.User do if BrowserAuth then SessionCreateReturns(Ctxt,Session,Session.fPrivateSalt,'', (SECPKGNAMEHTTPWWWAUTHENTICATE+' ')+BinToBase64(OutData)) else SessionCreateReturns(Ctxt,Session, BinToBase64(SecEncrypt(fSSPIAuthContexts[SecCtxIdx],Session.fPrivateSalt)), BinToBase64(OutData),''); finally User.Free; end else Ctxt.AuthenticationFailed(afUnknownUser); finally FreeSecContext(fSSPIAuthContexts[SecCtxIdx]); CtxArr.Delete(SecCtxIdx); end; end; class function TSQLRestServerAuthenticationSSPI.ClientComputeSessionKey( Sender: TSQLRestClientURI; User: TSQLAuthUser): RawUTF8; var SecCtx: TSecContext; WithPassword: Boolean; OutData: RawByteString; begin result := ''; InvalidateSecContext(SecCtx,0); WithPassword := User.LogonName<>''; Sender.fSessionData := ''; try repeat if WithPassword then ClientSSPIAuthWithPassword(SecCtx,Sender.fSessionData, User.LogonName,User.PasswordHashHexa,OutData) else ClientSSPIAuth(SecCtx,Sender.fSessionData,User.PasswordHashHexa,OutData); if OutData='' then break; if result<>'' then break; // 2nd pass // 1st call will return data, 2nd call SessionKey result := ClientGetSessionKey(Sender,User,['UserName','','data',BinToBase64(OutData)]); until Sender.fSessionData=''; if result<>'' then result := SecDecrypt(SecCtx,Base64ToBin(result)); finally FreeSecContext(SecCtx); end; // authenticated by Windows on the server side: use the returned // SessionKey + PasswordHashHexa to sign the URI, as usual User.PasswordHashHexa := ''; // should not appear on URI signature end; constructor TSQLRestServerAuthenticationSSPI.Create(aServer: TSQLRestServer); begin inherited Create(aServer); end; destructor TSQLRestServerAuthenticationSSPI.Destroy; var i: integer; begin for i := 0 to High(fSSPIAuthContexts) do FreeSecContext(fSSPIAuthContexts[i]); inherited; end; {$endif DOMAINAUTH} { TSynAuthenticationRest } constructor TSynAuthenticationRest.Create(aServer: TSQLRestServer; const aAllowedGroups: array of integer); begin inherited Create; fServer := aServer; RegisterAllowedGroups(aAllowedGroups); end; procedure TSynAuthenticationRest.RegisterAllowedGroups( const aAllowedGroups: array of integer); var i: integer; begin for i := 0 to high(aAllowedGroups) do AddSortedInteger(fAllowedGroups,aAllowedGroups[i]); end; function TSynAuthenticationRest.GetPassword(const UserName: RawUTF8; out Password: RawUTF8): boolean; var U: TSQLAuthUser; begin if fServer=nil then begin result := false; exit; end; U := fServer.fSQLAuthUserClass.Create(fServer,'LogonName=?',[UserName]); try result := (U.fID>0) and (FastFindIntegerSorted(fAllowedGroups,PtrInt(U.fGroup))>=0); if result then Password := U.PasswordHashHexa; // same as ComputeHash() below finally U.Free; end; end; function TSynAuthenticationRest.GetUsersCount: integer; begin result := 1; // fake answer indicating that authentication is enabled end; class function TSynAuthenticationRest.ComputeHash(Token: Int64; const UserName,PassWord: RawUTF8): cardinal; begin // same as GetPassword() above result := inherited ComputeHash(Token,UserName, TSQLAuthUser.ComputeHashedPassword(Password)); end; { TServiceContainer } function TServiceContainer.AddInterface( const aInterfaces: array of PTypeInfo; aInstanceCreation: TServiceInstanceImplementation; aContractExpected: RawUTF8): boolean; var i: integer; F: TServiceFactoryClient; begin result := false; if (self=nil) or (high(aInterfaces)<0) then exit; CheckInterface(aInterfaces); for i := 0 to high(aInterfaces) do begin F := ServicesFactoryClients.Create( Rest,aInterfaces[i],aInstanceCreation,aContractExpected); AddServiceInternal(F); aContractExpected := ''; // supplied contract is only for the 1st interface end; result := true; end; function TServiceContainer.AddInterface(aInterface: PTypeInfo; aInstanceCreation: TServiceInstanceImplementation; const aContractExpected: RawUTF8): TServiceFactoryClient; begin CheckInterface([aInterface]); result := ServicesFactoryClients.Create(Rest,aInterface,aInstanceCreation,aContractExpected); AddServiceInternal(result); end; function TServiceContainer.Count: integer; begin if self=nil then result := 0 else result := length(fInterface); end; constructor TServiceContainer.Create(aRest: TSQLRest); begin fRest := aRest; fInterfaces.InitSpecific(TypeInfo(TServiceContainerInterfaces), fInterface,djRawUTF8,nil,{caseinsensitive=}true); fInterfaceMethods.InitSpecific(TypeInfo(TServiceContainerInterfaceMethods), fInterfaceMethod,djRawUTF8,nil,{caseinsensitive=}true); fServicesFactoryClients := TServiceFactoryClient; end; destructor TServiceContainer.Destroy; var i: PtrInt; begin for i := 0 to high(fInterface) do fInterface[i].Service.Free; inherited; end; procedure TServiceContainer.Release; begin if (self<>nil) and (fRest<>nil) and (fRest.fServices=self) then FreeAndNil(fRest.fServices); end; function TServiceContainer.AddServiceInternal(aService: TServiceFactory): integer; var MethodIndex: integer; procedure AddOne(const aInterfaceDotMethodName: RawUTF8); var p: PServiceContainerInterfaceMethod; begin p := fInterfaceMethods.AddUniqueName(aInterfaceDotMethodName); p^.InterfaceService := aService; p^.InterfaceMethodIndex := MethodIndex; inc(MethodIndex); end; var aURI: RawUTF8; internal: TServiceInternalMethod; m: integer; begin if (self=nil) or (aService=nil) then result := 0 else // add service factory if ExpectMangledURI then aURI := aService.fInterfaceMangledURI else aURI := aService.fInterfaceURI; PServiceContainerInterface(fInterfaces.AddUniqueName(aURI,@result))^.Service := aService; // add associated methods aURI := aURI+'.'; MethodIndex := 0; for internal := Low(TServiceInternalMethod) to High(TServiceInternalMethod) do AddOne(aURI+SERVICE_PSEUDO_METHOD[internal]); for m := 0 to aService.fInterface.fMethodsCount-1 do AddOne(aURI+aService.fInterface.fMethods[m].URI); end; procedure TServiceContainer.CheckInterface(const aInterfaces: array of PTypeInfo); var i: integer; begin for i := 0 to high(aInterfaces) do if aInterfaces[i]=nil then raise EServiceException.CreateUTF8('%: aInterfaces[%]=nil',[self,i]) else with aInterfaces[i]^, PInterfaceTypeData(ClassType)^ do if Kind<>tkInterface then raise EServiceException.CreateUTF8('%: % is not an interface',[self,Name]) else if not (ifHasGuid in IntfFlags) then raise EServiceException.CreateUTF8('%: % interface has no GUID',[self,Name]) else if Info(IntfGuid)<>nil then raise EServiceException.CreateUTF8('%: % GUID already registered',[self,Name]); end; procedure TServiceContainer.SetExpectMangledURI(aValue: Boolean); var i: Integer; toregisteragain: TServiceContainerInterfaces; begin if aValue=fExpectMangledURI then exit; fExpectMangledURI := aValue; toregisteragain := fInterface; // same services, but other URIs fInterface := nil; fInterfaces.InitSpecific(TypeInfo(TServiceContainerInterfaces), fInterface,djRawUTF8,nil,{caseinsensitive=}not aValue); fInterfaceMethod := nil; fInterfaceMethods.InitSpecific(TypeInfo(TServiceContainerInterfaceMethods), fInterfaceMethod,djRawUTF8,nil,not aValue); for i := 0 to high(toregisteragain) do AddServiceInternal(toregisteragain[i].Service); end; procedure TServiceContainer.SetInterfaceMethodBits(MethodNamesCSV: PUTF8Char; IncludePseudoMethods: boolean; out bits: TServiceContainerInterfaceMethodBits); var i,n: integer; method: RawUTF8; begin FillCharFast(bits,SizeOf(bits),0); n := length(fInterfaceMethod); if n>SizeOf(bits) shl 3 then raise EServiceException.CreateUTF8('%.SetInterfaceMethodBits: n=%',[self,n]); if IncludePseudoMethods then for i := 0 to n-1 do if fInterfaceMethod[i].InterfaceMethodIndexnil do begin GetNextItem(MethodNamesCSV,',',method); if PosExChar('.',method)=0 then begin for i := 0 to n-1 do with fInterfaceMethod[i] do // O(n) search is fast enough here if (InterfaceMethodIndex>=SERVICE_PSEUDO_METHOD_COUNT) and IdemPropNameU(method,InterfaceService.fInterface. fMethods[InterfaceMethodIndex-SERVICE_PSEUDO_METHOD_COUNT].URI) then include(bits,i); end else begin i := fInterfaceMethods.FindHashed(method); // O(1) search if i>=0 then include(bits,i); end; end; end; function TServiceContainer.GetMethodName(ListInterfaceMethodIndex: integer): RawUTF8; begin if cardinal(ListInterfaceMethodIndex)>=cardinal(length(fInterfaceMethod)) then result := '' else with fInterfaceMethod[ListInterfaceMethodIndex] do result := InterfaceService.fInterface.GetMethodName(InterfaceMethodIndex); end; function TServiceContainer.GetService(const aURI: RawUTF8): TServiceFactory; var i: Integer; begin if (self<>nil) and (aURI<>'') then begin i := fInterfaces.FindHashed(aURI); if i>=0 then result := fInterface[i].Service else result := nil; end else result := nil; end; function TServiceContainer.Info(aTypeInfo: PTypeInfo): TServiceFactory; var i: PtrInt; p: PServiceContainerInterface; begin if self<>nil then begin p := pointer(fInterface); for i := 1 to length(fInterface) do begin result := p^.Service; if result.fInterface.fInterfaceTypeInfo=aTypeInfo then exit else inc(p); end; end; result := nil; end; function TServiceContainer.Info(const aGUID: TGUID): TServiceFactory; var i: PtrInt; p: PServiceContainerInterface; begin if self<>nil then begin p := pointer(fInterface); for i := 1 to length(fInterface) do begin result := p^.Service; if IsEqualGUID(@result.fInterface.fInterfaceIID,@aGUID) then exit else inc(p); end; end; result := nil; end; procedure TServiceContainer.SetGUIDs(out Services: TGUIDDynArray); var i,n: PtrInt; begin if self=nil then exit; n := length(fInterface); SetLength(Services,n); for i := 0 to n-1 do Services[i] := fInterface[i].Service.fInterface.fInterfaceIID; end; procedure TServiceContainer.SetInterfaceNames(out Names: TRawUTF8DynArray); var i,n: PtrInt; begin if self=nil then exit; n := length(fInterface); SetLength(Names,n); for i := 0 to n-1 do Names[i] := fInterface[i].Service.fInterface.fInterfaceURI; end; function TServiceContainer.AsJson: RawJSON; var WR: TTextWriter; i: PtrInt; temp: TTextWriterStackBuffer; begin result := ''; if (self=nil) or (fInterface=nil) then exit; WR := TJSONSerializer.CreateOwnedStream(temp); try WR.Add('['); for i := 0 to high(fInterface) do begin WR.AddString(fInterface[i].Service.Contract); WR.Add(','); end; WR.CancelLastComma; WR.Add(']'); WR.SetText(RawUTF8(result)); finally WR.Free; end; end; function TServiceContainer.TryResolve(aInterface: PTypeInfo; out Obj): boolean; var factory: TServiceFactory; begin factory := Info(aInterface); if factory=nil then result := inherited TryResolve(aInterface,Obj) else result := factory.Get(Obj); end; function TServiceContainer.Index(aIndex: integer): TServiceFactory; begin if (self=nil) or (cardinal(aIndex)>cardinal(high(fInterface))) then result := nil else result := fInterface[aIndex].Service; end; function TServiceContainer.CallBackUnRegister(const Callback: IInvokable): boolean; begin result := false; // nothing to be done here end; { TInterfacedObjectFake } const // QueryInterface, _AddRef and _Release methods are hard-coded RESERVED_VTABLE_SLOTS = 3; // see http://docwiki.embarcadero.com/RADStudio/en/Program_Control {$ifdef CPU64} // maximum stack size at method execution must match .PARAMS 64 (minus 4 regs) MAX_EXECSTACK = 60*8; {$else} // maximum stack size at method execution {$ifdef CPUARM} MAX_EXECSTACK = 60*4; {$else} MAX_EXECSTACK = 1024; {$endif} {$endif CPU64} {$ifdef CPUX86} // 32-bit integer param registers (in "register" calling convention) REGEAX = 1; REGEDX = 2; REGECX = 3; PARAMREG_FIRST = REGEAX; PARAMREG_LAST = REGECX; // floating-point params are passed by reference {$endif CPUX86} {$ifdef CPUX64} // 64-bit integer param registers {$ifdef LINUX} REGRDI = 1; REGRSI = 2; REGRDX = 3; REGRCX = 4; REGR8 = 5; REGR9 = 6; PARAMREG_FIRST = REGRDI; PARAMREG_RESULT = REGRSI; {$else} REGRCX = 1; REGRDX = 2; REGR8 = 3; REGR9 = 4; PARAMREG_FIRST = REGRCX; PARAMREG_RESULT = REGRDX; {$endif} PARAMREG_LAST = REGR9; // 64-bit floating-point (double) registers REGXMM0 = 1; REGXMM1 = 2; REGXMM2 = 3; REGXMM3 = 4; {$ifdef LINUX} REGXMM4 = 5; REGXMM5 = 6; REGXMM6 = 7; REGXMM7 = 8; FPREG_FIRST = REGXMM0; FPREG_LAST = REGXMM7; {$else} FPREG_FIRST = REGXMM0; FPREG_LAST = REGXMM3; {$endif} {$define HAS_FPREG} {$endif CPUX64} {$ifdef CPUARM} // 32-bit integer param registers REGR0 = 1; REGR1 = 2; REGR2 = 3; REGR3 = 4; PARAMREG_FIRST = REGR0; PARAMREG_LAST = REGR3; PARAMREG_RESULT = REGR1; // 64-bit floating-point (double) registers REGD0 = 1; REGD1 = 2; REGD2 = 3; REGD3 = 4; REGD4 = 5; REGD5 = 6; REGD6 = 7; REGD7 = 8; FPREG_FIRST = REGD0; FPREG_LAST = REGD7; {$define HAS_FPREG} {$endif CPUARM} {$ifdef CPUAARCH64} // 64-bit integer param registers REGX0 = 1; REGX1 = 2; REGX2 = 3; REGX3 = 4; REGX4 = 5; REGX5 = 6; REGX6 = 7; REGX7 = 8; PARAMREG_FIRST = REGX0; PARAMREG_LAST = REGX7; PARAMREG_RESULT = REGX1; // 64-bit floating-point (double) registers REGD0 = 1; // map REGV0 128-bit NEON register REGD1 = 2; // REGV1 REGD2 = 3; // REGV2 REGD3 = 4; // REGV3 REGD4 = 5; // REGV4 REGD5 = 6; // REGV5 REGD6 = 7; // REGV6 REGD7 = 8; // REGV7 FPREG_FIRST = REGD0; FPREG_LAST = REGD7; {$define HAS_FPREG} {$endif CPUAARCH64} PTRSIZ = SizeOf(Pointer); PTRSHR = {$ifdef CPU64}3{$else}2{$endif}; STACKOFFSET_NONE = -1; // ordinal values are stored within 64-bit buffer, and records in a RawUTF8 ARGS_TO_VAR: array[TServiceMethodValueType] of TServiceMethodValueVar = ( smvvNone, smvvSelf, smvv64, smvv64, smvv64, smvv64, smvv64, smvv64, smvv64, smvv64, smvv64, smvvRawUTF8, smvvString, smvvRawUTF8, smvvWideString, smvv64, smvvRecord, {$ifndef NOVARIANTS}smvvRecord,{$endif} smvvObject, smvvRawUTF8, smvvDynArray, smvvInterface); {$ifdef CPU32} // always aligned to 8 bytes boundaries for 64-bit ARGS_IN_STACK_SIZE: array[TServiceMethodValueType] of Cardinal = ( 0, PTRSIZ,PTRSIZ, PTRSIZ,PTRSIZ,PTRSIZ, PTRSIZ, 8, 8, 8, // None, Self, Boolean, Enum, Set, Integer, Cardinal, Int64, Double, DateTime, 8, PTRSIZ, PTRSIZ, PTRSIZ, PTRSIZ, 0, PTRSIZ, // Currency, RawUTF8, String, RawByteString, WideString, Binary, Record, {$ifndef NOVARIANTS}PTRSIZ,{$endif} // Variant PTRSIZ, PTRSIZ, PTRSIZ, PTRSIZ); // Object, RawJSON, DynArray, Interface {$endif} ARGS_RESULT_BY_REF: TServiceMethodValueTypes = [ smvRawUTF8, smvRawJSON, smvString, smvRawByteString, smvWideString, smvRecord, {$ifndef NOVARIANTS}smvVariant,{$endif} smvDynArray]; PSEUDO_RESULT_NAME: string[6] = 'Result'; PSEUDO_SELF_NAME: string[4] = 'Self'; INTEGER_NAME: string[7] = 'Integer'; CARDINAL_NAME: string[8] = 'Cardinal'; type /// map the stack memory layout at TInterfacedObjectFake.FakeCall() TFakeCallStack = packed record {$ifdef CPUX86} EDX, ECX, MethodIndex, EBP, Ret: cardinal; {$else} {$ifdef Linux} ParamRegs: packed array[PARAMREG_FIRST..PARAMREG_LAST] of pointer; {$endif} {$ifdef HAS_FPREG} FPRegs: packed array[FPREG_FIRST..FPREG_LAST] of double; {$endif} MethodIndex: PtrUInt; Frame: pointer; Ret: pointer; {$ifndef Linux} ParamRegs: packed array[PARAMREG_FIRST..PARAMREG_LAST] of pointer; {$endif} {$endif CPUX86} {$ifdef CPUARM} // alf: on ARM, there is more on the stack than you will expect DummyStack: packed array[0..9] of pointer; {$endif} {$ifdef CPUAARCH64} // alf: on AARCH64, there is more on the stack than you will expect DummyStack: packed array[0..0] of pointer; {$endif} Stack: packed array[word] of byte; end; /// instances of this class will emulate a given interface // - as used by TInterfaceFactory.CreateFakeInstance TInterfacedObjectFake = class(TInterfacedObjectFromFactory) protected fVTable: PPointerArray; fServiceFactory: TServiceFactory; function FakeCall(var aCall: TFakeCallStack): Int64; {$ifdef FPC} {$ifdef CPUARM} // on ARM, the FakeStub needs to be here, otherwise the FakeCall cannot be found by the FakeStub procedure ArmFakeStub; {$endif} {$ifdef CPUAARCH64} // on Aarch64, the FakeStub needs to be here, otherwise the FakeCall cannot be found by the FakeStub procedure AArch64FakeStub; {$endif} function FakeQueryInterface( {$IFDEF FPC_HAS_CONSTREF}constref{$ELSE}const{$ENDIF} IID: TGUID; out Obj): longint; {$IFNDEF WINDOWS}cdecl{$ELSE}stdcall{$ENDIF}; function Fake_AddRef: longint; {$IFNDEF WINDOWS}cdecl{$ELSE}stdcall{$ENDIF}; function Fake_Release: longint; {$IFNDEF WINDOWS}cdecl{$ELSE}stdcall{$ENDIF}; {$else} function FakeQueryInterface(const IID: TGUID; out Obj): HResult; stdcall; function Fake_AddRef: Integer; stdcall; function Fake_Release: Integer; stdcall; {$endif} function SelfFromInterface: TInterfacedObjectFake; {$ifdef HASINLINE}inline;{$endif} procedure InterfaceWrite(W: TJSONSerializer; const aMethod: TServiceMethod; const aParamInfo: TServiceMethodArgument; aParamValue: Pointer); virtual; public /// create an instance, using the specified interface constructor Create(aFactory: TInterfaceFactory; aServiceFactory: TServiceFactory; aOptions: TInterfacedObjectFromFactoryOptions; aInvoke: TOnFakeInstanceInvoke; aNotifyDestroy: TOnFakeInstanceDestroy); /// retrieve one local instance of this interface procedure Get(out Obj); end; TInterfacedObjectFakeClient = class(TInterfacedObjectFake) protected fClient: TServiceFactoryClient; procedure InterfaceWrite(W: TJSONSerializer; const aMethod: TServiceMethod; const aParamInfo: TServiceMethodArgument; aParamValue: Pointer); override; public constructor Create(aClient: TServiceFactoryClient; aInvoke: TOnFakeInstanceInvoke; aNotifyDestroy: TOnFakeInstanceDestroy); destructor Destroy; override; end; TInterfacedObjectFakeServer = class(TInterfacedObjectFake) protected fServer: TSQLRestServer; fLowLevelConnectionID: Int64; fService: TServiceFactoryServer; fReleasedOnClientSide: boolean; fFakeInterface: Pointer; fRaiseExceptionOnInvokeError: boolean; function CallbackInvoke(const aMethod: TServiceMethod; const aParams: RawUTF8; aResult, aErrorMsg: PRawUTF8; aClientDrivenID: PCardinal; aServiceCustomAnswer: PServiceCustomAnswer): boolean; virtual; public constructor Create(aRequest: TSQLRestServerURIContext; aFactory: TInterfaceFactory; aFakeID: Integer); destructor Destroy; override; end; EInterfaceStub = class(EInterfaceFactoryException) public constructor Create(Sender: TInterfaceStub; const Method: TServiceMethod; const Error: RawUTF8); overload; constructor Create(Sender: TInterfaceStub; const Method: TServiceMethod; const Format: RawUTF8; const Args: array of const); overload; end; constructor TInterfacedObjectFake.Create(aFactory: TInterfaceFactory; aServiceFactory: TServiceFactory; aOptions: TInterfacedObjectFromFactoryOptions; aInvoke: TOnFakeInstanceInvoke; aNotifyDestroy: TOnFakeInstanceDestroy); begin inherited Create(aFactory,aOptions,aInvoke,aNotifyDestroy); fVTable := aFactory.GetMethodsVirtualTable; fServiceFactory := aServiceFactory; end; function TInterfacedObjectFake.SelfFromInterface: TInterfacedObjectFake; {$ifdef HASINLINE} begin result := pointer(PtrUInt(self)-PtrUInt(@TInterfacedObjectFake(nil).fVTable)); end; {$else} asm sub eax,TInterfacedObjectFake.fVTable end; {$endif HASINLINE} function TInterfacedObjectFake.Fake_AddRef: {$ifdef FPC}longint{$else}integer{$endif}; begin result := SelfFromInterface._AddRef; end; function TInterfacedObjectFake.Fake_Release: {$ifdef FPC}longint{$else}integer{$endif}; begin result := SelfFromInterface._Release; end; function TInterfacedObjectFake.FakeQueryInterface( {$ifdef FPC} {$IFDEF FPC_HAS_CONSTREF}constref{$ELSE}const{$ENDIF} IID: TGUID; out Obj): longint; {$else} const IID: TGUID; out Obj): HResult; {$endif} begin self := SelfFromInterface; if IsEqualGUID(@IID,@fFactory.fInterfaceIID) then begin pointer(Obj) := @fVTable; _AddRef; result := NOERROR; end else if GetInterface(IID,Obj) then result := NOERROR else result := {$ifdef FPC}longint{$endif}(E_NOINTERFACE); end; procedure TInterfacedObjectFake.Get(out Obj); begin pointer(Obj) := @fVTable; _AddRef; end; procedure IgnoreComma(var P: PUTF8Char); {$ifdef HASINLINE}inline;{$endif} begin if P<>nil then begin while (P^<=' ') and (P^<>#0) do inc(P); if P^=',' then inc(P); end; end; function TInterfacedObjectFake.FakeCall(var aCall: TFakeCallStack): Int64; var method: PServiceMethod; resultType: TServiceMethodValueType; // type of value stored into result procedure RaiseError(const Format: RawUTF8; const Args: array of const); var msg: RawUTF8; begin msg := FormatUTF8(Format,Args); raise EInterfaceFactoryException.CreateUTF8('%.FakeCall(%.%) failed: %', [self,fFactory.fInterfaceTypeInfo^.Name,method^.URI,msg]); end; procedure InternalProcess; var Params: TJSONSerializer; Error, ResArray, ParamsJSON: RawUTF8; arg, ValLen: integer; V: PPointer; R, Val: PUTF8Char; resultAsJSONObject: boolean; opt: TTextWriterWriteObjectOptions; ServiceCustomAnswerPoint: PServiceCustomAnswer; DynArrays: array[0..MAX_METHOD_ARGS-1] of TDynArray; Value: array[0..MAX_METHOD_ARGS-1] of pointer; I64s: array[0..MAX_METHOD_ARGS-1] of Int64; temp: TTextWriterStackBuffer; begin Params := TJSONSerializer.CreateOwnedStream(temp); try // create the parameters if ifoJsonAsExtended in fOptions then include(Params.fCustomOptions,twoForceJSONExtended) else include(Params.fCustomOptions,twoForceJSONStandard); // e.g. for AJAX if ifoDontStoreVoidJSON in fOptions then begin opt := DEFAULT_WRITEOPTIONS[true]; include(Params.fCustomOptions,twoIgnoreDefaultInRecord); end else opt := DEFAULT_WRITEOPTIONS[false]; FillCharFast(I64s,method^.ArgsUsedCount[smvv64]*SizeOf(Int64),0); for arg := 1 to high(method^.Args) do with method^.Args[arg] do if ValueType>smvSelf then begin {$ifdef HAS_FPREG} // x64, arm, aarch64 if FPRegisterIdent>0 then V := @aCall.FPRegs[FPREG_FIRST+FPRegisterIdent-1] else if RegisterIdent>0 then V := @aCall.ParamRegs[PARAMREG_FIRST+RegisterIdent-1] else {$endif} V := nil; if RegisterIdent=PARAMREG_FIRST then RaiseError('unexpected self',[]); {$ifdef CPUX86} case RegisterIdent of REGEAX: RaiseError('unexpected self',[]); REGEDX: V := @aCall.EDX; REGECX: V := @aCall.ECX; else {$endif} if V=nil then if (SizeInStack>0) and (InStackOffset<>STACKOFFSET_NONE) then V := @aCall.Stack[InStackOffset] else V := @I64s[IndexVar]; // for results in CPU {$ifdef CPUX86} end; {$endif} if vPassedByReference in ValueKindAsm then V := PPointer(V)^; if ValueType=smvDynArray then DynArrays[IndexVar].InitFrom(DynArrayWrapper,V^); Value[arg] := V; if ValueDirection in [smdConst,smdVar] then case ValueType of smvInterface: InterfaceWrite(Params,method^,method^.Args[arg],V^); smvDynArray: begin if vIsObjArray in ValueKindAsm then Params.AddObjArrayJSON(V^,opt) else Params.AddDynArrayJSON(DynArrays[IndexVar]); Params.Add(','); end; else AddJSON(Params,V,opt); end; end; Params.CancelLastComma; Params.SetText(ParamsJSON); // without [ ] finally Params.Free; end; // call remote server or stub implementation if method^.ArgsResultIsServiceCustomAnswer then ServiceCustomAnswerPoint := Value[method^.ArgsResultIndex] else ServiceCustomAnswerPoint := nil; if not fInvoke(method^,ParamsJSON, @ResArray,@Error,@fClientDrivenID,ServiceCustomAnswerPoint) then RaiseError('''%''',[Error]); // retrieve method result and var/out parameters content if ServiceCustomAnswerPoint=nil then if ResArray<>'' then begin R := pointer(ResArray); if R^ in [#1..' '] then repeat inc(R) until not(R^ in [#1..' ']); resultAsJSONObject := false; // [value,...] JSON array format if R^='{' then // {"paramname":value,...} JSON object format resultAsJSONObject := true else if R^<>'[' then RaiseError('JSON array/object result expected',[]); inc(R); arg := method^.ArgsOutFirst; if arg>0 then repeat if resultAsJSONObject then begin Val := GetJSONPropName(R,@ValLen); if Val=nil then break; // end of JSON object if (arg>0) and not IdemPropName(method^.Args[arg].ParamName^,Val,ValLen) then begin arg := method^.ArgIndex(Val,ValLen,false); // only if were not in-order if arg<0 then RaiseError('unexpected parameter [%]',[Val]); end; end; with method^.Args[arg] do begin //assert(ValueDirection in [smdVar,smdOut,smdResult]); V := Value[arg]; FromJSON(method^.InterfaceDotMethodName,R,V,nil {$ifndef NOVARIANTS},fFactory.DocVariantOptions{$endif}); if ValueDirection=smdResult then begin resultType := ValueType; if ValueType in [smvBoolean..smvCurrency] then // ordinal/real result values to CPU/FPU registers MoveFast(V^,Result,SizeInStorage); end; end; if R=nil then break; if R^ in [#1..' '] then repeat inc(R) until not(R^ in [#1..' ']); if resultAsJSONObject then begin if (R^=#0) or (R^='}') then break else // end of JSON object if not method^.ArgNext(arg,false) then arg := 0; // no next result argument -> force manual search end else if not method^.ArgNext(arg,false) then break; // end of JSON array until false; end else if method^.ArgsOutputValuesCount>0 then RaiseError('method returned value, but ResArray=''''',[]); end; begin (* WELCOME ABOARD: you just landed in TInterfacedObjectFake.FakeCall() ! if your debugger reached here, you are executing a "fake" interface forged to call a remote SOA server or mock/stub an interface *) self := SelfFromInterface; if aCall.MethodIndex>=fFactory.fMethodsCount then raise EInterfaceFactoryException.CreateUTF8( '%.FakeCall(%.%) failed: out of range method %>=%',[self, fFactory.fInterfaceTypeInfo^.Name,aCall.MethodIndex,fFactory.fMethodsCount]); method := @fFactory.fMethods[aCall.MethodIndex]; if not Assigned(fInvoke)then RaiseError('fInvoke=nil',[]); result := 0; resultType := smvNone; InternalProcess; // use an inner proc to ensure direct fld/fild FPU ops case resultType of // al/ax/eax/eax:edx/rax already in result {$ifdef HAS_FPREG} smvDouble,smvDateTime: aCall.FPRegs[FPREG_FIRST] := unaligned(PDouble(@result)^); {$else} smvDouble,smvDateTime: asm fld qword ptr [result] end; // in st(0) smvCurrency: asm fild qword ptr [result] end; // in st(0) {$endif} end; end; procedure TInterfacedObjectFake.InterfaceWrite(W: TJSONSerializer; const aMethod: TServiceMethod; const aParamInfo: TServiceMethodArgument; aParamValue: Pointer); begin raise EInterfaceFactoryException.CreateUTF8('%: unhandled %.%(%: %) argument', [self,fFactory.fInterfaceTypeInfo^.Name,aMethod.URI, aParamInfo.ParamName^,aParamInfo.ArgTypeName^]); end; constructor TInterfacedObjectFakeClient.Create(aClient: TServiceFactoryClient; aInvoke: TOnFakeInstanceInvoke; aNotifyDestroy: TOnFakeInstanceDestroy); var opt: TInterfacedObjectFromFactoryOptions; begin fClient := aClient; if (fClient.fClient<>nil) and (fClient.fClient.fSessionID<>0) and (fClient.fClient.fSessionUser<>nil) then opt := [ifoJsonAsExtended,ifoDontStoreVoidJSON] else opt := []; inherited Create(aClient.fInterface,aClient,opt,aInvoke,aNotifyDestroy); end; procedure TInterfacedObjectFakeClient.InterfaceWrite(W: TJSONSerializer; const aMethod: TServiceMethod; const aParamInfo: TServiceMethodArgument; aParamValue: Pointer); begin W.Add(fClient.fClient.FakeCallbackRegister(fClient,aMethod,aParamInfo,aParamValue)); W.Add(','); end; destructor TInterfacedObjectFakeClient.Destroy; begin fClient.fClient.InternalLog('%(%).Destroy I%', [ClassType,pointer(self),fClient.InterfaceURI]); inherited Destroy; end; constructor TInterfacedObjectFakeServer.Create(aRequest: TSQLRestServerURIContext; aFactory: TInterfaceFactory; aFakeID: Integer); var opt: TInterfacedObjectFromFactoryOptions; begin if aRequest.ClientKind=ckFramework then opt := [ifoJsonAsExtended,ifoDontStoreVoidJSON] else opt := []; fServer := aRequest.Server; fService := aRequest.Service; fLowLevelConnectionID := aRequest.Call^.LowLevelConnectionID; fClientDrivenID := aFakeID; inherited Create(aFactory,nil,opt,CallbackInvoke,nil); Get(fFakeInterface); end; destructor TInterfacedObjectFakeServer.Destroy; begin if fServer<>nil then begin // may be called asynchronously AFTER server is down fServer.InternalLog('%(%:%).Destroy I%', [ClassType,pointer(self),fClientDrivenID,fService.InterfaceURI]); if fServer.Services<>nil then with (fServer.Services as TServiceContainerServer) do if fFakeCallbacks<>nil then FakeCallbackRemove(self); end; inherited Destroy; end; function TInterfacedObjectFakeServer.CallbackInvoke(const aMethod: TServiceMethod; const aParams: RawUTF8; aResult, aErrorMsg: PRawUTF8; aClientDrivenID: PCardinal; aServiceCustomAnswer: PServiceCustomAnswer): boolean; begin // here aClientDrivenID^ = FakeCall ID if fServer=nil then begin if aErrorMsg<>nil then aErrorMsg^ := 'Server was already shutdown'; result := true; exit; end; if not Assigned(fServer.OnNotifyCallback) then raise EServiceException.CreateUTF8('%(%) does not support callbacks for I%', [fServer,fServer.Model.Root,aMethod.InterfaceDotMethodName]); if fReleasedOnClientSide then begin if not IdemPropName(fFactory.fInterfaceTypeInfo^.Name,'ISynLogCallback') then fServer.InternalLog('%.CallbackInvoke: % instance has been released on '+ 'the client side, so I% callback notification was NOT sent', [self,fFactory.fInterfaceTypeInfo^.Name,aMethod.InterfaceDotMethodName],sllWarning); if fRaiseExceptionOnInvokeError or ((fServer.Services<>nil) and (coRaiseExceptionIfReleasedByClient in (fServer.Services as TServiceContainerServer).CallbackOptions)) then begin if aErrorMsg<>nil then FormatUTF8('%.CallbackInvoke(I%): instance has been released on client side', [self,aMethod.InterfaceDotMethodName],aErrorMsg^); result := false; // will raise an exception end else result := true; // do not raise an exception here: just log warning end else begin if aMethod.ArgsOutputValuesCount=0 then aResult := nil; // no result -> asynchronous non blocking callback result := fServer.OnNotifyCallback(fServer,aMethod.InterfaceDotMethodName, aParams,fLowLevelConnectionID,aClientDrivenID^,aResult,aErrorMsg); end; end; procedure TSQLRestServerURIContext.ExecuteCallback(var Par: PUTF8Char; ParamInterfaceInfo: PTypeInfo; out Obj); var FakeID: PtrInt; factory: TInterfaceFactory; instance: TInterfacedObjectFakeServer; begin if not Assigned(Server.OnNotifyCallback) then raise EServiceException.CreateUTF8('% does not implement callbacks for I%', [Server,ParamInterfaceInfo^.Name]); FakeID := GetInteger(GetJSONField(Par,Par)); // GetInteger returns a PtrInt if Par=nil then Par := @NULL_SHORTSTRING; // allow e.g. '[12345]' if (FakeID=0) or (ParamInterfaceInfo=TypeInfo(IInvokable)) then begin pointer(Obj) := pointer(FakeID); // Obj = IInvokable(FakeID) exit; end; factory := TInterfaceFactory.Get(ParamInterfaceInfo); instance := TInterfacedObjectFakeServer.Create(self,factory,FakeID); pointer(Obj) := instance.fFakeInterface; (Server.Services as TServiceContainerServer).FakeCallbackAdd(instance); end; { TInterfacedObjectFromFactory } constructor TInterfacedObjectFromFactory.Create(aFactory: TInterfaceFactory; aOptions: TInterfacedObjectFromFactoryOptions; aInvoke: TOnFakeInstanceInvoke; aNotifyDestroy: TOnFakeInstanceDestroy); begin inherited Create; fFactory := aFactory; fOptions := aOptions; fInvoke := aInvoke; fNotifyDestroy := aNotifyDestroy; end; destructor TInterfacedObjectFromFactory.Destroy; var C: TClass; begin if Assigned(fNotifyDestroy) then try // release server instance fNotifyDestroy(fClientDrivenID); except on E: Exception do begin C := E.ClassType; if (C=EInterfaceStub) or (C=EInterfaceFactoryException) or (C=EAccessViolation) {$ifndef LVCL}or (C=EInvalidPointer){$endif} then raise; // ignore all low-level exceptions end; end; inherited; end; { TInterfaceFactory } function TypeInfoToMethodValueType(P: PTypeInfo): TServiceMethodValueType; var parser: PJSONCustomParser; begin result := smvNone; if P<>nil then case P^.Kind of tkInteger: case P^.OrdType of otSLong: result := smvInteger; otULong: result := smvCardinal; end; tkInt64{$ifdef FPC}, tkQWord{$endif}: result := smvInt64; {$ifdef FPC} tkBool: result := smvBoolean; tkEnumeration: result := smvEnum; {$else} tkEnumeration: if P=TypeInfo(boolean) then result := smvBoolean else result := smvEnum; {$endif} tkSet: result := smvSet; tkFloat: if (P=TypeInfo(TDateTime)) or (P=TypeInfo(TDateTimeMS)) then result := smvDateTime else case P^.FloatType of ftCurr: result := smvCurrency; ftDoub: result := smvDouble; end; {$ifdef FPC}tkLStringOld,{$endif} tkLString: if P=TypeInfo(RawJSON) then result := smvRawJSON else if (P=TypeInfo(RawByteString)) or (P=TypeInfo(TSQLRawBlob)) then result := smvRawByteString else {$ifndef UNICODE} if P=TypeInfo(AnsiString) then result := smvString else result := smvRawUTF8; // UTF-8 by default {$ifdef HASVARUSTRING} tkUString: result := smvRawUTF8; {$endif} {$else UNICODE} result := smvRawUTF8; tkUString: result := smvString; {$endif UNICODE} tkWString: result := smvWideString; tkClass: with P^.ClassType^ do if ClassHasPublishedFields(ClassType) or // = oPersistent or oCustomPropName (JSONObjectFromClass(ClassType,parser) in // serialized w/o RTTI ? [{$ifndef LVCL}oCollection,{$endif}oObjectList,oUtfs,oStrings, oException,oCustomReaderWriter]) then result := smvObject; // JSONToObject/ObjectToJSON types {$ifdef FPC}tkObject,{$endif} tkRecord: // JSON or Base64 encoding of our RecordLoad / RecordSave binary format result := smvRecord; {$ifndef NOVARIANTS} tkVariant: result := smvVariant; {$endif} tkDynArray: // TDynArray.LoadFromJSON / TTextWriter.AddDynArrayJSON type result := smvDynArray; tkInterface: result := smvInterface; tkUnknown: // assume var/out untyped arguments are in fact objects result := smvObject; end; end; var InterfaceFactoryCache: TSynObjectListLocked; procedure EnterInterfaceFactoryCache; begin if InterfaceFactoryCache=nil then GarbageCollectorFreeAndNil(InterfaceFactoryCache,TSynObjectListLocked.Create); InterfaceFactoryCache.Safe.Lock; end; class function TInterfaceFactory.Get(aInterface: PTypeInfo): TInterfaceFactory; var i: integer; F: ^TInterfaceFactory; begin if (aInterface=nil) or (aInterface^.Kind<>tkInterface) then raise EInterfaceFactoryException.CreateUTF8('%.Get(nil)',[self]); EnterInterfaceFactoryCache; try F := pointer(InterfaceFactoryCache.List); for i := 1 to InterfaceFactoryCache.Count do if F^.fInterfaceTypeInfo=aInterface then begin result := F^; exit; // retrieved from cache end else inc(F); // not existing -> create new instance from RTTI {$ifdef HASINTERFACERTTI} result := TInterfaceFactoryRTTI.Create(aInterface); InterfaceFactoryCache.Add(result); {$else} result := nil; // make compiler happy raise EInterfaceFactoryException.CreateUTF8('No RTTI available for I%: please '+ 'define the methods using a TInterfaceFactoryGenerated wrapper',[aInterface^.Name]); {$endif} finally InterfaceFactoryCache.Safe.UnLock; end; end; class procedure TInterfaceFactory.RegisterInterfaces(const aInterfaces: array of PTypeInfo); {$ifdef HASINTERFACERTTI} var i: integer; begin for i := 0 to high(aInterfaces) do Get(aInterfaces[i]); end; {$else} begin // in fact, TInterfaceFactoryGenerated.RegisterInterface() should do it end; {$endif} class function TInterfaceFactory.Get(const aGUID: TGUID): TInterfaceFactory; type TGUID32 = packed record a,b{$ifdef CPU32},c,d{$endif}: PtrInt; end; PGUID32 = ^TGUID32; // brute force search optimization var i,ga: PtrInt; F: ^TInterfaceFactory; GUID32: TGUID32 absolute aGUID; begin if InterfaceFactoryCache<>nil then begin InterfaceFactoryCache.Safe.Lock; F := pointer(InterfaceFactoryCache.List); ga := GUID32.a; for i := 1 to InterfaceFactoryCache.Count do with PGUID32(@F^.fInterfaceIID)^ do if (a=ga) and (b=GUID32.b) {$ifdef CPU32}and (c=GUID32.c) and (d=GUID32.d){$endif} then begin result := F^; InterfaceFactoryCache.Safe.UnLock; exit; end else inc(F); InterfaceFactoryCache.Safe.UnLock; end; result := nil; end; function ToText({$IFDEF FPC_HAS_CONSTREF}constref{$ELSE}const{$ENDIF} aGUID: TGUID): TGUIDShortString; var fact: TInterfaceFactory; begin fact := TInterfaceFactory.Get(aGUID); if fact=nil then GUIDToShort(aGUID,result) else result := fact.fInterfaceTypeInfo^.Name; end; class procedure TInterfaceFactory.AddToObjArray(var Obj: TInterfaceFactoryObjArray; const aGUIDs: array of TGUID); var i: integer; fac: TInterfaceFactory; begin for i := 0 to high(aGUIDs) do begin fac := Get(aGUIDs[i]); if fac<>nil then ObjArrayAddOnce(Obj,fac); end; end; class function TInterfaceFactory.GUID2TypeInfo( const aGUIDs: array of TGUID): PTypeInfoDynArray; var i: integer; begin result := nil; SetLength(result,length(aGUIDs)); for i := 0 to high(aGUIDs) do result[i] := GUID2TypeInfo(aGUIDs[i]); end; class function TInterfaceFactory.GUID2TypeInfo(const aGUID: TGUID): PTypeInfo; var fact: TInterfaceFactory; begin fact := Get(aGUID); if fact=nil then raise EServiceException.CreateUTF8('%.GUID2TypeInfo(%): Interface not '+ 'registered - use %.RegisterInterfaces()',[self,GUIDToShort(aGUID),self]); result := fact.fInterfaceTypeInfo; end; class function TInterfaceFactory.Get(const aInterfaceName: RawUTF8): TInterfaceFactory; var L,i: integer; F: ^TInterfaceFactory; begin result := nil; L := length(aInterfaceName); if (InterfaceFactoryCache<>nil) and (L<>0) then begin InterfaceFactoryCache.Safe.Lock; try F := pointer(InterfaceFactoryCache.List); for i := 1 to InterfaceFactoryCache.Count do if IdemPropName(F^.fInterfaceTypeInfo^.Name,pointer(aInterfaceName),L) then begin result := F^; exit; // retrieved from cache end else inc(F); finally InterfaceFactoryCache.Safe.UnLock; end; end; end; class function TInterfaceFactory.GetUsedInterfaces: TSynObjectListLocked; begin result := InterfaceFactoryCache; end; var GlobalUnsafeSPIType: array of PTypeInfo; class procedure TInterfaceFactory.RegisterUnsafeSPIType(const Types: array of pointer); var a: integer; begin for a := 0 to high(Types) do if Types[a]<>nil then PtrArrayAddOnce(GlobalUnsafeSPIType,Types[a]) end; constructor TInterfaceFactory.Create(aInterface: PTypeInfo); var m,a,reg: integer; WR: TTextWriter; C: TClass; ErrorMsg: RawUTF8; dummy: pointer; {$ifdef HAS_FPREG} ValueIsInFPR: boolean; {$endif HAS_FPREG} {$ifdef CPUX86} offs: integer; {$else} {$ifdef Linux} // not used for Win64 fpreg: integer; {$endif Linux} {$endif CPUX86} procedure RaiseError(const Args: array of const); begin raise EInterfaceFactoryException.CreateUTF8( '%.Create: %.% [%] parameter has unexpected type %%',Args); end; begin if aInterface=nil then raise EInterfaceFactoryException.CreateUTF8('%.Create(nil)',[self]); if aInterface^.Kind<>tkInterface then raise EInterfaceFactoryException.CreateUTF8( '%.Create(%): % is not an interface',[self,aInterface^.Name,aInterface^.Name]); {$ifndef NOVARIANTS} fDocVariantOptions := JSON_OPTIONS_FAST; {$endif NOVARIANTS} fInterfaceTypeInfo := aInterface; fInterfaceIID := aInterface^.InterfaceGUID^; if IsNullGUID(fInterfaceIID) then raise EInterfaceFactoryException.CreateUTF8( '%.Create: % has no GUID',[self,aInterface^.Name]); fInterfaceName := ToUTF8(fInterfaceTypeInfo^.Name); fInterfaceURI := fInterfaceName; if fInterfaceURI[1] in ['i','I'] then delete(fInterfaceURI,1,1); // as in TServiceFactory.Create // retrieve all interface methods (recursively including ancestors) fMethod.InitSpecific(TypeInfo(TServiceMethodDynArray),fMethods,djRawUTF8, @fMethodsCount,true); AddMethodsFromTypeInfo(aInterface); // from RTTI or generated code if fMethodsCount=0 then raise EInterfaceFactoryException.CreateUTF8('%.Create(%): interface has '+ 'no RTTI - should inherit from IInvokable',[self,fInterfaceName]); if fMethodsCount>MAX_METHOD_COUNT then raise EInterfaceFactoryException.CreateUTF8( '%.Create(%): interface has too many methods (%), so breaks the '+ 'Interface Segregation Principle',[self,fInterfaceName,fMethodsCount]); fMethodIndexCurrentFrameCallback := -1; fMethodIndexCallbackReleased := -1; SetLength(fMethods,fMethodsCount); // compute additional information for each method for m := 0 to fMethodsCount-1 do with fMethods[m] do begin InterfaceDotMethodName := fInterfaceURI+'.'+URI; IsInherited := HierarchyLevel<>fAddMethodsLevel; ExecutionMethodIndex := m+RESERVED_VTABLE_SLOTS; ArgsInFirst := -1; ArgsInLast := -2; ArgsOutFirst := -1; ArgsOutLast := -2; ArgsNotResultLast := -2; ArgsOutNotResultLast := -2; ArgsResultIndex := -1; ArgsManagedFirst := -1; ArgsManagedLast := -2; Args[0].ValueType := smvSelf; for a := 1 to high(Args) do with Args[a] do begin ValueType := TypeInfoToMethodValueType(ArgTypeInfo); case ValueType of smvNone: begin case ArgTypeInfo^.Kind of tkClass: begin C := ArgTypeInfo^.ClassType^.ClassType; if C.InheritsFrom(TList) then ErrorMsg := ' - use TObjectList instead' else {$ifndef LVCL} if (C.InheritsFrom(TCollection) and not C.InheritsFrom(TInterfacedCollection)) and (JSONSerializerRegisteredCollection.Find(TCollectionClass(C))=nil) then ErrorMsg := ' - inherit from TInterfacedCollection '+ 'or use TJSONSerializer.RegisterCollectionForJSON()' else {$endif} ErrorMsg := ' - use TJSONSerializer.RegisterCustomSerializer()'; end; tkInteger: ErrorMsg := ' - use integer/cardinal instead'; tkFloat: ErrorMsg := ' - use double/currency instead'; end; RaiseError([self,aInterface^.Name,URI,ParamName^,ArgTypeInfo^.Name,ErrorMsg]); end; smvObject: if ValueDirection=smdResult then begin ErrorMsg := ' - class not allowed as function result: use a var/out parameter'; RaiseError([self,aInterface^.Name,URI,ParamName^,ArgTypeInfo^.Name,ErrorMsg]); end; smvInterface: if ValueDirection in [smdVar,smdOut,smdResult] then begin ErrorMsg := ' - interface not allowed as output: use a const parameter'; RaiseError([self,aInterface^.Name,URI,ParamName^,ArgTypeInfo^.Name,ErrorMsg]); end; end; if ValueDirection=smdResult then ArgsResultIndex := a else begin ArgsNotResultLast := a; if ValueDirection<>smdOut then begin inc(ArgsInputValuesCount); if ArgsInFirst<0 then ArgsInFirst := a; ArgsInLast := a; end; if ValueDirection<>smdConst then ArgsOutNotResultLast := a; end; if ValueDirection<>smdConst then begin if ArgsOutFirst<0 then ArgsOutFirst := a; ArgsOutLast := a; inc(ArgsOutputValuesCount); end; if ValueType in [smvObject,smvDynArray,smvRecord,smvInterface {$ifndef NOVARIANTS},smvVariant{$endif}] then begin if ArgsManagedFirst<0 then ArgsManagedFirst := a; ArgsManagedLast := a; end; if PtrArrayFind(GlobalUnsafeSPIType,ArgTypeInfo)>=0 then begin include(ValueKindAsm,vIsSPI); include(HasSPIParams,ValueDirection); end; end; if ArgsOutputValuesCount=0 then // plain procedure with no out param case ArgsInputValuesCount of 1: if Args[1].ValueType=smvBoolean then if IdemPropNameU(URI,'CurrentFrame') then fMethodIndexCurrentFrameCallback := m; 2: if (Args[1].ValueType=smvInterface) and (Args[1].ArgTypeInfo=TypeInfo(IInvokable)) and (Args[2].ValueType=smvRawUTF8) and IdemPropNameU(URI,'CallbackReleased') then fMethodIndexCallbackReleased := m; end; if ArgsResultIndex>=0 then with Args[ArgsResultIndex] do case ValueType of smvNone, smvObject, smvInterface: raise EInterfaceFactoryException.CreateUTF8('%.Create: I% unexpected result type %', [self,InterfaceDotMethodName,ArgTypeName^]); smvRecord: if ArgTypeInfo=System.TypeInfo(TServiceCustomAnswer) then begin for a := ArgsOutFirst to ArgsOutLast do if Args[a].ValueDirection in [smdVar,smdOut] then raise EInterfaceFactoryException.CreateUTF8('%.Create: I% '+ 'var/out parameter [%] not allowed with TServiceCustomAnswer result', [self,InterfaceDotMethodName,Args[a].ParamName^]); ArgsResultIsServiceCustomAnswer := true; end; end; if (ArgsInputValuesCount=1) and (Args[1].ValueType=smvRawByteString) then ArgsInputIsOctetStream := true; end; // compute asm low-level layout of the parameters for each method for m := 0 to fMethodsCount-1 do with fMethods[m] do begin // prepare stack and register layout reg := PARAMREG_FIRST; {$ifndef CPUX86} {$ifdef Linux} fpreg := FPREG_FIRST; {$endif Linux} {$endif CPUX86} for a := 0 to high(Args) do with Args[a] do begin RegisterIdent := 0; {$ifdef HAS_FPREG} FPRegisterIdent := 0; ValueIsInFPR := false; {$endif HAS_FPREG} ValueVar := ARGS_TO_VAR[ValueType]; IndexVar := ArgsUsedCount[ValueVar]; inc(ArgsUsedCount[ValueVar]); include(ArgsUsed,ValueType); if (ValueType in [smvRecord{$ifndef NOVARIANTS},smvVariant{$endif}]) or (ValueDirection in [smdVar,smdOut]) or ((ValueDirection=smdResult) and (ValueType in ARGS_RESULT_BY_REF)) then Include(ValueKindAsm,vPassedByReference); case ValueType of smvInteger, smvCardinal, smvInt64: if TJSONCustomParserRTTI.TypeNameToSimpleBinary( ShortStringToUTF8(ArgTypeName^),SizeInBinary,SizeInStorage) then begin ValueType := smvBinary; // transmitted as hexa string Include(ValueKindAsm,vIsString); end else if ArgTypeInfo^.IsQWord then Include(ValueKindAsm,vIsQword); smvDouble,smvDateTime: begin {$ifdef HAS_FPREG} ValueIsInFPR := not (vPassedByReference in ValueKindAsm); {$endif HAS_FPREG} if ValueType=smvDateTime then begin include(ValueKindAsm,vIsString); if ArgTypeInfo=System.TypeInfo(TDateTimeMS) then include(ValueKindAsm,vIsDateTimeMS); end; end; smvRawUTF8..smvWideString: Include(ValueKindAsm,vIsString); smvDynArray: begin if ObjArraySerializers.Find(ArgTypeInfo)<>nil then Include(ValueKindAsm,vIsObjArray) else if (ArgTypeInfo^.DynArraySQLFieldType in STRING_FIELDS) or DynArrayItemTypeIsSimpleBinary(ShortStringToUTF8(ArgTypeName^)) then Include(ValueKindAsm,vIsDynArrayString); DynArrayWrapper.Init(ArgTypeInfo,dummy); DynArrayWrapper.IsObjArray := vIsObjArray in ValueKindAsm; DynArrayWrapper.HasCustomJSONParser; // set DynArrayWrapper.fParser end; end; case ValueType of smvBoolean: SizeInStorage := 1; smvInteger, smvCardinal: SizeInStorage := 4; smvInt64, smvDouble, smvDateTime, smvCurrency: SizeInStorage := 8; smvEnum: SizeInStorage := ArgTypeInfo^.EnumBaseType^.SizeInStorageAsEnum; smvSet: begin SizeInStorage := ArgTypeInfo^.SetEnumType^.SizeInStorageAsSet; if SizeInStorage=0 then raise EInterfaceFactoryException.CreateUTF8( '%.Create: % set invalid SizeInStorage=% in %.% method % parameter', [self,ArgTypeName^,SizeInStorage,fInterfaceTypeInfo^.Name,URI,ParamName^]); end; smvBinary: ; // already set SizeInStorage smvRecord: if ArgTypeInfo^.RecordType^.Size<=PTRSIZ then raise EInterfaceFactoryException.CreateUTF8( '%.Create: % record too small in %.% method % parameter', [self,ArgTypeName^,fInterfaceTypeInfo^.Name,URI,ParamName^]) else SizeInStorage := PTRSIZ; // handle only records when passed by ref else SizeInStorage := PTRSIZ; end; if ValueDirection=smdResult then begin if not (ValueType in ARGS_RESULT_BY_REF) then continue; // ordinal/real/class results are returned in CPU/FPU registers {$ifndef CPUX86} InStackOffset := STACKOFFSET_NONE; RegisterIdent := PARAMREG_RESULT; continue; {$endif CPUX86} // CPUX86 will add an additional by-ref parameter end; {$ifdef CPU32} if ValueDirection=smdConst then if ValueType=smvBinary then SizeInStack := SizeInBinary else SizeInStack := ARGS_IN_STACK_SIZE[ValueType] else {$endif CPU32} SizeInStack := PTRSIZ; // always aligned to 8 bytes boundaries for 64-bit if {$ifndef CPUARM} // on ARM, ordinals>PTRSIZ can also be placed in the normal registers !! (SizeInStack<>PTRSIZ) or {$endif CPUARM} {$ifdef CPUX86} (reg>PARAMREG_LAST) // Win32, Linux x86 {$else} {$ifdef Linux} // Linux x64, arm, aarch64 ((ValueIsInFPR) and (fpreg>FPREG_LAST)) or (not ValueIsInFPR and (reg>PARAMREG_LAST)) {$else} (reg>PARAMREG_LAST) // Win64: XMMs overlap regular registers {$endif Linux} {$endif CPUX86} {$ifdef FPC}or ((ValueType in [smvRecord]) and // trunk i386/x86_64\cpupara.pas: DynArray const is passed as register not (vPassedByReference in ValueKindAsm)){$endif} then begin // this parameter will go on the stack InStackOffset := ArgsSizeInStack; inc(ArgsSizeInStack,SizeInStack); end else begin // this parameter will go in a register InStackOffset := STACKOFFSET_NONE; {$ifndef CPUX86} if (ArgsResultIndex>=0) and (reg=PARAMREG_RESULT) and (Args[ArgsResultIndex].ValueType in ARGS_RESULT_BY_REF) then begin inc(reg); // this register is reserved for method result pointer end; {$endif CPUX86} {$ifdef HAS_FPREG} if ValueIsInFPR then begin // put in a floating-point register {$ifdef Linux} FPRegisterIdent := fpreg; inc(fpreg); {$else} FPRegisterIdent := reg; // Win64 ABI: reg and fpreg do overlap inc(reg); {$endif Linux} end else {$endif HAS_FPREG} begin // put in an integer register {$ifdef CPUARM} // on 32-bit ARM, ordinals>PTRSIZ are also placed in normal registers if (SizeInStack>PTRSIZ) and ((reg and 1)=0) then inc(reg); // must be aligned on even boundary // check if we have still enough registers, after previous increments if ((PARAMREG_LAST-reg+1)*PTRSIZ)PTRSIZ then inc(reg,SizeInStack shr PTRSHR) else inc(reg); {$else} RegisterIdent := reg; inc(reg); {$endif CPUARM} end; end; end; if ArgsSizeInStack>MAX_EXECSTACK then raise EInterfaceFactoryException.CreateUTF8( '%.Create: Stack size % > % for %.% method', [self,ArgsSizeInStack,MAX_EXECSTACK,fInterfaceTypeInfo^.Name,URI]); {$ifdef CPUX86} // pascal/register convention are passed left-to-right -> reverse order offs := ArgsSizeInStack; for a := 0 to high(Args) do with Args[a] do if InStackOffset>=0 then begin dec(offs,SizeInStack); InStackOffset := offs; end; //assert(offs=0); {$endif CPUX86} end; WR := TJSONSerializer.CreateOwnedStream; try // compute the default results JSON array for all methods for m := 0 to fMethodsCount-1 do with fMethods[m] do begin WR.CancelAll; WR.Add('['); for a := ArgsOutFirst to ArgsOutLast do with Args[a] do if ValueDirection in [smdVar,smdOut,smdResult] then AddDefaultJSON(WR); WR.CancelLastComma; WR.Add(']'); WR.SetText(DefaultResult); end; // compute the service contract as a JSON array WR.CancelAll; WR.Add('['); for m := 0 to fMethodsCount-1 do with fMethods[m] do begin WR.Add('{"method":"%","arguments":[',[URI]); for a := 0 to High(Args) do Args[a].SerializeToContract(WR); WR.CancelLastComma; WR.AddShort(']},'); end; WR.CancelLastComma; WR.Add(']'); WR.SetText(fContract); {.$define SOA_DEBUG} // write the low-level interface info as json {$ifdef SOA_DEBUG} JSONReformatToFile(fContract,TFileName(fInterfaceName+ '-'+COMP_TEXT+OS_TEXT+CPU_ARCH_TEXT+'.json')); {$endif SOA_DEBUG} finally WR.Free; end; end; function TInterfaceFactory.FindMethodIndex(const aMethodName: RawUTF8): integer; begin if (self=nil) or (aMethodName='') then result := -1 else begin if fMethodsCount<10 then begin for result := 0 to fMethodsCount-1 do if IdemPropNameU(fMethods[result].URI,aMethodName) then exit; result := -1; end else result := fMethod.FindHashed(aMethodName); if (result<0) and (aMethodName[1]<>'_') then result := FindMethodIndex('_'+aMethodName); end; end; function TInterfaceFactory.FindMethod(const aMethodName: RawUTF8): PServiceMethod; var i: integer; begin i := FindMethodIndex(aMethodName); if i < 0 then result := nil else result := @fMethods[i]; end; function TInterfaceFactory.FindFullMethodIndex(const aFullMethodName: RawUTF8; alsoSearchExactMethodName: boolean): integer; begin if PosExChar('.',aFullMethodName)<>0 then for result := 0 to fMethodsCount-1 do if IdemPropNameU(fMethods[result].InterfaceDotMethodName,aFullMethodName) then exit; if alsoSearchExactMethodName then result := FindMethodIndex(aFullMethodName) else result := -1; end; function TInterfaceFactory.CheckMethodIndex(const aMethodName: RawUTF8): integer; begin if self=nil then raise EInterfaceFactoryException.Create('TInterfaceFactory(nil).CheckMethodIndex'); result := FindMethodIndex(aMethodName); if result<0 then raise EInterfaceFactoryException.CreateUTF8( '%.CheckMethodIndex: %.% not found',[self,fInterfaceTypeInfo^.Name,aMethodName]); end; function TInterfaceFactory.CheckMethodIndex(aMethodName: PUTF8Char): integer; begin result := CheckMethodIndex(RawUTF8(aMethodName)); end; procedure TInterfaceFactory.CheckMethodIndexes(const aMethodName: array of RawUTF8; aSetAllIfNone: boolean; out aBits: TInterfaceFactoryMethodBits); var i: integer; begin if aSetAllIfNone and (high(aMethodName)<0) then begin FillCharFast(aBits,SizeOf(aBits),255); exit; end; FillCharFast(aBits,SizeOf(aBits),0); for i := 0 to high(aMethodName) do include(aBits,CheckMethodIndex(aMethodName[i])); end; function TInterfaceFactory.GetMethodName(MethodIndex: integer): RawUTF8; begin if (MethodIndex<0) or (self=nil) then result := '' else if MethodIndex0 then start := StubCallAllocMemLastStart else begin start := stub-STUB_INTERV; if start>stub then start := 0; // avoid range overflow start := start and STUB_ALIGN; end; stop := stub+STUB_INTERV; if stopMAP_FAILED then begin // close enough for a 24/32-bit relative jump? dist := abs(stub-PtrUInt(result)); if distMAP_FAILED then MemoryProtection := True; end; {$endif KYLIX3} if fStub=MAP_FAILED then {$endif MSWINDOWS} raise EServiceException.CreateUTF8('%.Create: OS memory allocation failed',[Self]); end; destructor TFakeStubBuffer.Destroy; begin {$ifdef MSWINDOWS} VirtualFree(fStub,0,MEM_RELEASE); {$else} {$ifdef KYLIX3} munmap(fStub,STUB_SIZE); {$else} fpmunmap(fStub,STUB_SIZE); {$endif} {$endif} inherited; end; class function TFakeStubBuffer.Reserve(size: Cardinal): pointer; begin if size>STUB_SIZE then raise EServiceException.CreateUTF8('%.Reserve(size=%>%)',[self,size,STUB_SIZE]); if CurrentFakeStubBuffer=nil then GarbageCollectorFreeAndNil(CurrentFakeStubBuffer,TFakeStubBuffer.Create) else if CurrentFakeStubBuffer.fStubUsed+size>STUB_SIZE then begin GarbageCollector.Add(CurrentFakeStubBuffer); CurrentFakeStubBuffer := TFakeStubBuffer.Create; end; with CurrentFakeStubBuffer do begin result := @fStub[fStubUsed]; inc(fStubUsed,size); end; end; function TInterfaceFactory.GetMethodsVirtualTable: pointer; var i, tmp: cardinal; P: PCardinal; {$ifdef UNIX} PageAlignedFakeStub: pointer; {$endif UNIX} {$ifdef CPUAARCH64}stub: PtrUInt;{$endif} begin if fFakeVTable=nil then begin InterfaceFactoryCache.Safe.Lock; try if fFakeVTable=nil then begin // avoid race condition error SetLength(fFakeVTable,fMethodsCount+RESERVED_VTABLE_SLOTS); fFakeVTable[0] := @TInterfacedObjectFake.FakeQueryInterface; fFakeVTable[1] := @TInterfacedObjectFake.Fake_AddRef; fFakeVTable[2] := @TInterfacedObjectFake.Fake_Release; if fMethodsCount=0 then begin result := pointer(fFakeVTable); exit; end; tmp := {$ifdef CPUX86}fMethodsCount*24{$endif} {$ifdef CPUX64}fMethodsCount*16{$endif} {$ifdef CPUARM}fMethodsCount*12{$endif} {$ifdef CPUAARCH64}($120 shr 2)+fMethodsCount*28{$endif}; fFakeStub := TFakeStubBuffer.Reserve(tmp); P := pointer(fFakeStub); {$ifdef CPUAARCH64} PtrUInt(P) := PtrUInt(P)+$120; {$endif}; {$ifdef UNIX} if MemoryProtection then begin // Disable execution permission of memory to be able to write into memory PageAlignedFakeStub := Pointer((PtrUInt(P) DIV SystemInfo.dwPageSize) * SystemInfo.dwPageSize); if SynMProtect(PageAlignedFakeStub , (SystemInfo.dwPageSize shl 1), PROT_READ or PROT_WRITE)<0 then raise EServiceException.CreateUTF8('%.Create: SynMProtect write failure.',[self]); end; {$endif UNIX} for i := 0 to fMethodsCount-1 do begin fFakeVTable[i+RESERVED_VTABLE_SLOTS] := P; {$ifdef CPUX64} PWord(P)^ := $b848; inc(PWord(P)); // mov rax,offset x64FakeStub PPtrUInt(P)^ := PtrUInt(@x64FakeStub); inc(PPtrUInt(P)); PByte(P)^ := $50; inc(PByte(P)); // push rax P^ := $b866+(i shl 16); inc(P); // mov (r)ax,{MethodIndex} PByte(P)^ := $c3; inc(PByte(P)); // ret {$endif CPUX64} {$ifdef CPUARM} {$ifdef ASMORIG} P^ := ($e3a040 shl 8)+i; inc(P); // mov r4 (v1),{MethodIndex} : store method index in register {$else} P^ := ($e3a0c0 shl 8)+i; inc(P); // mov r12 (ip),{MethodIndex} : store method index in register {$endif} tmp := ((PtrUInt(@TInterfacedObjectFake.ArmFakeStub)-PtrUInt(P)) shr 2)-2; P^ := ($ea shl 24) + (tmp and $00ffffff); // branch ArmFakeStub (24bit relative, word aligned) inc(P); P^ := $e320f000; inc(P); {$endif CPUARM} {$ifdef CPUAARCH64} // store method index in register r16 [IP0] // $10 = r16 ... loop to $1F -> number shifted * $20 P^ := ($d280 shl 16)+(i shl 5)+$10; inc(P); // mov r16 ,{MethodIndex} // we are using a register branch here // fill register x10 with address stub := PtrUInt(@TInterfacedObjectFake.AArch64FakeStub); tmp := (stub shr 0) and $ffff; P^ := ($d280 shl 16)+(tmp shl 5)+$0a; inc(P); tmp := (stub shr 16) and $ffff; P^ := ($f2a0 shl 16)+(tmp shl 5)+$0a; inc(P); tmp := (stub shr 32) and $ffff; P^ := ($f2c0 shl 16)+(tmp shl 5)+$0a; inc(P); tmp := (stub shr 48) and $ffff; P^ := ($f2e0 shl 16)+(tmp shl 5)+$0a; inc(P); // branch to address in x10 register P^ := $d61f0140; inc(P); P^ := $d503201f; inc(P); {$endif CPUAARCH64} {$ifdef CPUX86} P^ := $68ec8b55; inc(P); // push ebp; mov ebp,esp P^ := i; inc(P); // push {MethodIndex} P^ := $e2895251; inc(P); // push ecx; push edx; mov edx,esp PByte(P)^ := $e8; inc(PByte(P)); // call FakeCall P^ := PtrUInt(@TInterfacedObjectFake.FakeCall)-PtrUInt(P)-4; inc(P); P^ := $c25dec89; inc(P); // mov esp,ebp; pop ebp {$ifdef DARWIN} P^ := $900000; // ret; nop {$else} P^ := fMethods[i].ArgsSizeInStack or $900000; // ret {StackSize}; nop {$endif DARWIN} inc(PByte(P),3); {$endif CPUX86} end; {$ifdef UNIX} if MemoryProtection then // Enable execution permission of memory if SynMProtect(PageAlignedFakeStub , (SystemInfo.dwPageSize shl 1), PROT_READ OR PROT_EXEC)<0 then raise EServiceException.CreateUTF8('%.Create: SynMProtect exec failure.',[self]); {$endif UNIX} end; finally InterfaceFactoryCache.Safe.UnLock; end; end; result := pointer(fFakeVTable); end; {$ifdef HASINTERFACERTTI} // see http://bugs.freepascal.org/view.php?id=26774 { TInterfaceFactoryRTTI } type TMethodKind = (mkProcedure, mkFunction, mkConstructor, mkDestructor, mkClassProcedure, mkClassFunction, mkClassConstructor, mkClassDestructor, mkOperatorOverload{$ifndef FPC},{ Obsolete } mkSafeProcedure, mkSafeFunction{$endif}); {$ifndef FPC} TIntfMethodEntryTail = {$ifndef FPC_REQUIRES_PROPER_ALIGNMENT}packed{$endif} record {$ifdef FPC_NEWRTTI} ResultType: PPTypeInfo; CC: TCallingConvention; Kind: TMethodKind; ParamCount: Word; StackSize: SizeInt; Name: PShortString; {$else} Kind: TMethodKind; CC: TCallingConvention; ParamCount: Byte; {Params: array[0..ParamCount - 1] of TVmtMethodParam;} {$endif FPC_NEWRTTI} end; {$endif FPC} procedure TInterfaceFactoryRTTI.AddMethodsFromTypeInfo(aInterface: PTypeInfo); const {$ifdef FPC} {$if defined(CPUI386) or defined(CPUI8086) or defined(CPUX86_64) or defined(CPUM68K)} DEFCC = ccRegister; {$else} DEFCC = ccStdCall; {$ifend} {$else} DEFCC = ccRegister; {$endif FPC} var P: Pointer; {$ifdef FPC} PI: PFPCInterfaceData; // low-level types redirected from SynFPCTypInfo VMP: PFPCVmtMethodParam; methtable: PFPCIntfMethodTable; PME: PFPCIntfMethodEntry; PW: PWord; aResultType: PTypeInfo; argsindex: word; {$else} PI: PInterfaceTypeData absolute P; PB: PByte absolute P; PS: PShortString absolute P; PME: ^TIntfMethodEntryTail absolute P; PF: ^TParamFlags absolute P; PP: ^PPTypeInfo absolute P; PW: PWord absolute P; {$endif FPC} Ancestor: PTypeInfo; Kind: TMethodKind; f: TParamFlags; m: integer; paramcounter: word; sm: PServiceMethod; n,na: cardinal; aURI: RawUTF8; procedure RaiseError(const Format: RawUTF8; const Args: array of const); begin raise EInterfaceFactoryException.CreateUTF8('%.AddMethodsFromTypeInfo(%.%) failed - %', [self,fInterfaceName,aURI,FormatUTF8(Format,Args)]); end; begin // handle interface inheritance via recursive calls P := GetTypeDataClean(aInterface^); {$ifdef FPC} PI := P; if PI^.Parent<>nil then Ancestor := Deref(pointer(PI^.Parent)) else {$else} if PI^.IntfParent<>nil then Ancestor := Deref(PI^.IntfParent) else {$endif FPC} Ancestor := nil; if Ancestor<>nil then begin AddMethodsFromTypeInfo(Ancestor); inc(fAddMethodsLevel); end; {$ifdef FPC} if PI^.UnitName='System' then exit; methtable := PI^.MethodTable; n := methtable^.Count; PW := @methtable^.RttiCount; if (PW^=$ffff) or (n=0) then exit; // no RTTI or no method at this level of interface {$else} P := @PI^.IntfUnit[ord(PI^.IntfUnit[0])+1]; n := PW^; inc(PW); if (PW^=$ffff) or (n=0) then exit; // no RTTI or no method at this level of interface inc(PW); {$endif FPC} for m := 0 to n-1 do begin // retrieve method name, and add to the methods list (with hashing) {$ifdef FPC} PME := methtable^.Method[m]; ShortStringToAnsi7String(PME^.Name,aURI); {$else} ShortStringToAnsi7String(PS^,aURI); {$endif FPC} sm := fMethod.AddUniqueName(aURI,'%.% method: duplicated name for %', [fInterfaceTypeInfo^.Name,aURI,self]); sm^.HierarchyLevel := fAddMethodsLevel; {$ifndef FPC} PS := @PS^[ord(PS^[0])+1]; // skip method name in Delphi {$endif FPC} Kind := TMethodKind(PME^.Kind); if TCallingConvention(PME^.CC)<>DEFCC then RaiseError('unhandled calling convention %',[SynCommons.GetEnumName( TypeInfo(TCallingConvention),ord(PME^.CC))^]); // retrieve method call arguments from RTTI n := PME^.ParamCount; {$ifndef FPC} inc(PME); // PF now points to parameter flags for Delphi {$endif FPC} na := n; if Kind=mkFunction then inc(na); // function result is an additional output parameter if na>MAX_METHOD_ARGS then RaiseError('method has too many parameters: %>%',[na,MAX_METHOD_ARGS]); SetLength(sm^.Args,na); {$ifdef FPC} aResultType := Deref(pointer(PME^.ResultType)); if aResultType<>nil then with sm^.Args[n] do begin ParamName := @PSEUDO_RESULT_NAME; ValueDirection := smdResult; ArgTypeInfo := aResultType; if ArgTypeInfo=TypeInfo(Integer) then // under FPC integer->'longint' ArgTypeName := @INTEGER_NAME else ArgTypeName := @ArgTypeInfo^.Name; end; argsindex := 0; {$endif FPC} paramcounter := 0; while paramcountersmdConst then sm^.ArgsOutNotResultLast := argsindex; ArgTypeInfo := pointer(Deref(pointer(VMP^.ParamType))); ArgTypeName := @ArgTypeInfo^.Name; if paramcounter>0 then case TypeInfoToMethodValueType(ArgTypeInfo) of smvRecord,smvDynArray: if f*[pfConst,pfVar,pfOut{$IFDEF FPC_HAS_CONSTREF},pfConstRef{$endif}]=[] then RaiseError('%: % parameter should be declared as const, var or out', [ParamName^,ArgTypeName^]); smvInterface: if not(pfConst in f) then RaiseError('%: % parameter should be declared as const', [ParamName^,ArgTypeName^]); end; if pfSelf in f then ParamName := @PSEUDO_SELF_NAME else if pfResult in f then begin if (paramcounter<>n-1) or (High(sm^.Args)<>paramcounter+1) then begin // at least on ARM, function (result) is on paramcounter different position than on x86 // so, cleanup and re-use array for next param entry FillCharFast(sm^.Args[argsindex],SizeOf(TServiceMethodArgument),0); // needed for re-use due to following Inc(argsindex) ... see below dec(argsindex); end; sm^.Args[n-1] := sm^.Args[n]; SetLength(sm^.Args,n); end else { since https://svn.freepascal.org/cgi-bin/viewvc.cgi?view=revision&revision=39684 TVmtMethodParam.Name is a local stack copy -> direct NamePtr use } ParamName := {$ifdef VER3_1}@VMP^.Name{$else}VMP^.NamePtr{$endif}; end; inc(paramcounter); inc(argsindex); end; {$else FPC} // Delphi code: with sm^.Args[paramcounter] do begin f := PF^; inc(PF); if pfVar in f then ValueDirection := smdVar else if pfOut in f then ValueDirection := smdOut; sm^.ArgsNotResultLast := paramcounter; if ValueDirection<>smdConst then sm^.ArgsOutNotResultLast := paramcounter; ParamName := PS; PS := @PS^[ord(PS^[0])+1]; SetFromRTTI(PB); {$ifdef ISDELPHIXE} inc(PB,PW^); // skip custom attributes {$endif ISDELPHIXE} if paramcounter>0 then case TypeInfoToMethodValueType(ArgTypeInfo) of smvRecord,smvDynArray: if f*[pfConst,pfVar,pfOut]=[] then RaiseError('%: % parameter should be declared as const, var or out', [ParamName^,ArgTypeName^]); smvInterface: if not(pfConst in f) then RaiseError('%: % parameter should be declared as const', [ParamName^,ArgTypeName^]); end; inc(paramcounter); end; // add a pseudo argument after all arguments for functions if Kind=mkFunction then with sm^.Args[n] do begin ParamName := @PSEUDO_RESULT_NAME; ValueDirection := smdResult; SetFromRTTI(PB); end; {$ifdef ISDELPHIXE} inc(PB,PW^); // skip custom attributes {$endif ISDELPHIXE} {$endif FPC} end; // go to next method end; {$endif HASINTERFACERTTI} // see http://bugs.freepascal.org/view.php?id=26774 { TInterfaceFactoryGenerated } procedure TInterfaceFactoryGenerated.AddMethod( const aName: RawUTF8; const aParams: array of const); const ARGPERARG = 3; // [ 0,'n1',TypeInfo(Integer), ... ] var meth: PServiceMethod; arg: ^TServiceMethodArgument; na,ns,a: integer; u: RawUTF8; begin if Length(aParams) mod ARGPERARG<>0 then raise EInterfaceFactoryException.CreateUTF8( '%: invalid aParams count for %.AddMethod("%")',[fInterfaceName,self,aName]); meth := fMethod.AddUniqueName(aName,'%.% method: duplicated generated name for %', [fInterfaceName,aName,self]); na := length(aParams) div ARGPERARG; SetLength(meth^.Args,na+1); // leave Args[0]=self with meth^.Args[0] do begin ParamName := @PSEUDO_SELF_NAME; ArgTypeInfo := fInterfaceTypeInfo; ArgTypeName := @ArgTypeInfo^.Name; end; ns := length(fTempStrings); SetLength(fTempStrings,ns+na); for a := 0 to na-1 do begin arg := @meth^.Args[a+1]; if aParams[a*ARGPERARG].VType<>vtInteger then raise EInterfaceFactoryException.CreateUTF8('%: invalid param type #% for %.AddMethod("%")', [fInterfaceTypeInfo^.Name,a,self,aName]); arg^.ValueDirection := TServiceMethodValueDirection(aParams[a*ARGPERARG].VInteger); VarRecToUTF8(aParams[a*ARGPERARG+1],u); if u='' then raise EInterfaceFactoryException.CreateUTF8('%: invalid param name #% for %.AddMethod("%")', [fInterfaceTypeInfo^.Name,a,self,aName]); insert(AnsiChar(Length(u)),u,1); // create fake PShortString arg^.ParamName := pointer(u); fTempStrings[ns+a] := u; if aParams[a*ARGPERARG+2].VType<>vtPointer then raise EInterfaceFactoryException.CreateUTF8('%: expect TypeInfo() at #% for %.AddMethod("%")', [fInterfaceTypeInfo^.Name,a,self,aName]); arg^.ArgTypeInfo := aParams[a*ARGPERARG+2].VPointer; {$ifdef FPC} // under FPC, TypeInfo(Integer/Cardinal)=TypeInfo(LongInt/LongWord) if arg^.ArgTypeInfo=TypeInfo(Integer) then arg^.ArgTypeName := @INTEGER_NAME else if arg^.ArgTypeInfo=TypeInfo(Cardinal) then arg^.ArgTypeName := @CARDINAL_NAME else {$endif FPC} arg^.ArgTypeName := @arg^.ArgTypeInfo^.Name; end; end; class procedure TInterfaceFactoryGenerated.RegisterInterface(aInterface: PTypeInfo); var i: integer; begin if (aInterface=nil) or (self=TInterfaceFactoryGenerated) then raise EInterfaceFactoryException.CreateUTF8('%.RegisterInterface(nil)',[self]); EnterInterfaceFactoryCache; try for i := 0 to InterfaceFactoryCache.Count-1 do if TInterfaceFactory(InterfaceFactoryCache.List[i]).fInterfaceTypeInfo=aInterface then raise EInterfaceFactoryException.CreateUTF8('Duplicated %.RegisterInterface(%)', [self,aInterface^.Name]); InterfaceFactoryCache.Add(Create(aInterface)); finally InterfaceFactoryCache.Safe.UnLock; end; end; { TInterfacedObjectFakeCallback } type TInterfacedObjectFakeCallback = class(TInterfacedObjectFake) protected fRest: TSQLRest; fName: RawUTF8; function FakeInvoke(const aMethod: TServiceMethod; const aParams: RawUTF8; aResult, aErrorMsg: PRawUTF8; aClientDrivenID: PCardinal; aServiceCustomAnswer: PServiceCustomAnswer): boolean; virtual; end; function TInterfacedObjectFakeCallback.FakeInvoke(const aMethod: TServiceMethod; const aParams: RawUTF8; aResult, aErrorMsg: PRawUTF8; aClientDrivenID: PCardinal; aServiceCustomAnswer: PServiceCustomAnswer): boolean; begin fRest.InternalLog('%.FakeInvoke %(%)',[ClassType,aMethod.InterfaceDotMethodName,aParams]); if aMethod.ArgsOutputValuesCount>0 then begin if aErrorMsg<>nil then FormatUTF8('%.FakeInvoke [%]: % has out parameters', [self,fName,aMethod.InterfaceDotMethodName], aErrorMsg^); result := false; end else result := true; end; { TInterfacedObjectAsynch } type TInterfacedObjectAsynch = class(TInterfacedObjectFakeCallback) protected fTimer: TSQLRestBackgroundTimer; fDest: IInvokable; fOnResult: TOnAsynchRedirectResult; function FakeInvoke(const aMethod: TServiceMethod; const aParams: RawUTF8; aResult, aErrorMsg: PRawUTF8; aClientDrivenID: PCardinal; aServiceCustomAnswer: PServiceCustomAnswer): boolean; override; public constructor Create(aTimer: TSQLRestBackgroundTimer; aFactory: TInterfaceFactory; const aDestinationInterface: IInvokable; out aCallbackInterface; const aOnResult: TOnAsynchRedirectResult); end; TInterfacedObjectAsynchCall = packed record Method: PServiceMethod; Instance: pointer; // weak IInvokable reference Params: RawUTF8; OnOutputParamsCopy: RawUTF8; OnOutput: TOnAsynchRedirectResult; end; constructor TInterfacedObjectAsynch.Create(aTimer: TSQLRestBackgroundTimer; aFactory: TInterfaceFactory; const aDestinationInterface: IInvokable; out aCallbackInterface; const aOnResult: TOnAsynchRedirectResult); begin fTimer := aTimer; fRest := fTimer.fRest; fName := fTimer.fThreadName; fDest := aDestinationInterface; fOnResult := aOnResult; inherited Create(aFactory,nil,[ifoJsonAsExtended,ifoDontStoreVoidJSON],FakeInvoke,nil); Get(aCallbackInterface); end; function TInterfacedObjectAsynch.FakeInvoke(const aMethod: TServiceMethod; const aParams: RawUTF8; aResult, aErrorMsg: PRawUTF8; aClientDrivenID: PCardinal; aServiceCustomAnswer: PServiceCustomAnswer): boolean; var msg: RawUTF8; call: TInterfacedObjectAsynchCall; begin result := inherited FakeInvoke(aMethod,aParams,aResult,aErrorMsg, aClientDrivenID,aServiceCustomAnswer); if not result then exit; call.Method := @aMethod; call.Instance := pointer(fDest); call.Params := aParams; if Assigned(fOnResult) then begin FastSetString(call.OnOutputParamsCopy,pointer(aParams),length(aParams)); call.OnOutput := fOnResult; end else call.OnOutput := nil; msg := RecordSave(call,TypeInfo(TInterfacedObjectAsynchCall)); result := fTimer.EnQueue(fTimer.AsynchBackgroundExecute,msg,true); end; { TSQLRestBackgroundTimer } constructor TSQLRestBackgroundTimer.Create(aRest: TSQLRest; const aThreadName: RawUTF8; aStats: TSynMonitorClass); var aName: RawUTF8; begin if aRest=nil then raise EORMException.CreateUTF8('%.Create(aRest=nil,"%")',[self,aThreadName]); fRest := aRest; if aThreadName<>'' then aName := aThreadName else FormatUTF8('% %',[fRest.Model.Root, ClassType],aName); inherited Create(aName,fRest.BeginCurrentThread,fRest.EndCurrentThread,aStats); end; destructor TSQLRestBackgroundTimer.Destroy; begin AsynchBatchStop(nil); inherited Destroy; end; function TSQLRestBackgroundTimer.AsynchBatchIndex(aTable: TSQLRecordClass): integer; begin if (self=nil) or (fBackgroundBatch=nil) then result := -1 else begin result := fRest.Model.GetTableIndexExisting(aTable); if (result>=length(fBackgroundBatch)) or (fBackgroundBatch[result]=nil) then result := -1; end; end; function TSQLRestBackgroundTimer.AsynchBatchLocked(aTable: TSQLRecordClass; out aBatch: TSQLRestBatchLocked): boolean; var b: integer; begin b := AsynchBatchIndex(aTable); if b>=0 then begin aBatch := fBackgroundBatch[b]; aBatch.Safe.Lock; result := true; end else result := false; end; procedure TSQLRestBackgroundTimer.AsynchBatchUnLock(aBatch: TSQLRestBatchLocked); begin try if aBatch.Count>=aBatch.Threshold then ExecuteNow(AsynchBatchExecute); finally aBatch.Safe.UnLock; end; end; procedure TSQLRestBackgroundTimer.AsynchBatchExecute(Sender: TSynBackgroundTimer; Event: TWaitResult; const Msg: RawUTF8); var data, tablename: RawUTF8; table: TSQLRecordClass; batch: TSQLRestBatchLocked; b, count, status: integer; res: TIDDynArray; {$ifdef WITHLOG} log: ISynLog; // for Enter auto-leave to work with FPC {$endif} begin try // send any pending data for b := 0 to high(fBackgroundBatch) do begin batch := fBackgroundBatch[b]; if batch.Count=0 then continue; batch.Safe.Lock; try table := batch.Table; count := batch.Count; if count>0 then try {$ifdef WITHLOG} if log=nil then log := fRest.LogClass.Enter('AsynchBatchExecute % count=%', [table,count],self); {$endif} batch.PrepareForSending(data); finally batch.Reset; end; finally batch.Safe.UnLock; end; // inlined TSQLRest.BatchSend for lower contention if data<>'' then try status := fRest.EngineBatchSend(table,data,res,count); // may take a while fRest.InternalLog('AsynchBatchExecute % EngineBatchSend=%',[table,status]); except on E: Exception do fRest.InternalLog('% during AsynchBatchExecute %',[E.ClassType,table],sllWarning); end; end; finally if IdemPChar(pointer(Msg),'FREE@') then begin // from AsynchBatchStop() fRest.InternalLog('AsynchBatchExecute %',[Msg]); tablename := copy(Msg,6,127); if tablename='' then // AsynchBatchStop(nil) ObjArrayClear(fBackgroundBatch,true) else begin // AsynchBatchStop(table) b := fRest.Model.GetTableIndex(tablename); if bnil) and (fBackgroundBatch[b]<>nil) then exit; // already defined for this Table fRest.InternalLog('AsynchBatchStart(%,%,%)',[Table,SendSeconds,PendingRowThreshold],sllDebug); Enable(AsynchBatchExecute,SendSeconds); if fBackgroundBatch=nil then SetLength(fBackgroundBatch,fRest.Model.TablesMax+1); fBackgroundBatch[b] := TSQLRestBatchLocked.Create( fRest,Table,AutomaticTransactionPerRow,Options); fBackgroundBatch[b].Threshold := PendingRowThreshold; result := true; end; function TSQLRestBackgroundTimer.AsynchBatchStop(Table: TSQLRecordClass): boolean; var b: integer; timeout: Int64; {$ifdef WITHLOG} log: ISynLog; // for Enter auto-leave to work with FPC {$endif} begin result := false; if (self=nil) or (fBackgroundBatch=nil) then exit; {$ifdef WITHLOG} log := fRest.LogClass.Enter('AsynchBatchStop(%)',[Table],self); {$endif} timeout := SynCommons.GetTickCount64+5000; if Table=nil then begin // e.g. from TSQLRest.Destroy if not EnQueue(AsynchBatchExecute,'free@',true) then exit; repeat SleepHiRes(1); // wait for all batchs to be released until (fBackgroundBatch=nil) or (SynCommons.GetTickCount64>timeout); result := Disable(AsynchBatchExecute); end else begin b := AsynchBatchIndex(Table); if (b<0) or not EnQueue(AsynchBatchExecute,'free@'+Table.SQLTableName,true) then exit; repeat SleepHiRes(1); // wait for all pending rows to be sent until (fBackgroundBatch[b]=nil) or (SynCommons.GetTickCount64>timeout); if ObjArrayCount(fBackgroundBatch)>0 then result := true else begin result := Disable(AsynchBatchExecute); if result then ObjArrayClear(fBackgroundBatch,true); end; end; end; function TSQLRestBackgroundTimer.AsynchBatchAdd(Value: TSQLRecord; SendData,ForceID: boolean; const CustomFields: TSQLFieldBits; DoNotAutoComputeFields: boolean): integer; var b: TSQLRestBatchLocked; begin result := -1; if (self=nil) or (fBackgroundBatch=nil) or (Value=nil) then exit; fRest.InternalLog('AsynchBatchAdd %',[Value],sllDebug); if AsynchBatchLocked(Value.RecordClass,b) then try result := b.Add(Value,SendData,ForceID,CustomFields,DoNotAutoComputeFields); finally AsynchBatchUnLock(b); end; end; function TSQLRestBackgroundTimer.AsynchBatchRawAdd(Table: TSQLRecordClass; const SentData: RawUTF8): integer; var b: TSQLRestBatchLocked; begin result := -1; if (self=nil) or (fBackgroundBatch=nil) or (Table=nil) then exit; fRest.InternalLog('AsynchBatchRawAdd % %',[Table,SentData],sllDebug); if AsynchBatchLocked(Table,b) then try result := b.RawAdd(SentData); finally AsynchBatchUnLock(b); end; end; procedure TSQLRestBackgroundTimer.AsynchBatchRawAppend(Table: TSQLRecordClass; SentData: TTextWriter); var b: TSQLRestBatchLocked; begin if (self=nil) or (fBackgroundBatch=nil) or (Table=nil) or (SentData=nil) then exit; fRest.InternalLog('AsynchBatchRawAppend %',[Table],sllDebug); if AsynchBatchLocked(Table,b) then try b.RawAppend.AddNoJSONEscape(SentData); finally AsynchBatchUnLock(b); end; end; function TSQLRestBackgroundTimer.AsynchBatchUpdate(Value: TSQLRecord; const CustomFields: TSQLFieldBits; DoNotAutoComputeFields: boolean): integer; var b: TSQLRestBatchLocked; begin result := -1; if (self=nil) or (fBackgroundBatch=nil) or (Value=nil) then exit; fRest.InternalLog('AsynchBatchUpdate %',[Value],sllDebug); if AsynchBatchLocked(Value.RecordClass,b) then try result := b.Update(Value,CustomFields,DoNotAutoComputeFields); finally AsynchBatchUnLock(b); end; end; function TSQLRestBackgroundTimer.AsynchBatchDelete(Table: TSQLRecordClass; ID: TID): integer; var b: TSQLRestBatchLocked; begin result := -1; if (self=nil) or (fBackgroundBatch=nil) then exit; fRest.InternalLog('AsynchBatchDelete % %',[Table,ID],sllDebug); if AsynchBatchLocked(Table,b) then try result := b.Delete(Table,ID); finally AsynchBatchUnLock(b); end; end; procedure TSQLRestBackgroundTimer.AsynchBackgroundExecute(Sender: TSynBackgroundTimer; Event: TWaitResult; const Msg: RawUTF8); var exec: TServiceMethodExecute; call: TInterfacedObjectAsynchCall; o: PRawUTF8; output: RawUTF8; {$ifdef WITHLOG} log: ISynLog; // for Enter auto-leave to work with FPC {$endif} begin if not RecordLoad(call,Msg,TypeInfo(TInterfacedObjectAsynchCall)) then exit; // invalid message (e.g. periodic execution) {$ifdef WITHLOG} log := fRest.LogClass.Enter('AsynchBackgroundExecute % %', [call.Method^.InterfaceDotMethodName,call.Params],self); {$endif} exec := TServiceMethodExecute.Create(call.Method); try if Assigned(call.OnOutput) then o := @output else o := nil; if not exec.ExecuteJsonCallback(call.Instance,call.Params, o) then fRest.InternalLog('%.AsynchBackgroundExecute %: ExecuteJsonCallback failed', [ClassType,call.Method^.InterfaceDotMethodName],sllWarning) else if o<>nil then call.OnOutput(call.Method^,IInvokable(call.Instance), call.OnOutputParamsCopy,output); finally exec.Free; end; end; procedure TSQLRestBackgroundTimer.AsynchRedirect(const aGUID: TGUID; const aDestinationInterface: IInvokable; out aCallbackInterface; const aOnResult: TOnAsynchRedirectResult); var factory: TInterfaceFactory; begin factory := TInterfaceFactory.Get(aGUID); if factory=nil then raise EInterfaceFactoryException.CreateUTF8('%.AsynchRedirect: unknown %', [self,GUIDToShort(aGUID)]); if aDestinationInterface=nil then raise EInterfaceFactoryException.CreateUTF8('%.AsynchRedirect(nil)',[self]); fRest.InternalLog('AsynchRedirect % to % using %',[factory.InterfaceName, ObjectFromInterface(aDestinationInterface),self]); Enable(AsynchBackgroundExecute,3600); TInterfacedObjectAsynch.Create(self,factory,aDestinationInterface,aCallbackInterface,aOnResult); end; procedure TSQLRestBackgroundTimer.AsynchRedirect(const aGUID: TGUID; const aDestinationInstance: TInterfacedObject; out aCallbackInterface; const aOnResult: TOnAsynchRedirectResult); var dest: IInvokable; begin if aDestinationInstance=nil then raise EInterfaceFactoryException.CreateUTF8('%.AsynchRedirect(nil)',[self]); if not aDestinationInstance.GetInterface(aGUID,dest) then raise EInterfaceFactoryException.CreateUTF8('%.AsynchRedirect [%]: % is not a %', [self,fThreadName,aDestinationInstance,GUIDToShort(aGUID)]); AsynchRedirect(aGUID,dest,aCallbackInterface,aOnResult); end; procedure TSQLRestBackgroundTimer.AsynchBackgroundInterning( Sender: TSynBackgroundTimer; Event: TWaitResult; const Msg: RawUTF8); var i, claimed, total: integer; timer: TPrecisionTimer; begin timer.Start; claimed := 0; for i := 0 to high(fBackgroundInterning) do inc(claimed,fBackgroundInterning[i].Clean(fBackgroundInterningMaxRefCount)); if claimed=0 then exit; // nothing to collect total := claimed; for i := 0 to high(fBackgroundInterning) do inc(total,fBackgroundInterning[i].Count); fRest.InternalLog('%.AsynchInterning: Clean(%) claimed %/% strings from % pools in %', [ClassType,fBackgroundInterningMaxRefCount,claimed,total, length(fBackgroundInterning),timer.Stop],sllDebug); end; procedure TSQLRestBackgroundTimer.AsynchInterning(Interning: TRawUTF8Interning; InterningMaxRefCount, PeriodMinutes: integer); begin if (self=nil) or (Interning=nil) then exit; fTaskLock.Lock; try if (InterningMaxRefCount<=0) or (PeriodMinutes<=0) then ObjArrayDelete(fBackgroundInterning,Interning) else begin fBackgroundInterningMaxRefCount := InterningMaxRefCount; ObjArrayAddOnce(fBackgroundInterning,Interning); Enable(AsynchBackgroundInterning,PeriodMinutes*60); end; finally fTaskLock.UnLock; end; end; { TInterfacedObjectMulti / TInterfacedObjectMultiList } type TInterfacedObjectMultiDest = record instance: IInvokable; methods: TInterfaceFactoryMethodBits; end; TInterfacedObjectMultiDestDynArray = array of TInterfacedObjectMultiDest; TInterfacedObjectMulti = class; TInterfacedObjectMultiList = class(TInterfacedObjectLocked, IMultiCallbackRedirect) protected fDest: TInterfacedObjectMultiDestDynArray; fDestCount: integer; fDests: TDynArray; fFakeCallback: TInterfacedObjectMulti; procedure Redirect(const aCallback: IInvokable; const aMethodsNames: array of RawUTF8; aSubscribe: boolean); overload; procedure Redirect(const aCallback: TInterfacedObject; const aMethodsNames: array of RawUTF8; aSubscribe: boolean); overload; procedure CallBackUnRegister; function GetInstances(aMethod: integer; var aInstances: TPointerDynArray): integer; constructor Create; override; destructor Destroy; override; end; TInterfacedObjectMulti = class(TInterfacedObjectFakeCallback) protected fList: TInterfacedObjectMultiList; fCallBackUnRegisterNeeded: boolean; function FakeInvoke(const aMethod: TServiceMethod; const aParams: RawUTF8; aResult, aErrorMsg: PRawUTF8; aClientDrivenID: PCardinal; aServiceCustomAnswer: PServiceCustomAnswer): boolean; override; public constructor Create(aRest: TSQLRest; aFactory: TInterfaceFactory; aCallBackUnRegisterNeeded: boolean; out aCallbackInterface); destructor Destroy; override; procedure CallBackUnRegister; end; constructor TInterfacedObjectMultiList.Create; begin inherited Create; fDests.InitSpecific(TypeInfo(TInterfacedObjectMultiDestDynArray), fDest,djInterface,@fDestCount); end; procedure TInterfacedObjectMultiList.Redirect(const aCallback: IInvokable; const aMethodsNames: array of RawUTF8; aSubscribe: boolean); var ndx: integer; new: TInterfacedObjectMultiDest; const NAM: array[boolean] of string[11] = ('Unsubscribe','Subscribe'); begin if (self=nil) or (fFakeCallback=nil) then exit; fFakeCallback.fRest.InternalLog('%.Redirect: % % using %',[ClassType,NAM[aSubscribe], fFakeCallback.fFactory.fInterfaceName,ObjectFromInterface(aCallback)],sllDebug); fFakeCallback.fFactory.CheckMethodIndexes(aMethodsNames,true,new.methods); new.instance := aCallback; fSafe.Lock; try ndx := fDests.Find(aCallback); if aSubscribe then if ndx<0 then fDests.Add(new) else fDest[ndx] := new else fDests.Delete(ndx); finally fSafe.UnLock; end; end; procedure TInterfacedObjectMultiList.Redirect(const aCallback: TInterfacedObject; const aMethodsNames: array of RawUTF8; aSubscribe: boolean); var dest: IInvokable; begin if (self=nil) or (fFakeCallback=nil) then exit; if aCallback=nil then raise EInterfaceFactoryException.CreateUTF8('%.Redirect(nil)',[self]); if not aCallback.GetInterface(fFakeCallback.fFactory.fInterfaceIID,dest) then raise EInterfaceFactoryException.CreateUTF8('%.Redirect [%]: % is not a %', [self,fFakeCallback.fName,aCallback,fFakeCallback.fFactory.fInterfaceName]); Redirect(dest,aMethodsNames,aSubscribe); end; procedure TInterfacedObjectMultiList.CallBackUnRegister; begin fSafe.Lock; try fDests.ClearSafe; finally fSafe.UnLock; end; if fFakeCallback<>nil then begin fFakeCallback.CallBackUnRegister; fFakeCallback := nil; // disable any further Redirect() end; end; destructor TInterfacedObjectMultiList.Destroy; begin CallBackUnRegister; inherited Destroy; end; function TInterfacedObjectMultiList.GetInstances(aMethod: integer; var aInstances: TPointerDynArray): integer; var i: integer; dest: ^TInterfacedObjectMultiDest; begin result := 0; dec(aMethod,RESERVED_VTABLE_SLOTS); if aMethod<0 then exit; SetLength(aInstances,fDestCount); dest := pointer(fDest); for i := 1 to fDestCount do begin if aMethod in dest^.methods then begin aInstances[result] := pointer(dest^.instance); inc(result); end; inc(dest); end; if result<>fDestCount then SetLength(aInstances,result); end; procedure TInterfacedObjectMulti.CallBackUnRegister; begin if fCallBackUnRegisterNeeded then begin fRest.InternalLog('%.Destroy -> Services.CallbackUnRegister(%)', [fList.ClassType,fFactory.fInterfaceTypeInfo.Name],sllDebug); fRest.Services.CallBackUnRegister(IInvokable(pointer(@fVTable))); end; end; constructor TInterfacedObjectMulti.Create(aRest: TSQLRest; aFactory: TInterfaceFactory; aCallBackUnRegisterNeeded: boolean; out aCallbackInterface); begin if aRest=nil then raise EServiceException.CreateUTF8('%.Create(aRest=nil)',[self]); fRest := aRest; fName := fRest.Model.Root; fCallBackUnRegisterNeeded := aCallBackUnRegisterNeeded; fList := TInterfacedObjectMultiList.Create; fList.fFakeCallback := self; inherited Create(aFactory,nil,[ifoJsonAsExtended,ifoDontStoreVoidJSON],FakeInvoke,nil); Get(aCallbackInterface); end; destructor TInterfacedObjectMulti.Destroy; begin fList.CallBackUnRegister; inherited Destroy; end; function TInterfacedObjectMulti.FakeInvoke(const aMethod: TServiceMethod; const aParams: RawUTF8; aResult, aErrorMsg: PRawUTF8; aClientDrivenID: PCardinal; aServiceCustomAnswer: PServiceCustomAnswer): boolean; var i: integer; exec: TServiceMethodExecute; instances: TPointerDynArray; begin result := inherited FakeInvoke(aMethod,aParams,aResult,aErrorMsg, aClientDrivenID,aServiceCustomAnswer); if not result or (fList.fDestCount=0) then exit; fList.fSafe.Lock; try if fList.GetInstances(aMethod.ExecutionMethodIndex,instances)=0 then exit; exec := TServiceMethodExecute.Create(@aMethod); try exec.Options := [optIgnoreException]; // use exec.ExecutedInstancesFailed result := exec.ExecuteJson(instances,pointer('['+aParams+']'),nil); if exec.ExecutedInstancesFailed<>nil then for i := high(exec.ExecutedInstancesFailed) downto 0 do if exec.ExecutedInstancesFailed[i]<>'' then try fRest.InternalLog('%.FakeInvoke % failed due to % -> unsubscribe', [ClassType,aMethod.InterfaceDotMethodName,exec.ExecutedInstancesFailed[i]],sllDebug); fList.fDests.FindAndDelete(instances[i]); except // ignore any exception when releasing the (unstable?) callback end; finally exec.Free; end; finally fList.fSafe.UnLock; end; end; function TSQLRest.MultiRedirect(const aGUID: TGUID; out aCallbackInterface; aCallBackUnRegisterNeeded: boolean): IMultiCallbackRedirect; var factory: TInterfaceFactory; begin factory := TInterfaceFactory.Get(aGUID); if factory=nil then raise EInterfaceFactoryException.CreateUTF8('%.MultiRedirect: unknown %', [self,GUIDToShort(aGUID)]); result := TInterfacedObjectMulti.Create(self,factory,aCallBackUnRegisterNeeded, aCallbackInterface).fList; end; { TInterfaceStubRules } function TInterfaceStubRules.FindRuleIndex(const aParams: RawUTF8): integer; begin for result := 0 to length(Rules)-1 do if Rules[result].Params=aParams then exit; result := -1; end; function TInterfaceStubRules.FindStrongRuleIndex(const aParams: RawUTF8): integer; begin for result := 0 to length(Rules)-1 do if (Rules[result].Kind<>isUndefined) and (Rules[result].Params=aParams) then exit; result := -1; end; procedure TInterfaceStubRules.AddRule(Sender: TInterfaceStub; aKind: TInterfaceStubRuleKind; const aParams, aValues: RawUTF8; const aEvent: TNotifyEvent; aExceptionClass: ExceptClass; aExpectedPassCountOperator: TSQLQueryOperator; aValue: cardinal); var n,ndx: integer; begin ndx := FindRuleIndex(aParams); n := length(Rules); if ndx<0 then SetLength(Rules,n+1) else n := ndx; if (aParams='') and (aKind<>isUndefined) then DefaultRule := n; with Rules[n] do begin Params := aParams; case aKind of isUndefined: ; // do not overwrite Values for weak rules like ExpectsCount/ExpectsTrace isReturns: Values := '['+AValues+']'; isFails: Values := ToText(Sender.ClassType)+' returned error: '+aValues; else Values := aValues; end; if aKind=isUndefined then if aExpectedPassCountOperator=qoContains then ExpectedTraceHash := aValue else begin ExpectedPassCountOperator := aExpectedPassCountOperator; ExpectedPassCount := aValue; end else begin Kind := aKind; Execute := TMethod(aEvent); ExceptionClass := aExceptionClass; end; end; end; { EInterfaceStub } constructor EInterfaceStub.Create(Sender: TInterfaceStub; const Method: TServiceMethod; const Error: RawUTF8); begin inherited CreateUTF8('Error in % for %.% - %', [Sender,Sender.fInterface.fInterfaceName,Method.URI,Error]); end; constructor EInterfaceStub.Create(Sender: TInterfaceStub; const Method: TServiceMethod; const Format: RawUTF8; const Args: array of const); begin Create(Sender,Method,FormatUTF8(Format,Args)); end; { TInterfaceStubLog } function TInterfaceStubLog.Results: RawUTF8; begin if CustomResults='' then result := Method^.DefaultResult else result := CustomResults; end; procedure TInterfaceStubLog.AddAsText(WR: TTextWriter; aScope: TInterfaceStubLogLayouts; SepChar: AnsiChar=','); begin if wName in aScope then WR.AddString(Method^.URI); if wParams in aScope then begin WR.Add('('); WR.AddString(Params); WR.Add(')'); end; if WasError then begin WR.AddShort(' error "'); WR.AddString(CustomResults); WR.Add('"'); end else if (wResults in aScope) and (Method^.ArgsResultIndex>=0) then begin if (wName in aScope) or (wParams in aScope) then WR.Add('='); if CustomResults='' then WR.AddString(Method^.DefaultResult) else WR.AddString(CustomResults); end; WR.Add(SepChar); end; { TOnInterfaceStubExecuteParamsAbstract } constructor TOnInterfaceStubExecuteParamsAbstract.Create(aSender: TInterfaceStub; aMethod: PServiceMethod; const aParams,aEventParams: RawUTF8); begin fSender := aSender; fMethod := aMethod; fParams := aParams; fEventParams := aEventParams; end; procedure TOnInterfaceStubExecuteParamsAbstract.Error( const Format: RawUTF8; const Args: array of const); begin Error(FormatUTF8(Format,Args)); end; procedure TOnInterfaceStubExecuteParamsAbstract.Error(const aErrorMessage: RawUTF8); begin fFailed := true; fResult := aErrorMessage; end; function TOnInterfaceStubExecuteParamsAbstract.GetSenderAsMockTestCase: TSynTestCase; begin result := (fSender as TInterfaceMock).TestCase; end; { TOnInterfaceStubExecuteParamsJSON } procedure TOnInterfaceStubExecuteParamsJSON.Returns(const Values: array of const); begin JSONEncodeArrayOfConst(Values,false,fResult); end; procedure TOnInterfaceStubExecuteParamsJSON.Returns(const ValuesJsonArray: RawUTF8); begin fResult := ValuesJsonArray; end; {$ifndef NOVARIANTS} { TOnInterfaceStubExecuteParamsVariant } constructor TOnInterfaceStubExecuteParamsVariant.Create(aSender: TInterfaceStub; aMethod: PServiceMethod; const aParams, aEventParams: RawUTF8); var i: integer; P: PUTF8Char; tmp: TSynTempBuffer; begin inherited; SetLength(fInput,fMethod^.ArgsInputValuesCount); tmp.Init(aParams); try P := tmp.buf; for i := 0 to fMethod^.ArgsInputValuesCount-1 do P := VariantLoadJSON(fInput[i],P,nil,@aSender.fInterface.DocVariantOptions); finally tmp.Done; end; SetLength(fOutput,fMethod^.ArgsOutputValuesCount); end; function TOnInterfaceStubExecuteParamsVariant.GetInput(Index: Integer): variant; begin if cardinal(Index)>=fMethod^.ArgsInputValuesCount then raise EInterfaceStub.Create(fSender,fMethod^,'Input[%>=%]', [Index,fMethod^.ArgsInputValuesCount]) else result := fInput[Index]; end; procedure TOnInterfaceStubExecuteParamsVariant.SetOutput(Index: Integer; const Value: variant); begin if cardinal(Index)>=fMethod^.ArgsOutputValuesCount then raise EInterfaceStub.Create(fSender,fMethod^,'Output[%>=%]', [Index,fMethod^.ArgsOutputValuesCount]) else fOutput[Index] := Value; end; function TOnInterfaceStubExecuteParamsVariant.GetInNamed(const aParamName: RawUTF8): variant; var L,a,ndx: integer; begin L := Length(aParamName); ndx := 0; if (L>0) and (fInput<>nil) then for a := fMethod^.ArgsInFirst to fMethod^.ArgsInLast do with fMethod^.Args[a] do if ValueDirection in [smdConst,smdVar] then begin if IdemPropName(ParamName^,pointer(aParamName),L) then begin result := fInput[ndx]; exit; end; inc(ndx); if cardinal(ndx)>=cardinal(fMethod^.ArgsInputValuesCount) then break; end; raise EInterfaceStub.Create(fSender,fMethod^,'unknown input parameter [%]',[aParamName]); end; function TOnInterfaceStubExecuteParamsVariant.GetInUTF8(const ParamName: RawUTF8): RawUTF8; var wasString: boolean; begin result := ''; VariantToUTF8(GetInNamed(ParamName),result,wasString); end; procedure TOnInterfaceStubExecuteParamsVariant.SetOutNamed(const aParamName: RawUTF8; const Value: variant); var L,a,ndx: integer; begin L := Length(aParamName); ndx := 0; if (L>0) and (fOutput<>nil) then for a := fMethod^.ArgsOutFirst to fMethod^.ArgsOutLast do with fMethod^.Args[a] do if ValueDirection<>smdConst then begin if IdemPropName(ParamName^,pointer(aParamName),L) then begin fOutput[ndx] := Value; exit; end; inc(ndx); if cardinal(ndx)>=cardinal(fMethod^.ArgsOutputValuesCount) then break; end; raise EInterfaceStub.Create(fSender,fMethod^,'unknown output parameter [%]',[aParamName]); end; procedure TOnInterfaceStubExecuteParamsVariant.SetResultFromOutput; var a,ndx: integer; W: TJSONSerializer; temp: TTextWriterStackBuffer; begin fResult := ''; if fOutput=nil then exit; W := TJSONSerializer.CreateOwnedStream(temp); try W.Add('['); ndx := 0; for a := fMethod^.ArgsOutFirst to fMethod^.ArgsOutLast do with fMethod^.Args[a] do if ValueDirection<>smdConst then begin if TVarData(fOutput[ndx]).VType=varEmpty then AddDefaultJSON(W) else begin W.AddVariant(fOutput[ndx],twJSONEscape); W.Add(','); end; inc(ndx); if cardinal(ndx)>=cardinal(fMethod^.ArgsOutputValuesCount) then break; end; W.CancelLastComma; W.Add(']'); W.SetText(fResult); finally W.Free; end; end; function TOnInterfaceStubExecuteParamsVariant.InputAsDocVariant( Kind: TServiceMethodParamsDocVariantKind; Options: TDocVariantOptions): variant; begin VarClear(result); fMethod^.ArgsValuesAsDocVariant(Kind,TDocVariantData(Result),fInput,true,Options); end; function TOnInterfaceStubExecuteParamsVariant.OutputAsDocVariant( Kind: TServiceMethodParamsDocVariantKind; Options: TDocVariantOptions): variant; begin VarClear(result); fMethod^.ArgsValuesAsDocVariant(Kind,TDocVariantData(Result),fOutput,false,Options); end; {$ifdef WITHLOG} procedure TOnInterfaceStubExecuteParamsVariant.AddLog(aLog: TSynLogClass; aOutput: boolean; aLevel: TSynLogInfo); var val: variant; begin if aLog=nil then exit; with aLog.Family do if aLevel in Level then begin if aOutput then val := OutputAsDocVariant(pdvObjectFixed) else val := InputAsDocVariant(pdvObjectFixed); SynLog.Log(aLevel,'%(%)',[fMethod^.InterfaceDotMethodName, _Safe(val)^.ToTextPairs('=',',',twJSONEscape)],self); end; end; {$endif} {$endif NOVARIANTS} { TInterfaceStub } constructor TInterfaceStub.Create(aFactory: TInterfaceFactory; const aInterfaceName: RawUTF8); var i: integer; begin if aFactory=nil then raise EInterfaceStub.CreateUTF8( '%.Create(%): Interface not registered - you could use '+ 'TInterfaceFactory.RegisterInterfaces()',[self,aInterfaceName]); fInterface := aFactory; SetLength(fRules,fInterface.MethodsCount); for i := 0 to fInterface.MethodsCount-1 do fRules[i].DefaultRule := -1; fLog.Init(TypeInfo(TInterfaceStubLogDynArray),fLogs,@fLogCount); end; procedure TInterfaceStub.InternalGetInstance(out aStubbedInterface); var fake: TInterfacedObjectFake; begin fake := TInterfacedObjectFake.Create(fInterface,nil,[ifoJsonAsExtended,ifoDontStoreVoidJSON], Invoke,InstanceDestroyed); pointer(aStubbedInterface) := @fake.fVTable; fake._AddRef; fLastInterfacedObjectFake := fake; end; function TInterfaceStub.InternalCheck(aValid,aExpectationFailed: boolean; const aErrorMsgFmt: RawUTF8; const aErrorMsgArgs: array of const): boolean; begin result := aValid; if aExpectationFailed and not aValid then raise EInterfaceStub.CreateUTF8('%.InternalCheck(%) failed: %', [self,fInterface.fInterfaceName,FormatUTF8(aErrorMsgFmt,aErrorMsgArgs)]); end; constructor TInterfaceStub.Create(const aInterfaceName: RawUTF8; out aStubbedInterface); begin Create(TInterfaceFactory.Get(aInterfaceName),aInterfaceName); InternalGetInstance(aStubbedInterface); end; constructor TInterfaceStub.Create(const aGUID: TGUID; out aStubbedInterface); begin Create(TInterfaceFactory.Get(aGUID),GUIDToRawUTF8(aGUID)); InternalGetInstance(aStubbedInterface); end; constructor TInterfaceStub.Create(aInterface: PTypeInfo; out aStubbedInterface); begin Create(aInterface); InternalGetInstance(aStubbedInterface); end; constructor TInterfaceStub.Create(aInterface: PTypeInfo); begin Create(TInterfaceFactory.Get(aInterface),ToUTF8(aInterface^.Name)); end; constructor TInterfaceStub.Create(const aGUID: TGUID); begin Create(TInterfaceFactory.Get(aGUID),ToUTF8(aGUID)); end; procedure TInterfaceStub.IntSetOptions(Options: TInterfaceStubOptions); begin if Options=fOptions then exit; fOptions := Options; end; procedure TInterfaceStub.IntCheckCount(aMethodIndex, aComputed: cardinal; aOperator: TSQLQueryOperator; aCount: cardinal); var ok: boolean; begin case aOperator of qoEqualTo: ok := aComputed=aCount; qoNotEqualTo: ok := aComputed<>aCount; qoLessThan: ok := aComputedaCount; qoGreaterThanOrEqualTo: ok := aComputed>=aCount; else raise EInterfaceStub.CreateUTF8('%.IntCheckCount(): Unexpected % operator', [self,Ord(aOperator)]); end; InternalCheck(ok,True,'ExpectsCount(''%'',%,%) failed: count=%', [fInterface.Methods[aMethodIndex].URI,ToText(aOperator)^,aCount,aComputed]); end; procedure TInterfaceStub.InstanceDestroyed(aClientDrivenID: cardinal); var m,r,asmndx: integer; num: cardinal; begin if self<>nil then try if eCount in fHasExpects then for m := 0 to fInterface.MethodsCount-1 do with fRules[m] do for r := 0 to high(Rules) do with Rules[r] do if ExpectedPassCountOperator<>qoNone then begin if Params='' then num := MethodPassCount else num := RulePassCount; IntCheckCount(m,num,ExpectedPassCountOperator,ExpectedPassCount); end; if fInterfaceExpectedTraceHash<>0 then InternalCheck(LogHash=fInterfaceExpectedTraceHash,True, 'ExpectsTrace(%) returned %',[fInterfaceExpectedTraceHash,LogHash]); if eTrace in fHasExpects then for m := 0 to fInterface.MethodsCount-1 do with fRules[m] do begin asmndx := m+RESERVED_VTABLE_SLOTS; for r := 0 to high(Rules) do with Rules[r] do if ExpectedTraceHash<>0 then InternalCheck(ExpectedTraceHash=Hash32(IntGetLogAsText( asmndx,Params,[wName,wParams,wResults],',')),True, 'ExpectsTrace(''%'') failed',[fInterface.Methods[m].URI]); end; finally if not (imoFakeInstanceWontReleaseTInterfaceStub in Options) then Free; // creature will release its creator end; end; function TInterfaceStub.SetOptions(Options: TInterfaceStubOptions): TInterfaceStub; begin IntSetOptions(Options); result := self; end; function TInterfaceStub.Executes(const aMethodName, aParams: RawUTF8; aEvent: TOnInterfaceStubExecuteJSON; const aEventParams: RawUTF8): TInterfaceStub; begin fRules[fInterface.CheckMethodIndex(aMethodName)]. AddRule(self,isExecutesJSON,aParams,aEventParams,TNotifyEvent(aEvent)); result := self; end; function TInterfaceStub.Executes(const aMethodName: RawUTF8; aEvent: TOnInterfaceStubExecuteJSON; const aEventParams: RawUTF8): TInterfaceStub; begin result := Executes(aMethodName,'',aEvent,aEventParams); end; function TInterfaceStub.Executes(const aMethodName: RawUTF8; const aParams: array of const; aEvent: TOnInterfaceStubExecuteJSON; const aEventParams: RawUTF8): TInterfaceStub; begin result := Executes(aMethodName,JSONEncodeArrayOfConst(aParams,true), aEvent,aEventParams); end; {$ifndef NOVARIANTS} function TInterfaceStub.Executes(const aMethodName, aParams: RawUTF8; aEvent: TOnInterfaceStubExecuteVariant; const aEventParams: RawUTF8): TInterfaceStub; begin fRules[fInterface.CheckMethodIndex(aMethodName)]. AddRule(self,isExecutesVariant,aParams,aEventParams,TNotifyEvent(aEvent)); result := self; end; function TInterfaceStub.Executes(const aMethodName: RawUTF8; aEvent: TOnInterfaceStubExecuteVariant; const aEventParams: RawUTF8): TInterfaceStub; begin result := Executes(aMethodName,'',aEvent,aEventParams); end; function TInterfaceStub.Executes(const aMethodName: RawUTF8; const aParams: array of const; aEvent: TOnInterfaceStubExecuteVariant; const aEventParams: RawUTF8): TInterfaceStub; begin result := Executes(aMethodName,JSONEncodeArrayOfConst(aParams,true), aEvent,aEventParams); end; function TInterfaceStub.Executes(aEvent: TOnInterfaceStubExecuteVariant; const aEventParams: RawUTF8): TInterfaceStub; var i: integer; begin for i := 0 to fInterface.MethodsCount-1 do fRules[i].AddRule(self,isExecutesVariant,'',aEventParams,TNotifyEvent(aEvent)); result := self; end; type TInterfaceStubExecutesToLog = packed record Log: TSynLogClass; LogLevel: TSynLogInfo; Kind: TServiceMethodParamsDocVariantKind; end; PInterfaceStubExecutesToLog = ^TInterfaceStubExecutesToLog; procedure TInterfaceStub.OnExecuteToLog(Ctxt: TOnInterfaceStubExecuteParamsVariant); begin if length(Ctxt.EventParams)=SizeOf(TInterfaceStubExecutesToLog) then with PInterfaceStubExecutesToLog(Ctxt.EventParams)^ do Log.Add.Log(LogLevel,'% %',[Ctxt.Method^.InterfaceDotMethodName, Ctxt.InputAsDocVariant(Kind,JSON_OPTIONS_FAST_EXTENDED)]); end; function TInterfaceStub.Executes(aLog: TSynLogClass; aLogLevel: TSynLogInfo; aKind: TServiceMethodParamsDocVariantKind): TInterfaceStub; var tmp: RawUTF8; begin SetLength(tmp,SizeOf(TInterfaceStubExecutesToLog)); with PInterfaceStubExecutesToLog(tmp)^ do begin Log := aLog; LogLevel := aLogLevel; Kind := aKind; end; Executes(OnExecuteToLog,tmp); result := self; end; {$endif NOVARIANTS} function TInterfaceStub.ExpectsCount(const aMethodName: RawUTF8; aOperator: TSQLQueryOperator; aValue: cardinal): TInterfaceStub; begin result := ExpectsCount(aMethodName,'',aOperator,aValue); end; function TInterfaceStub.ExpectsCount(const aMethodName, aParams: RawUTF8; aOperator: TSQLQueryOperator; aValue: cardinal): TInterfaceStub; var ndx: integer; begin ndx := fInterface.CheckMethodIndex(aMethodName); if aOperator in [qoEqualTo..qoGreaterThanOrEqualTo] then with fRules[ndx] do AddRule(self,isUndefined,aParams,'',nil,nil,aOperator,aValue) else raise EInterfaceStub.Create(self,fInterface.fMethods[ndx], 'ExpectsCount(aOperator=%)',[ord(aOperator)]); include(fHasExpects,eCount); result := self; end; function TInterfaceStub.ExpectsCount(const aMethodName: RawUTF8; const aParams: array of const; aOperator: TSQLQueryOperator; aValue: cardinal): TInterfaceStub; begin result := ExpectsCount(aMethodName,JSONEncodeArrayOfConst(aParams,true),aOperator,aValue); end; function TInterfaceStub.ExpectsTrace(aValue: cardinal): TInterfaceStub; begin include(fOptions,imoLogMethodCallsAndResults); fInterfaceExpectedTraceHash := aValue; result := self; end; function TInterfaceStub.ExpectsTrace(const aMethodName: RawUTF8; aValue: cardinal): TInterfaceStub; begin result := ExpectsTrace(aMethodName,'',aValue); end; function TInterfaceStub.ExpectsTrace(const aMethodName, aParams: RawUTF8; aValue: cardinal): TInterfaceStub; begin fRules[fInterface.CheckMethodIndex(aMethodName)]. AddRule(self,isUndefined,aParams,'',nil,nil,qoContains,aValue); include(fOptions,imoLogMethodCallsAndResults); include(fHasExpects,eTrace); result := self; end; function TInterfaceStub.ExpectsTrace(const aMethodName: RawUTF8; const aParams: array of const; aValue: cardinal): TInterfaceStub; begin result := ExpectsTrace(aMethodName,JSONEncodeArrayOfConst(aParams,true),aValue); end; function TInterfaceStub.ExpectsTrace(const aValue: RawUTF8): TInterfaceStub; begin result := ExpectsTrace(Hash32(aValue)); end; function TInterfaceStub.ExpectsTrace(const aMethodName, aValue: RawUTF8): TInterfaceStub; begin result := ExpectsTrace(aMethodName,Hash32(aValue)); end; function TInterfaceStub.ExpectsTrace(const aMethodName, aParams, aValue: RawUTF8): TInterfaceStub; begin result := ExpectsTrace(aMethodName,aParams,Hash32(aValue)); end; function TInterfaceStub.ExpectsTrace(const aMethodName: RawUTF8; const aParams: array of const; const aValue: RawUTF8): TInterfaceStub; begin result := ExpectsTrace(aMethodName,aParams,Hash32(aValue)); end; function TInterfaceStub.Fails(const aMethodName, aErrorMsg: RawUTF8): TInterfaceStub; begin result := Fails(aMethodName,'',aErrorMsg); end; function TInterfaceStub.Fails(const aMethodName, aParams, aErrorMsg: RawUTF8): TInterfaceStub; begin fRules[fInterface.CheckMethodIndex(aMethodName)]. AddRule(self,isFails,aParams,aErrorMsg); result := self; end; function TInterfaceStub.Fails(const aMethodName: RawUTF8; const aParams: array of const; const aErrorMsg: RawUTF8): TInterfaceStub; begin result := Fails(aMethodName,JSONEncodeArrayOfConst(aParams,true),aErrorMsg); end; function TInterfaceStub.Raises(const aMethodName, aParams: RawUTF8; aException: ExceptClass; const aMessage: string): TInterfaceStub; begin fRules[fInterface.CheckMethodIndex(aMethodName)]. AddRule(self,isRaises,aParams,StringToUTF8(aMessage),nil,aException); result := self; end; function TInterfaceStub.Raises(const aMethodName: RawUTF8; const aParams: array of const; aException: ExceptClass; const aMessage: string): TInterfaceStub; begin result := Raises(aMethodName,JSONEncodeArrayOfConst(aParams,true), aException,aMessage); end; function TInterfaceStub.Raises(const aMethodName: RawUTF8; aException: ExceptClass; const aMessage: string): TInterfaceStub; begin result := Raises(aMethodName,'',aException,aMessage); end; function TInterfaceStub.Returns(const aMethodName, aParams, aExpectedResults: RawUTF8): TInterfaceStub; begin fRules[fInterface.CheckMethodIndex(aMethodName)]. AddRule(self,isReturns,aParams,aExpectedResults); result := self; end; function TInterfaceStub.Returns(const aMethodName: RawUTF8; const aParams, aExpectedResults: array of const): TInterfaceStub; begin result := Returns(aMethodName,JSONEncodeArrayOfConst(aParams,true), JSONEncodeArrayOfConst(aExpectedResults,true)); end; function TInterfaceStub.Returns(const aMethodName, aExpectedResults: RawUTF8): TInterfaceStub; begin result := Returns(aMethodName,'',aExpectedResults); end; function TInterfaceStub.Returns(const aMethodName: RawUTF8; const aExpectedResults: array of const): TInterfaceStub; begin result := Returns(aMethodName,'',JSONEncodeArrayOfConst(aExpectedResults,true)); end; function TInterfaceStub.Invoke(const aMethod: TServiceMethod; const aParams: RawUTF8; aResult, aErrorMsg: PRawUTF8; aClientDrivenID: PCardinal; aServiceCustomAnswer: PServiceCustomAnswer): boolean; var ndx: cardinal; rule: integer; ExecutesCtxtJSON: TOnInterfaceStubExecuteParamsJSON; ExecutesCtxtVariant: TOnInterfaceStubExecuteParamsVariant; log: TInterfaceStubLog; begin ndx := aMethod.ExecutionMethodIndex-RESERVED_VTABLE_SLOTS; if ndx>=fInterface.MethodsCount then result := false else with fRules[ndx] do begin inc(MethodPassCount); rule := FindStrongRuleIndex(aParams); if rule<0 then begin rule := FindRuleIndex(aParams); if (rule>=0) and (DefaultRule>=0) then inc(Rules[rule].RulePassCount); rule := DefaultRule; end; if rule<0 then if imoRaiseExceptionIfNoRuleDefined in Options then raise EInterfaceStub.Create(self,aMethod,'No rule defined') else begin rule := FindRuleIndex(aParams); if rule>=0 then inc(Rules[rule].RulePassCount); if imoReturnErrorIfNoRuleDefined in Options then begin result := false; FormatUTF8('No stubbing rule defined for %.%', [fInterface.fInterfaceName,aMethod.URI],log.CustomResults); end else result := true; end else with Rules[rule] do begin inc(RulePassCount); case Kind of isExecutesJSON: begin ExecutesCtxtJSON := TOnInterfaceStubExecuteParamsJSON.Create( self,@aMethod,aParams,Values); try TOnInterfaceStubExecuteJSON(Execute)(ExecutesCtxtJSON); result := not ExecutesCtxtJSON.Failed; log.CustomResults := ExecutesCtxtJSON.Result; finally ExecutesCtxtJSON.Free; end; end; {$ifndef NOVARIANTS} isExecutesVariant: begin ExecutesCtxtVariant := TOnInterfaceStubExecuteParamsVariant.Create( self,@aMethod,aParams,Values); try TOnInterfaceStubExecuteVariant(Execute)(ExecutesCtxtVariant); result := not ExecutesCtxtVariant.Failed; if result then begin ExecutesCtxtVariant.SetResultFromOutput; log.CustomResults := ExecutesCtxtVariant.Result; end; finally ExecutesCtxtVariant.Free; end; end; {$endif} isRaises: raise ExceptionClass.Create(UTF8ToString(Values)); isReturns: begin result := true; log.CustomResults := Values; end; isFails: begin result := InternalCheck(false,false,'%',[Values]); if not result then log.CustomResults := Values; end; else result := true; // ignore isUndefined (ExpectsCount only) rules end; end; if result then begin if aResult<>nil then // make unique due to JSONDecode() by caller if log.CustomResults='' then FastSetString(aResult^,pointer(aMethod.DefaultResult),length(aMethod.DefaultResult)) else FastSetString(aResult^,pointer(log.CustomResults),length(log.CustomResults)); end else if aErrorMsg<>nil then aErrorMsg^ := log.CustomResults; if imoLogMethodCallsAndResults in Options then begin log.Timestamp64 := GetTickCount64; log.WasError := not result; log.Method := @aMethod; log.Params := aParams; fLog.Add(log); end; end; end; function TInterfaceStub.LogAsText(SepChar: AnsiChar): RawUTF8; begin result := IntGetLogAsText(0,'',[wName,wParams,wResults],SepChar); end; procedure TInterfaceStub.ClearLog; begin fLog.Clear; end; function TInterfaceStub.IntGetLogAsText(asmndx: integer; const aParams: RawUTF8; aScope: TInterfaceStubLogLayouts; SepChar: AnsiChar): RawUTF8; var i: integer; WR: TTextWriter; temp: TTextWriterStackBuffer; log: ^TInterfaceStubLog; begin if fLogCount=0 then result := '' else begin WR := TTextWriter.CreateOwnedStream(temp); try log := Pointer(fLogs); if asmndxfInterface.fInterfaceTypeInfo then result := false else begin InternalGetInstance(Obj); result := true; end; end; function TInterfaceStub.Implements(aInterface: PTypeInfo): boolean; begin result := fInterface.fInterfaceTypeInfo=aInterface; end; { TInterfaceMock } constructor TInterfaceMock.Create(aInterface: PTypeInfo; out aMockedInterface; aTestCase: TSynTestCase); begin inherited Create(aInterface,aMockedInterface); fTestCase := aTestCase; end; constructor TInterfaceMock.Create(const aGUID: TGUID; out aMockedInterface; aTestCase: TSynTestCase); begin inherited Create(aGUID,aMockedInterface); fTestCase := aTestCase; end; constructor TInterfaceMock.Create(const aInterfaceName: RawUTF8; out aMockedInterface; aTestCase: TSynTestCase); begin inherited Create(aInterfaceName,aMockedInterface); fTestCase := aTestCase; end; constructor TInterfaceMock.Create(aInterface: PTypeInfo; aTestCase: TSynTestCase); begin inherited Create(aInterface); fTestCase := aTestCase; end; constructor TInterfaceMock.Create(const aGUID: TGUID; aTestCase: TSynTestCase); begin inherited Create(aGUID); fTestCase := aTestCase; end; function TInterfaceMock.InternalCheck(aValid,aExpectationFailed: boolean; const aErrorMsgFmt: RawUTF8; const aErrorMsgArgs: array of const): boolean; begin if fTestCase=nil then result := inherited InternalCheck(aValid,aExpectationFailed,aErrorMsgFmt,aErrorMsgArgs) else begin if aValid xor (imoMockFailsWillPassTestCase in Options) then fTestCase.Check(true) else fTestCase.Check(false,UTF8ToString(FormatUTF8(aErrorMsgFmt,aErrorMsgArgs))); result := true; // do not raise any exception at this stage for TInterfaceMock end; end; { TInterfaceMockSpy } constructor TInterfaceMockSpy.Create(aFactory: TInterfaceFactory; const aInterfaceName: RawUTF8); begin inherited Create(aFactory,aInterfaceName); include(fOptions,imoLogMethodCallsAndResults); end; procedure TInterfaceMockSpy.IntSetOptions(Options: TInterfaceStubOptions); begin include(Options,imoLogMethodCallsAndResults); inherited IntSetOptions(Options); end; procedure TInterfaceMockSpy.Verify(const aMethodName: RawUTF8; const aParams: array of const; aOperator: TSQLQueryOperator; aCount: cardinal); begin Verify(aMethodName,JSONEncodeArrayOfConst(aParams,true),aOperator,aCount); end; procedure TInterfaceMockSpy.Verify(const aMethodName: RawUTF8; const aParams: array of const; const aTrace: RawUTF8); begin Verify(aMethodName,JSONEncodeArrayOfConst(aParams,true),aTrace); end; procedure TInterfaceMockSpy.Verify(const aMethodName: RawUTF8; aOperator: TSQLQueryOperator; aCount: cardinal); var m: integer; begin m := fInterface.CheckMethodIndex(aMethodName); IntCheckCount(m,fRules[m].MethodPassCount,aOperator,aCount); end; procedure TInterfaceMockSpy.Verify(const aMethodName, aParams: RawUTF8; aOperator: TSQLQueryOperator; aCount: cardinal); var asmndx, i: integer; c: cardinal; begin asmndx := fInterface.CheckMethodIndex(aMethodName)+RESERVED_VTABLE_SLOTS; if aParams='' then c := fRules[asmndx-RESERVED_VTABLE_SLOTS].MethodPassCount else begin c := 0; for i := 0 to fLogCount-1 do with fLogs[i] do if (Method.ExecutionMethodIndex=asmndx) and (Params=aParams) then inc(c); end; IntCheckCount(asmndx-RESERVED_VTABLE_SLOTS,c,aOperator,aCount); end; procedure TInterfaceMockSpy.Verify(const aTrace: RawUTF8; aScope: TInterfaceMockSpyCheck); const VERIFY_SCOPE: array[TInterfaceMockSpyCheck] of TInterfaceStubLogLayouts = ( [wName], [wName, wParams], [wName, wParams, wResults]); begin InternalCheck(IntGetLogAsText(0,'',VERIFY_SCOPE[aScope],',')=aTrace,true, 'Verify(''%'',%) failed',[aTrace,ToText(aScope)^]); end; procedure TInterfaceMockSpy.Verify(const aMethodName, aParams, aTrace: RawUTF8); var m: integer; begin m := fInterface.CheckMethodIndex(aMethodName); InternalCheck( IntGetLogAsText(m+RESERVED_VTABLE_SLOTS,aParams,[wResults],',')=aTrace,True, 'Verify(''%'',''%'',''%'') failed',[aMethodName,aParams,aTrace]); end; procedure TInterfaceMockSpy.Verify(const aMethodName, aTrace: RawUTF8; aScope: TInterfaceMockSpyCheck); const VERIFY_SCOPE: array[TInterfaceMockSpyCheck] of TInterfaceStubLogLayouts = ( [], [wParams], [wParams, wResults]); var m: integer; begin m := fInterface.CheckMethodIndex(aMethodName); if aScope=chkName then raise EInterfaceStub.Create(self,fInterface.Methods[m],'Invalid scope for Verify()'); InternalCheck( IntGetLogAsText(m+RESERVED_VTABLE_SLOTS,'',VERIFY_SCOPE[aScope],',')=aTrace,True, 'Verify(''%'',''%'',%) failed',[aMethodName,aTrace,ToText(aScope)^]); end; { TInterfaceResolverForSingleInterface } constructor TInterfaceResolverForSingleInterface.Create( aInterface: PTypeInfo; aImplementation: TInterfacedObjectClass); var guid: PGUID; begin fInterfaceTypeInfo := aInterface; guid := aInterface^.InterfaceGUID; if guid=nil then raise EInterfaceResolverException.CreateUTF8('%.Create expects an Interface',[self]); fImplementationEntry := aImplementation.GetInterfaceEntry(guid^); if fImplementationEntry=nil then raise EInterfaceResolverException.CreateUTF8('%.Create: % does not implement %', [self,aImplementation,fInterfaceTypeInfo^.Name]); aInterface^.InterfaceAncestors(fInterfaceAncestors,aImplementation, fInterfaceAncestorsImplementationEntry); fImplementation.Init(aImplementation); end; constructor TInterfaceResolverForSingleInterface.Create(const aInterface: TGUID; aImplementation: TInterfacedObjectClass); begin Create(TInterfaceFactory.GUID2TypeInfo(aInterface),aImplementation); end; function TInterfaceResolverForSingleInterface.CreateInstance: TInterfacedObject; begin result := TInterfacedObject(fImplementation.CreateNew); end; function TInterfaceResolverForSingleInterface.GetImplementationName: string; begin if (self=nil) or (fImplementation.ItemClass=nil) then result := '' else result := string(fImplementation.ItemClass.ClassName); end; function TInterfaceResolverForSingleInterface.GetOneInstance(out Obj): boolean; begin if (self=nil) or (fImplementation.ItemClass=nil) then result := false else // here we now that CreateInstance will implement the interface result := GetInterfaceFromEntry(CreateInstance,fImplementationEntry,Obj); end; function TInterfaceResolverForSingleInterface.TryResolve( aInterface: PTypeInfo; out Obj): boolean; var i: integer; begin if fImplementation.ItemClass=nil then result := false else if fInterfaceTypeInfo=aInterface then result := GetInterfaceFromEntry( CreateInstance,fImplementationEntry,Obj) else begin // if not found exact interface, try any parent/ancestor interface for i := 0 to length(fInterfaceAncestors)-1 do if fInterfaceAncestors[i]=aInterface then begin // here we know that CreateInstance will implement fInterfaceAncestors[] result := GetInterfaceFromEntry( CreateInstance,fInterfaceAncestorsImplementationEntry[i],Obj); exit; end; result := false; end; end; function TInterfaceResolverForSingleInterface.Implements(aInterface: PTypeInfo): boolean; var i: integer; begin result := true; if fInterfaceTypeInfo=aInterface then exit; // found exact interface for i := 0 to length(fInterfaceAncestors)-1 do if fInterfaceAncestors[i]=aInterface then exit; // found any parent/ancestor interface result := false; end; { TInterfaceResolverInjected } var GlobalInterfaceResolutionLock: TRTLCriticalSection; GlobalInterfaceResolution: array of record TypeInfo: PTypeInfo; ImplementationClass: TClassInstance; InterfaceEntry: PInterfaceEntry; Instance: IInterface; end; class function TInterfaceResolverInjected.RegisterGlobalCheck(aInterface: PTypeInfo; aImplementationClass: TClass): PInterfaceEntry; var i: integer; begin if (aInterface=nil) or (aImplementationClass=nil) then raise EInterfaceResolverException.CreateUTF8('%.RegisterGlobal(nil)',[self]); if aInterface^.Kind<>tkInterface then raise EInterfaceResolverException.CreateUTF8('%.RegisterGlobal(%): % is not an interface', [self,aInterface^.Name]); result := aImplementationClass.GetInterfaceEntry(aInterface^.InterfaceGUID^); if result=nil then raise EInterfaceResolverException.CreateUTF8('%.RegisterGlobal(): % does not implement %', [self,aImplementationClass,aInterface^.Name]); EnterCriticalSection(GlobalInterfaceResolutionLock); for i := 0 to length(GlobalInterfaceResolution)-1 do if GlobalInterfaceResolution[i].TypeInfo=aInterface then begin LeaveCriticalSection(GlobalInterfaceResolutionLock); // release fSafe.Lock now raise EInterfaceResolverException.CreateUTF8('%.RegisterGlobal(%): % already registered', [self,aImplementationClass,aInterface^.Name]); end; end; // caller should explicitly call finally LeaveCriticalSection(...) end; class procedure TInterfaceResolverInjected.RegisterGlobal( aInterface: PTypeInfo; aImplementationClass: TInterfacedObjectClass); var aInterfaceEntry: PInterfaceEntry; n: integer; begin aInterfaceEntry := RegisterGlobalCheck(aInterface,aImplementationClass); try // here we are protected within a EnterCriticalSection() call n := length(GlobalInterfaceResolution); SetLength(GlobalInterfaceResolution,n+1); with GlobalInterfaceResolution[n] do begin TypeInfo := aInterface; ImplementationClass.Init(aImplementationClass); InterfaceEntry := aInterfaceEntry; end; finally LeaveCriticalSection(GlobalInterfaceResolutionLock); end; end; class procedure TInterfaceResolverInjected.RegisterGlobal( aInterface: PTypeInfo; aImplementation: TInterfacedObject); var aInterfaceEntry: PInterfaceEntry; n: integer; begin aInterfaceEntry := RegisterGlobalCheck(aInterface,aImplementation.ClassType); try // here we are protected within a EnterCriticalSection() call n := length(GlobalInterfaceResolution); SetLength(GlobalInterfaceResolution,n+1); with GlobalInterfaceResolution[n] do begin if not GetInterfaceFromEntry(aImplementation,aInterfaceEntry,Instance) then raise EInterfaceResolverException.CreateUTF8('Unexcepted %.RegisterGlobal(%,%)', [self,aInterface^.Name,aImplementation]); TypeInfo := aInterface; InterfaceEntry := aInterfaceEntry; end; finally LeaveCriticalSection(GlobalInterfaceResolutionLock); end; end; class procedure TInterfaceResolverInjected.RegisterGlobalDelete(aInterface: PTypeInfo); var i,n: integer; begin if (aInterface=nil) or (aInterface^.Kind<>tkInterface) then raise EInterfaceResolverException.CreateUTF8('%.RegisterGlobalDelete(?)',[self]); EnterCriticalSection(GlobalInterfaceResolutionLock); try n := length(GlobalInterfaceResolution)-1; for i := 0 to n do with GlobalInterfaceResolution[i] do if TypeInfo=aInterface then begin if Instance=nil then raise EInterfaceResolverException.CreateUTF8( '%.RegisterGlobalDelete(%) does not match an instance, but a class', [self,aInterface^.Name]); Instance := nil; // avoid GPF if n>i then MoveFast(GlobalInterfaceResolution[i+1],GlobalInterfaceResolution[i], (n-i)*SizeOf(GlobalInterfaceResolution[i])); SetLength(GlobalInterfaceResolution,n); exit; end; finally LeaveCriticalSection(GlobalInterfaceResolutionLock); end; end; procedure FinalizeGlobalInterfaceResolution; begin GlobalInterfaceResolution := nil; // also cleanup Instance fields DeleteCriticalSection(GlobalInterfaceResolutionLock); DeleteCriticalSection(vmtAutoTableLock); end; function TInterfaceResolverInjected.TryResolve(aInterface: PTypeInfo; out Obj): boolean; var i: integer; begin if aInterface<>nil then begin result := true; if self<>nil then begin // first check local DI/IoC if fResolvers<>nil then for i := 0 to length(fResolvers)-1 do if fResolvers[i].TryResolve(aInterface,Obj) then exit; if fDependencies<>nil then for i := 0 to Length(fDependencies)-1 do if fDependencies[i].GetInterface(aInterface^.InterfaceGUID^,Obj) then exit; end; EnterCriticalSection(GlobalInterfaceResolutionLock); // shared DI/IoC try for i := 0 to length(GlobalInterfaceResolution)-1 do with GlobalInterfaceResolution[i] do if TypeInfo=aInterface then if Instance<>nil then begin IInterface(Obj) := Instance; exit; end else if GetInterfaceFromEntry(ImplementationClass.CreateNew,InterfaceEntry,Obj) then exit; finally LeaveCriticalSection(GlobalInterfaceResolutionLock); end; end; result := false; end; function TInterfaceResolverInjected.TryResolveInternal(aInterface: PTypeInfo; out Obj): boolean; var i: integer; begin result := true; if (self<>nil) and (aInterface<>nil) and (fResolvers<>nil) then for i := 0 to length(fResolvers)-1 do if fResolvers[i].TryResolve(aInterface,Obj) then exit; result := false; end; function TInterfaceResolverInjected.Implements(aInterface: PTypeInfo): boolean; var i: integer; begin result := true; if (self<>nil) and (aInterface<>nil) and (fResolvers<>nil) then for i := 0 to length(fResolvers)-1 do if fResolvers[i].Implements(aInterface) then exit; result := false; end; procedure TInterfaceResolverInjected.InjectStub(const aStubsByGUID: array of TGUID); var i: integer; begin for i := 0 to high(aStubsByGUID) do InjectResolver([TInterfaceStub.Create(aStubsByGUID[i])]); end; procedure TInterfaceResolverInjected.InjectResolver( const aOtherResolvers: array of TInterfaceResolver; OwnOtherResolvers: boolean); var i: integer; begin for i := 0 to high(aOtherResolvers) do if aOtherResolvers[i]<>nil then begin if aOtherResolvers[i].InheritsFrom(TInterfaceStub) then begin include(TInterfaceStub(aOtherResolvers[i]).fOptions, imoFakeInstanceWontReleaseTInterfaceStub); ObjArrayAdd(fResolversToBeReleased,aOtherResolvers[i]); end else if OwnOtherResolvers then ObjArrayAdd(fResolversToBeReleased,aOtherResolvers[i]); ObjArrayAddOnce(fResolvers,aOtherResolvers[i]); end; end; procedure TInterfaceResolverInjected.InjectInstance( const aDependencies: array of TInterfacedObject); var i: integer; begin for i := 0 to high(aDependencies) do if aDependencies[i]<>nil then begin IInterface(aDependencies[i])._AddRef; // Destroy will do _Release ObjArrayAdd(fDependencies,aDependencies[i]); end; end; destructor TInterfaceResolverInjected.Destroy; var i: integer; begin try ObjArrayClear(fResolversToBeReleased); for i := 0 to length(fDependencies)-1 do IInterface(fDependencies[i])._Release; finally inherited Destroy; end; end; function TInterfaceResolverInjected.Resolve(aInterface: PTypeInfo; out Obj): boolean; begin if self=nil then result := false else result := TryResolve(aInterface,Obj); end; function TInterfaceResolverInjected.Resolve(const aGUID: TGUID; out Obj): boolean; var known: TInterfaceFactory; begin if self=nil then result := false else begin known := TInterfaceFactory.Get(aGUID); if known<>nil then result := Resolve(known.fInterfaceTypeInfo,Obj) else result := false; end; end; procedure TInterfaceResolverInjected.ResolveByPair( const aInterfaceObjPairs: array of pointer; aRaiseExceptionIfNotFound: boolean); var n,i: integer; begin n := length(aInterfaceObjPairs); if (n=0) or (n and 1=1) then raise EServiceException.CreateUTF8('%.Resolve([odd])',[self]); for i := 0 to (n shr 1)-1 do if not Resolve(aInterfaceObjPairs[i*2],aInterfaceObjPairs[i*2+1]^) then if aRaiseExceptionIfNotFound then raise EServiceException.CreateUTF8('%.ResolveByPair(%) unsatisfied', [self,PTypeInfo(aInterfaceObjPairs[i*2])^.Name]); end; procedure TInterfaceResolverInjected.Resolve(const aInterfaces: array of TGUID; const aObjs: array of pointer; aRaiseExceptionIfNotFound: boolean); var n,i: integer; info: PTypeInfo; begin n := length(aInterfaces); if (n=0) or (n<>length(aObjs)) then raise EServiceException.CreateUTF8('%.Resolve([?,?])',[self]); for i := 0 to n-1 do if PPointer(aObjs[i])^=nil then begin info := TInterfaceFactory.GUID2TypeInfo(aInterfaces[i]); if not Resolve(info,aObjs[i]^) then if aRaiseExceptionIfNotFound then raise EServiceException.CreateUTF8('%.Resolve(%) unsatisfied',[self,info^.Name]); end; end; { TInjectableObject } function TInjectableObject.TryResolve(aInterface: PTypeInfo; out Obj): boolean; begin if (self<>nil) and (aInterface<>nil) and (fResolver<>nil) then result := fResolver.TryResolve(aInterface,Obj) else result := false; end; procedure TInjectableObject.Resolve(aInterface: PTypeInfo; out Obj); begin if not TryResolve(aInterface,Obj) then raise EServiceException.CreateUTF8('%.Resolve(%) unsatisfied',[self,aInterface^.Name]); end; procedure TInjectableObject.Resolve(const aGUID: TGUID; out Obj); var info: PTypeInfo; begin info := TInterfaceFactory.GUID2TypeInfo(aGUID); if not TryResolve(info,Obj) then raise EServiceException.CreateUTF8( '%.Resolve(%): Interface not registered',[self,info^.Name]); end; procedure TInjectableObject.ResolveByPair(const aInterfaceObjPairs: array of pointer); begin if fResolver.InheritsFrom(TInterfaceResolverInjected) then TInterfaceResolverInjected(fResolver).ResolveByPair(aInterfaceObjPairs) else if high(aInterfaceObjPairs)=1 then Resolve(aInterfaceObjPairs[0],aInterfaceObjPairs[1]^) else raise EServiceException.CreateUTF8('%.ResolveByPair(?)',[self]); end; procedure TInjectableObject.Resolve(const aInterfaces: array of TGUID; const aObjs: array of pointer); begin if fResolver.InheritsFrom(TInterfaceResolverInjected) then TInterfaceResolverInjected(fResolver).Resolve(aInterfaces,aObjs) else if (high(aInterfaces)=0) and (high(aObjs)=0) then Resolve(aInterfaces[0],aObjs[0]^) else raise EServiceException.CreateUTF8('%.Resolve(?,?)',[self]); end; procedure TInjectableObject.AutoResolve(aRaiseEServiceExceptionIfNotFound: boolean); var i: integer; CT: TClass; P: PPropInfo; addr: pointer; begin if (self=nil) or (fResolver=nil) then raise EServiceException.CreateUTF8('%.AutoResolve with no prior registration',[self]); CT := ClassType; if CT<>TInjectableObject then repeat for i := 1 to InternalClassPropInfo(CT,P) do begin if P^.PropType^.Kind=tkInterface then if P^.GetterIsField then begin addr := P^.GetterAddr(self); if not TryResolve(P^.TypeInfo,addr^) then if aRaiseEServiceExceptionIfNotFound then raise EServiceException.CreateUTF8( '%.AutoResolve: impossible to resolve published property %: %', [self,P^.Name,P^.PropType^.Name]); end else raise EServiceException.CreateUTF8( '%.AutoResolve: published property %: % should directly read the field', [self,P^.Name,P^.PropType^.Name]); P := P^.Next; end; CT := GetClassParent(CT); until CT=TInjectableObject; end; constructor TInjectableObject.CreateInjected(const aStubsByGUID: array of TGUID; const aOtherResolvers: array of TInterfaceResolver; const aDependencies: array of TInterfacedObject; aRaiseEServiceExceptionIfNotFound: boolean); begin fResolver := TInterfaceResolverInjected.Create; fResolverOwned := true; TInterfaceResolverInjected(fResolver).InjectStub(aStubsByGUID); TInterfaceResolverInjected(fResolver).InjectResolver(aOtherResolvers); TInterfaceResolverInjected(fResolver).InjectInstance(aDependencies); Create; AutoResolve(aRaiseEServiceExceptionIfNotFound); end; constructor TInjectableObject.CreateWithResolver(aResolver: TInterfaceResolver; aRaiseEServiceExceptionIfNotFound: boolean); begin if fResolver<>nil then exit; // inject once! if aResolver=nil then raise EServiceException.CreateUTF8('%.CreateWithResolver(nil)',[self]); fResolver := aResolver; // may be needed by overriden Create Create; AutoResolve(aRaiseEServiceExceptionIfNotFound); end; destructor TInjectableObject.Destroy; begin inherited Destroy; CleanupInstance; // ensure creatures are released before their creator if fResolverOwned then FreeAndNil(fResolver); // let the creator move away end; { TInjectableObjectRest } constructor TInjectableObjectRest.CreateWithResolverAndRest( aResolver: TInterfaceResolver; aFactory: TServiceFactoryServer; aServer: TSQLRestServer; aRaiseEServiceExceptionIfNotFound: boolean); begin fFactory := aFactory; // may be needed by overriden Create fServer := aServer; CreateWithResolver(aResolver,aRaiseEServiceExceptionIfNotFound); end; { TServiceFactory } constructor TServiceFactory.Create(aRest: TSQLRest; aInterface: PTypeInfo; aInstanceCreation: TServiceInstanceImplementation; const aContractExpected: RawUTF8); begin // check supplied interface if (aRest=nil) or (aInterface=nil) then raise EServiceException.CreateUTF8('Invalid %.Create(%,%)',[self,aRest,aInterface]); inherited Create; fInterface := TInterfaceFactory.Get(aInterface); fRest := aRest; fInstanceCreation := aInstanceCreation; fInterfaceMangledURI := BinToBase64URI(@fInterface.fInterfaceIID,SizeOf(TGUID)); fInterfaceURI := ToUTF8(aInterface^.Name); if fInterfaceURI[1] in ['I','i'] then delete(fInterfaceURI,1,1); if fRest.Model.GetTableIndex(fInterfaceURI)>=0 then raise EServiceException.CreateUTF8('%.Create: I% routing name is '+ 'already used by a % SQL table name',[self,fInterfaceURI,fInterfaceURI]); SetLength(fExecution,fInterface.fMethodsCount); // compute interface signature (aka "contract"), serialized as a JSON object FormatUTF8('{"contract":"%","implementation":"%","methods":%}', [InterfaceURI,LowerCase(TrimLeftLowerCaseShort(ToText(InstanceCreation))), fInterface.fContract],fContract); fContractHash := '"'+CardinalToHex(Hash32(fContract))+ CardinalToHex(CRC32string(fContract))+'"'; // 2 hashes to avoid collision if aContractExpected<>'' then // override default contract if aContractExpected[1]<>'"' then // stored as JSON string fContractExpected := '"'+aContractExpected+'"' else fContractExpected := aContractExpected else fContractExpected := fContractHash; // for security end; function TServiceFactory.GetInterfaceTypeInfo: PTypeInfo; begin if (Self<>nil) and (fInterface<>nil) then result := fInterface.fInterfaceTypeInfo else result := nil; end; function TServiceFactory.GetInterfaceIID: TGUID; begin result := fInterface.fInterfaceIID; end; procedure TServiceFactory.ExecutionAction(const aMethod: array of RawUTF8; aOptions: TServiceMethodOptions; aAction: TServiceMethodOptionsAction); procedure SetAction(var exec: TServiceFactoryExecution); begin case aAction of moaReplace: exec.Options := aOptions; moaInclude: exec.Options := exec.Options + aOptions; moaExclude: exec.Options := exec.Options - aOptions; end; end; var i,m: integer; begin if high(aMethod)<0 then for i := 0 to fInterface.fMethodsCount-1 do SetAction(fExecution[i]) else for m := 0 to high(aMethod) do SetAction(fExecution[fInterface.CheckMethodIndex(aMethod[m])]); fAnyOptions := []; for i := 0 to fInterface.fMethodsCount-1 do fAnyOptions := fAnyOptions+fExecution[i].Options; end; { TServiceContainerServer } function TServiceContainerServer.AddImplementation( aImplementationClass: TInterfacedClass; const aInterfaces: array of PTypeInfo; aInstanceCreation: TServiceInstanceImplementation; aSharedImplementation: TInterfacedObject; const aContractExpected: RawUTF8): TServiceFactoryServer; var C: TClass; T: PInterfaceTable; i, j: PtrInt; UID: array of PGUID; F: TServiceFactoryServer; begin result := nil; // check input parameters if (self=nil) or (aImplementationClass=nil) or (high(aInterfaces)<0) then exit; if aSharedImplementation<>nil then if (aSharedImplementation.ClassType<>aImplementationClass) or (aInstanceCreation<>sicShared) then raise EServiceException.CreateUTF8('%.AddImplementation: invalid % class', [self,aSharedImplementation]); CheckInterface(aInterfaces); SetLength(UID,length(aInterfaces)); for j := 0 to high(aInterfaces) do UID[j] := pointer(aInterfaces[j]^.InterfaceGUID); // check all interfaces available in aSharedImplementation/aImplementationClass if (aSharedImplementation<>nil) and aSharedImplementation.InheritsFrom(TInterfacedObjectFake) then begin if IsEqualGUID(UID[0],@TInterfacedObjectFake(aSharedImplementation). fFactory.fInterfaceIID) then UID[0] := nil; // mark TGUID implemented by this fake interface end else begin C := aImplementationClass; // search all implemented TGUID for this class repeat T := C.GetInterfaceTable; if T<>nil then for i := 0 to T^.EntryCount-1 do with T^.Entries[i] do for j := 0 to high(aInterfaces) do if (UID[j]<>nil) and IsEqualGUID(UID[j],{$ifdef FPC}IID{$else}@IID{$endif}) then begin UID[j] := nil; // mark TGUID found break; end; C := GetClassParent(C); until C=nil; end; for j := 0 to high(aInterfaces) do if UID[j]<>nil then raise EServiceException.CreateUTF8('%.AddImplementation: % not found in %', [self,aInterfaces[j]^.Name,aImplementationClass]); // register this implementation class for j := 0 to high(aInterfaces) do begin F := TServiceFactoryServer.Create(Rest as TSQLRestServer,aInterfaces[j], aInstanceCreation,aImplementationClass,aContractExpected, fSessionTimeout,aSharedImplementation); if result=nil then begin result := F; // returns the first registered interface if (aInstanceCreation=sicShared) and (aSharedImplementation=nil) then aSharedImplementation := F.fSharedInstance; // re-use existing instance end; AddServiceInternal(F); end; end; procedure TServiceContainerServer.OnCloseSession(aSessionID: cardinal); var i,j: Integer; P: ^TServiceFactoryServerInstance; fact: TServiceFactoryServer; inst: TServiceFactoryServerInstance; begin for i := 0 to high(fInterface) do begin fact := TServiceFactoryServer(fInterface[i].Service); if fact.fInstanceCount>0 then case fact.InstanceCreation of sicPerSession: begin inst.InstanceID := aSessionID; fact.InternalInstanceRetrieve(inst,ord(imFree),aSessionID); end; sicClientDriven: begin // release ASAP if was not notified by client EnterCriticalSection(fact.fInstanceLock); try P := pointer(fact.fInstances); for j := 1 to fact.fInstanceCapacity do begin if P^.Session=aSessionID then P^.SafeFreeInstance(fact); inc(P); end; finally LeaveCriticalSection(fact.fInstanceLock); end; end; end; end; end; constructor TServiceContainerServer.Create(aRest: TSQLRest); begin inherited Create(aRest); fSessionTimeout := 30*60; // 30 minutes by default end; destructor TServiceContainerServer.Destroy; var i: integer; begin if fFakeCallbacks<>nil then begin for i := 0 to fFakeCallbacks.Count-1 do // prevent GPF in Destroy TInterfacedObjectFakeServer(fFakeCallbacks.List[i]).fServer := nil; FreeAndNil(fFakeCallbacks); // do not own objects end; fRecordVersionCallback := nil; // to be done after fFakeCallbacks[].fServer := nil inherited Destroy; end; procedure TServiceContainerServer.FakeCallbackAdd(aFakeInstance: TObject); begin if self=nil then exit; if fFakeCallbacks=nil then fFakeCallbacks := TSynObjectListLocked.Create(false); fFakeCallbacks.Add(aFakeInstance); end; procedure TServiceContainerServer.FakeCallbackRemove(aFakeInstance: TObject); var i,callbackID: integer; connectionID: Int64; fake: TInterfacedObjectFakeServer; server: TSQLRestServer; begin if (self=nil) or (fFakeCallbacks=nil) then exit; connectionID := 0; callbackID := 0; fFakeCallbacks.Safe.Lock; try i := fFakeCallbacks.IndexOf(aFakeInstance); if i>=0 then begin fake := fFakeCallbacks.List[i]; if not fake.fReleasedOnClientSide then begin connectionID := fake.fLowLevelConnectionID; callbackID := fake.ClientDrivenID; if Assigned(OnCallbackReleasedOnServerSide) then OnCallbackReleasedOnServerSide(self,fake,fake.fFakeInterface); end; fFakeCallbacks.Delete(i); end; finally fFakeCallbacks.Safe.UnLock; end; if connectionID<>0 then begin server := fRest as TSQLRestServer; if Assigned(server.OnNotifyCallback) then server.OnNotifyCallback(server,SERVICE_PSEUDO_METHOD[imFree],'', connectionID,callbackID,nil,nil); end; end; procedure TServiceContainerServer.FakeCallbackRelease(Ctxt: TSQLRestServerURIContext); var i: integer; fake: TInterfacedObjectFakeServer; connectionID: Int64; fakeID: PtrUInt; Values: TNameValuePUTF8CharDynArray; withLog: boolean; // avoid stack overflow begin if (self=nil) or (fFakeCallbacks=nil) or (Ctxt=nil) then exit; connectionID := Ctxt.Call^.LowLevelConnectionID; JSONDecode(pointer(Ctxt.Call^.InBody),Values); if length(Values)<>1 then exit; fakeID := GetCardinal(Values[0].Value); if (fakeID=0) or (connectionID=0) or (Values[0].Name=nil) then exit; withLog := not IdemPropNameU('ISynLogCallback',Values[0].Name,Values[0].NameLen); if withLog then // avoid stack overflow ;) fRest.InternalLog('%.FakeCallbackRelease(%,"%") remote call', [ClassType,fakeID,Values[0].Name],sllDebug); try fFakeCallbacks.Safe.Lock; for i := 0 to fFakeCallbacks.Count-1 do begin fake := fFakeCallbacks.List[i]; if (fake.fLowLevelConnectionID=connectionID) and (fake.ClientDrivenID=fakeID) then begin fake.fReleasedOnClientSide := true; if Assigned(OnCallbackReleasedOnClientSide) then OnCallbackReleasedOnClientSide(self,fake,fake.fFakeInterface); if fake.fService.fInterface.MethodIndexCallbackReleased>=0 then begin // emulate a call to CallbackReleased(callback,'ICallbackName') Ctxt.ServiceMethodIndex := fake.fService.fInterface.MethodIndexCallbackReleased; Ctxt.ServiceMethod := @fake.fService.fInterface.fMethods[Ctxt.ServiceMethodIndex]; Ctxt.ServiceExecution := @fake.fService.fExecution[Ctxt.ServiceMethodIndex]; Ctxt.ServiceExecutionOptions := Ctxt.ServiceExecution.Options; Ctxt.Service := fake.fService; inc(Ctxt.ServiceMethodIndex,SERVICE_PSEUDO_METHOD_COUNT); fake._AddRef; // IInvokable=pointer in Ctxt.ExecuteCallback Ctxt.ServiceParameters := pointer(FormatUTF8('[%,"%"]', [PtrInt(PtrUInt(fake.fFakeInterface)),Values[0].Name])); fake.fService.ExecuteMethod(Ctxt); if withLog then fRest.InternalLog('I%() returned %',[PServiceMethod(Ctxt.ServiceMethod)^. InterfaceDotMethodName, Ctxt.Call^.OutStatus],sllDebug); end else Ctxt.Success; exit; end; end; finally fFakeCallbacks.Safe.UnLock; end; end; function TServiceContainerServer.RecordVersionSynchronizeSubscribeMaster( TableIndex: integer; RecordVersion: TRecordVersion; const SlaveCallback: IServiceRecordVersionCallback): boolean; var instance: TObject; begin result := false; if (self=nil) or (cardinal(TableIndex)>cardinal(fRest.Model.TablesMax)) then exit; fRest.fAcquireExecution[execORMWrite].fSafe.Lock; try if RecordVersion<>(fRest as TSQLRestServer).fRecordVersionMax then exit; // there are some missing items on the client side if fRecordVersionCallback=nil then SetLength(fRecordVersionCallback,fRest.Model.TablesMax+1); InterfaceArrayAdd(fRecordVersionCallback[TableIndex],SlaveCallback); instance := ObjectFromInterface(SlaveCallback); if (instance<>nil) and (instance.ClassType=TInterfacedObjectFakeServer) then TInterfacedObjectFakeServer(instance).fRaiseExceptionOnInvokeError := True; finally fRest.fAcquireExecution[execORMWrite].Safe.UnLock; end; result := true; end; class function TServiceContainerServer.CallbackReleasedOnClientSide( const callback: IInterface; callbacktext: PShortString): boolean; procedure Append(var dest: shortstring; const source: shortstring); var d,s: integer; begin d := ord(dest[0]); s := ord(source[0]); if d+s<254 then begin dest[d+1] := ' '; MoveFast(source[1],dest[d+2],s); inc(dest[0],s+1); end; end; var instance: TObject; begin instance := ObjectFromInterface(callback); if instance=nil then result := false else begin if callbacktext<>nil then Append(callbacktext^,ClassNameShort(instance)^); result := (instance.ClassType=TInterfacedObjectFakeServer) and TInterfacedObjectFakeServer(instance).fReleasedOnClientSide; end; end; procedure TServiceContainerServer.RecordVersionCallbackNotify(TableIndex: integer; Occasion: TSQLOccasion; const DeletedID: TID; const DeletedRevision: TRecordVersion; const AddUpdateJson: RawUTF8); var i: integer; arr: ^IServiceRecordVersionCallbackDynArray; begin try fRest.fAcquireExecution[execORMWrite].fSafe.Lock; try arr := @fRecordVersionCallback[TableIndex]; for i := length(arr^)-1 downto 0 do // downto: InterfaceArrayDelete() below if CallbackReleasedOnClientSide(arr^[i]) then // automatic removal of any released callback InterfaceArrayDelete(arr^,i) else try case Occasion of soInsert: arr^[i].Added(AddUpdateJson); soUpdate: arr^[i].Updated(AddUpdateJson); soDelete: arr^[i].Deleted(DeletedID,DeletedRevision); end; except // on notification error -> delete this entry InterfaceArrayDelete(arr^,i); end; finally fRest.fAcquireExecution[execORMWrite].Safe.UnLock; end; except // ignore any exception here end; end; procedure TServiceContainerServer.RecordVersionNotifyAddUpdate( Occasion: TSQLOccasion; TableIndex: integer; const Document: TDocVariantData); var json: RawUTF8; begin if (Occasion in [soInsert,soUpdate]) and (fRecordVersionCallback<>nil) and (fRecordVersionCallback[TableIndex]<>nil) then begin json := Document.ToJSON; RecordVersionCallbackNotify(TableIndex,Occasion,0,0,json); end; end; procedure TServiceContainerServer.RecordVersionNotifyAddUpdate( Occasion: TSQLOccasion; TableIndex: integer; const Decoder: TJSONObjectDecoder); var json: RawUTF8; begin if (Occasion in [soInsert,soUpdate]) and (fRecordVersionCallback<>nil) and (fRecordVersionCallback[TableIndex]<>nil) then begin Decoder.EncodeAsJSON(json); RecordVersionCallbackNotify(TableIndex,Occasion,0,0,json); end; end; procedure TServiceContainerServer.RecordVersionNotifyDelete( TableIndex: integer; const ID: TID; const Revision: TRecordVersion); begin if (fRecordVersionCallback<>nil) and (fRecordVersionCallback[TableIndex]<>nil) then RecordVersionCallbackNotify(TableIndex,soDelete,ID,Revision,''); end; procedure TServiceContainerServer.SetServiceLog(aLogRest: TSQLRest; aLogClass: TSQLRecordServiceLogClass; const aExcludedMethodNamesCSV: RawUTF8); var i,n: integer; fact: TServiceFactory; excluded: TServiceContainerInterfaceMethodBits; methods: TInterfaceFactoryMethodBits; somemethods: boolean; begin somemethods := aExcludedMethodNamesCSV<>''; if somemethods then SetInterfaceMethodBits(pointer(aExcludedMethodNamesCSV),true,excluded) else FillcharFast(methods,SizeOf(methods),255); n := length(fInterfaceMethod); i := 0; while i'') and not (i in excluded) then begin include(methods,fInterfaceMethod[i].InterfaceMethodIndex-SERVICE_PSEUDO_METHOD_COUNT); somemethods := true; end; inc(i); until (i>=n) or (fInterfaceMethod[i].InterfaceService<>fact); if (aExcludedMethodNamesCSV='') or somemethods then TServiceFactoryServer(fact).SetServiceLogByIndex(methods,aLogRest,aLogClass); end; end; { TServiceFactoryServer } type PCallMethodArgs = ^TCallMethodArgs; {$ifdef FPC} {$push} {$PACKRECORDS 16} {$endif} TCallMethodArgs = record StackSize: PtrInt; StackAddr, method: PtrInt; ParamRegs: array[PARAMREG_FIRST..PARAMREG_LAST] of PtrInt; {$ifdef HAS_FPREG} FPRegs: array[FPREG_FIRST..FPREG_LAST] of Double; {$endif} res64: Int64Rec; resKind: TServiceMethodValueType; end; {$ifdef FPC} {$pop} {$endif} // ARM/AARCH64 code below provided by ALF, greatly inspired by pascalscript {$ifdef CPUARM} procedure CallMethod(var Args: TCallMethodArgs); assembler; nostackframe; label stack_loop,load_regs,asmcall_end,float_result; asm //name r#(normally, darwin can differ) //a1 0 argument 1 / integer result / scratch register //a2 1 argument 2 / scratch register //a3 2 argument 3 / scratch register //a4 3 argument 4 / scratch register //v1 4 register variable //v2 5 register variable //v3 6 register variable //v4 7 register variable //v5 8 register variable //sb 9 static base / register variable //sl 10 stack limit / stack chunk handle / reg. variable //fp 11 frame pointer //ip 12 scratch register / new-sb in inter-link-unit calls //sp 13 lower end of current stack frame //lr 14 link address / scratch register //pc 15 program counter // sometimes, the entry-point is not exact ... give some room for errors nop nop // prolog mov ip, sp // sp is the stack pointer ; ip is the Intra-Procedure-call scratch register stmfd sp!, {v1, v2, sb, sl, fp, ip, lr, pc} sub fp, ip, #4 // make space on stack sub sp, sp, #MAX_EXECSTACK mov v2, Args // copy (push) stack content (if any) ldr a1, [v2,#TCallMethodArgs.StackSize] // if there is no stack content, do nothing cmp a1, #0 beq load_regs // point a2 to bottom of stack. mov a2, sp // load a3 with CallMethod stack address ldr a3, [v2,#TCallMethodArgs.StackAddr] stack_loop: // copy a3 to a4 and increment a3 (a3 = StackAddr) ldmia a3!, {a4} // copy a4 to a2 and increment a2 (a2 = StackPointer) stmia a2!, {a4} // decrement stacksize counter, with update of flags for loop subs a1, a1, #1 bne stack_loop load_regs: ldr r0, [v2,#TCallMethodArgs.ParamRegs+REGR0*4-4] ldr r1, [v2,#TCallMethodArgs.ParamRegs+REGR1*4-4] ldr r2, [v2,#TCallMethodArgs.ParamRegs+REGR2*4-4] ldr r3, [v2,#TCallMethodArgs.ParamRegs+REGR3*4-4] vldr d0, [v2,#TCallMethodArgs.FPRegs+REGD0*8-8] vldr d1, [v2,#TCallMethodArgs.FPRegs+REGD1*8-8] vldr d2, [v2,#TCallMethodArgs.FPRegs+REGD2*8-8] vldr d3, [v2,#TCallMethodArgs.FPRegs+REGD3*8-8] vldr d4, [v2,#TCallMethodArgs.FPRegs+REGD4*8-8] vldr d5, [v2,#TCallMethodArgs.FPRegs+REGD5*8-8] vldr d6, [v2,#TCallMethodArgs.FPRegs+REGD6*8-8] vldr d7, [v2,#TCallMethodArgs.FPRegs+REGD7*8-8] ldr v1, [v2,#TCallMethodArgs.method] {$ifdef CPUARM_HAS_BLX} blx v1 {$else} mov lr, pc {$ifdef CPUARM_HAS_BX} bx v1 {$else} mov pc, v1 {$endif} {$endif} str a1, [v2,#TCallMethodArgs.res64.Lo] str a2, [v2,#TCallMethodArgs.res64.Hi] ldr a3, [v2,#TCallMethodArgs.resKind] cmp a3, smvDouble beq float_result cmp a3, smvDateTime beq float_result cmp a3, smvCurrency bne asmcall_end // store double result in res64 float_result: vstr d0, [v2,#TCallMethodArgs.res64] asmcall_end: // epilog ldmea fp, {v1, v2, sb, sl, fp, sp, pc} end; {$endif CPUARM} {$ifdef CPUAARCH64} procedure CallMethod(var Args: TCallMethodArgs); assembler; nostackframe; label stack_loop,load_regs,asmcall_end,float_result; asm // inspired by pascal script // fp x29 // lr x30 // sp sp // sometimes, the entry-point is not exact ... give some room for errors nop nop // prolog stp x29, x30, [sp, #-16]! mov x29, sp stp x19, x19, [sp, #-16]! // make space on stack sub sp, sp, #MAX_EXECSTACK //and sp, sp, #-16 // Always align sp. mov x19, Args // prepare to copy (push) stack content (if any) ldr x2, [x19,#TCallMethodArgs.StackSize] // if there is no stack content, do nothing cmp x2, #0 b.eq load_regs // point x3 to bottom of stack. mov x3, sp // load x4 with CallMethod stack address ldr x4, [x19,#TCallMethodArgs.StackAddr] stack_loop: // load x5 and x6 with stack contents ldr x5, [x4] ldr x6, [x4,#8] // store contents at "real" stack and increment address counter x3 stp x5, x6, [x3], #16 // with update of flags for loop // (mandatory: stacksize must be a multiple of 2 [16 bytes] !!) // inc stackaddr counter by 16 (2 registers are pushed every loop) add x4, x4, #16 // decrement stacksize counter by 2 (2 registers are pushed every loop), // with update of flags for loop subs x2, x2, #2 b.ne stack_loop load_regs: ldr x0, [x19,#TCallMethodArgs.ParamRegs+REGX0*8-8] ldr x1, [x19,#TCallMethodArgs.ParamRegs+REGX1*8-8] ldr x2, [x19,#TCallMethodArgs.ParamRegs+REGX2*8-8] ldr x3, [x19,#TCallMethodArgs.ParamRegs+REGX3*8-8] ldr x4, [x19,#TCallMethodArgs.ParamRegs+REGX4*8-8] ldr x5, [x19,#TCallMethodArgs.ParamRegs+REGX5*8-8] ldr x6, [x19,#TCallMethodArgs.ParamRegs+REGX6*8-8] ldr x7, [x19,#TCallMethodArgs.ParamRegs+REGX7*8-8] ldr d0, [x19,#TCallMethodArgs.FPRegs+REGD0*8-8] ldr d1, [x19,#TCallMethodArgs.FPRegs+REGD1*8-8] ldr d2, [x19,#TCallMethodArgs.FPRegs+REGD2*8-8] ldr d3, [x19,#TCallMethodArgs.FPRegs+REGD3*8-8] ldr d4, [x19,#TCallMethodArgs.FPRegs+REGD4*8-8] ldr d5, [x19,#TCallMethodArgs.FPRegs+REGD5*8-8] ldr d6, [x19,#TCallMethodArgs.FPRegs+REGD6*8-8] ldr d7, [x19,#TCallMethodArgs.FPRegs+REGD7*8-8] // call TCallMethodArgs.method ldr x15, [x19,#TCallMethodArgs.method] blr x15 // store normal result str x0, [x19, #TCallMethodArgs.res64] ldr x15, [x19, #TCallMethodArgs.resKind] cmp x15, smvDouble b.eq float_result cmp x15, smvDateTime b.eq float_result cmp x15, smvCurrency b.ne asmcall_end // store double result in res64 float_result: str d0, [x19,#TCallMethodArgs.res64] asmcall_end: add sp, sp, #MAX_EXECSTACK ldr x19,[sp], #16 ldp x29,x30,[sp], #16 ret end; {$endif CPUAARCH64} {$ifdef CPUX64} procedure CallMethod(var Args: TCallMethodArgs); assembler; {$ifdef FPC} nostackframe; asm push rbp push r12 mov rbp, rsp // simulate .params 60 ... size for 60 parameters lea rsp, [rsp - MAX_EXECSTACK] // align stack to 16 bytes and rsp, -16 {$else DELPHI} // ensure we use regular .params command for easier debugging asm .params 64 // size for 64 parameters .pushnv r12 // generate prolog+epilog to save and restore non-volatile r12 {$endif FPC} // get Args mov r12, Args // copy (push) stack content (if any) mov rcx, [r12].TCallMethodArgs.StackSize mov rdx, [r12].TCallMethodArgs.StackAddr jmp @checkstack @addstack: dec ecx push qword ptr[rdx] sub rdx, 8 @checkstack: test ecx, ecx jnz @addstack // fill registers and call method {$ifdef LINUX} // Linux/BSD System V AMD64 ABI mov rdi, [r12 + TCallMethodArgs.ParamRegs + REGRDI * 8 - 8] mov rsi, [r12 + TCallMethodArgs.ParamRegs + REGRSI * 8 - 8] mov rdx, [r12 + TCallMethodArgs.ParamRegs + REGRDX * 8 - 8] mov rcx, [r12 + TCallMethodArgs.ParamRegs + REGRCX * 8 - 8] mov r8, [r12 + TCallMethodArgs.ParamRegs + REGR8 * 8 - 8] mov r9, [r12 + TCallMethodArgs.ParamRegs + REGR9 * 8 - 8] movsd xmm0, qword ptr[r12 + TCallMethodArgs.FPRegs + REGXMM0 * 8 - 8] movsd xmm1, qword ptr[r12 + TCallMethodArgs.FPRegs + REGXMM1 * 8 - 8] movsd xmm2, qword ptr[r12 + TCallMethodArgs.FPRegs + REGXMM2 * 8 - 8] movsd xmm3, qword ptr[r12 + TCallMethodArgs.FPRegs + REGXMM3 * 8 - 8] movsd xmm4, qword ptr[r12 + TCallMethodArgs.FPRegs + REGXMM4 * 8 - 8] movsd xmm5, qword ptr[r12 + TCallMethodArgs.FPRegs + REGXMM5 * 8 - 8] movsd xmm6, qword ptr[r12 + TCallMethodArgs.FPRegs + REGXMM6 * 8 - 8] movsd xmm7, qword ptr[r12 + TCallMethodArgs.FPRegs + REGXMM7 * 8 - 8] call [r12].TCallMethodArgs.method {$else} // Win64 ABI mov rcx, [r12 + TCallMethodArgs.ParamRegs + REGRCX * 8 - 8] mov rdx, [r12 + TCallMethodArgs.ParamRegs + REGRDX * 8 - 8] mov r8, [r12 + TCallMethodArgs.ParamRegs + REGR8 * 8 - 8] mov r9, [r12 + TCallMethodArgs.ParamRegs + REGR9 * 8 - 8] movsd xmm0, qword ptr[r12 + TCallMethodArgs.FPRegs + REGXMM0 * 8 - 8] movsd xmm1, qword ptr[r12 + TCallMethodArgs.FPRegs + REGXMM1 * 8 - 8] movsd xmm2, qword ptr[r12 + TCallMethodArgs.FPRegs + REGXMM2 * 8 - 8] movsd xmm3, qword ptr[r12 + TCallMethodArgs.FPRegs + REGXMM3 * 8 - 8] sub rsp, 8 * 4 // reserve shadow-space for RCX,RDX,R8,R9 registers call [r12].TCallMethodArgs.method add rsp, 8 * 4 {$endif LINUX} // retrieve result mov [r12].TCallMethodArgs.res64, rax mov cl, [r12].TCallMethodArgs.resKind cmp cl, smvDouble je @d cmp cl, smvDateTime je @d cmp cl, smvCurrency jne @e @d: movlpd qword ptr[r12].TCallMethodArgs.res64, xmm0 // movlpd to ignore upper 64-bit of 128-bit xmm0 reg @e: {$ifdef FPC} mov rsp, rbp pop r12 pop rbp {$endif} end; {$endif CPUX64} {$ifdef CPUX86} procedure CallMethod(var Args: TCallMethodArgs); {$ifdef FPC}nostackframe; assembler;{$endif} asm push esi push ebp push eax // keep stack aligned on 16 bytes - required on DARWIN mov ebp, esp mov esi, Args // copy stack content (if any) mov eax, [esi].TCallMethodArgs.StackSize mov edx, dword ptr[esi].TCallMethodArgs.StackAddr add edx, eax // pascal/register convention = left-to-right shr eax, 2 jz @z @n: sub edx, 4 mov ecx, [edx] push ecx dec eax jnz @n // before a call instruction, esp should be divisible by 16: // mandatory on Darwin, and also on Linux i386 as stated by Florian in // https://www.mail-archive.com/fpc-devel@lists.freepascal.org/msg38885.html @z: mov eax, [esi + TCallMethodArgs.ParamRegs + REGEAX * 4 - 4] mov edx, [esi + TCallMethodArgs.ParamRegs + REGEDX * 4 - 4] mov ecx, [esi + TCallMethodArgs.ParamRegs + REGECX * 4 - 4] call [esi].TCallMethodArgs.method // retrieve result mov cl, [esi].TCallMethodArgs.resKind cmp cl, smvDouble je @d cmp cl, smvDateTime je @d cmp cl, smvCurrency jne @i fistp qword [esi].TCallMethodArgs.res64 jmp @e @d: fstp qword [esi].TCallMethodArgs.res64 jmp @e @i: mov [esi].TCallMethodArgs.res64.Lo, eax mov [esi].TCallMethodArgs.res64.Hi, edx @e: mov esp, ebp pop eax pop ebp pop esi end; {$endif CPUX86} {$ifdef ISDELPHI7ANDUP}{$WARN COMPARING_SIGNED_UNSIGNED ON}{$endif} procedure BackgroundExecuteProc(Call: pointer); var synch: PBackgroundLauncher absolute Call; threadContext: PServiceRunningContext; backup: TServiceRunningContext; begin threadContext := @ServiceContext; // faster to use a pointer than GetTls() backup := threadContext^; threadContext^.Factory := synch^.Context^.Factory; threadContext^.Request := synch^.Context^.Request; try case synch^.Action of doCallMethod: CallMethod(PCallMethodArgs(synch^.CallMethodArgs)^); doInstanceRelease: synch^.Instance.InternalRelease; doThreadMethod: synch^.ThreadMethod; end; finally threadContext^ := backup; end; end; constructor TServiceFactoryServer.Create(aRestServer: TSQLRestServer; aInterface: PTypeInfo; aInstanceCreation: TServiceInstanceImplementation; aImplementationClass: TInterfacedClass; const aContractExpected: RawUTF8; aTimeOutSec: cardinal; aSharedInstance: TInterfacedObject); begin // extract RTTI from the interface InitializeCriticalSection(fInstanceLock); inherited Create(aRestServer,aInterface,aInstanceCreation,aContractExpected); if fRest.MethodAddress(ShortString(InterfaceURI))<>nil then raise EServiceException.CreateUTF8('%.Create: I% already exposed as % published method', [self,InterfaceURI,fRest]) else fImplementationClass := aImplementationClass; if fImplementationClass.InheritsFrom(TInterfacedObjectFake) then begin fImplementationClassKind := ickFake; if aSharedInstance=nil then raise EServiceException.CreateUTF8('%.Create: no Shared Instance for %/I%', [self,fImplementationClass,fInterfaceURI]); if (aSharedInstance as TInterfacedObjectFake).Factory.fInterfaceTypeInfo<>aInterface then raise EServiceException.CreateUTF8('%.Create: shared % instance does not implement I%', [self,fImplementationClass,fInterfaceURI]) else end else begin if aRestServer.Services.Implements(fInterface.fInterfaceTypeInfo) then fImplementationClassKind := ickFromInjectedResolver else if fImplementationClass.InheritsFrom(TInjectableObjectRest) then fImplementationClassKind := ickInjectableRest else if fImplementationClass.InheritsFrom(TInjectableObject) then fImplementationClassKind := ickInjectable else if fImplementationClass.InheritsFrom(TInterfacedObjectWithCustomCreate) then fImplementationClassKind := ickWithCustomCreate; fImplementationClassInterfaceEntry := fImplementationClass.GetInterfaceEntry(fInterface.fInterfaceIID); if fImplementationClassInterfaceEntry=nil then raise EServiceException.CreateUTF8('%.Create: % does not implement I%', [self,fImplementationClass,fInterfaceURI]) else end; if (fInterface.MethodIndexCallbackReleased>=0) and (InstanceCreation<>sicShared) then raise EServiceException.CreateUTF8('%.Create: I%() should be run as sicShared', [self,fInterface.fMethods[fInterface.MethodIndexCallbackReleased]. InterfaceDotMethodName]); // initialize the shared instance or client driven parameters case InstanceCreation of sicShared: begin if aSharedInstance=nil then fSharedInstance := CreateInstance(false) else if aSharedInstance.InheritsFrom(fImplementationClass) then fSharedInstance := aSharedInstance else raise EServiceException.CreateUTF8('%.Create: % shared instance '+ 'does not inherit from %',[self,aSharedInstance,fImplementationClass]); if fImplementationClassKind<>ickFake then if (fSharedInstance=nil) or not GetInterfaceFromEntry( fSharedInstance,fImplementationClassInterfaceEntry,fSharedInterface) then raise EServiceException.CreateUTF8('%.Create: % is no implementation of I%', [self,fSharedInstance,fInterfaceURI]); end; sicClientDriven, sicPerSession, sicPerUser, sicPerGroup, sicPerThread: if (aTimeOutSec=0) and (InstanceCreation<>sicPerThread) then fInstanceCreation := sicSingle else begin // only instances list is protected, since client calls shall be pipelined fInstance.InitSpecific(TypeInfo(TServiceFactoryServerInstanceDynArray), fInstances,djCardinal,@fInstanceCapacity); // sort by InstanceID: cardinal fInstanceTimeOut := aTimeOutSec*1000; end; end; SetLength(fStats,fInterface.MethodsCount); end; procedure TServiceFactoryServer.SetTimeoutSecInt(value: cardinal); begin if (self=nil) or not (InstanceCreation in [ sicClientDriven,sicPerSession,sicPerUser,sicPerGroup,sicPerThread]) then raise EServiceException.CreateUTF8('%.SetTimeoutSecInt() with %', [self,ToText(InstanceCreation)^]); fInstanceTimeOut := value*1000; end; function TServiceFactoryServer.GetTimeoutSec: cardinal; begin if (self=nil) or not (InstanceCreation in [ sicClientDriven,sicPerSession,sicPerUser,sicPerGroup,sicPerThread]) then result := 0 else result := fInstanceTimeout div 1000; end; function TServiceFactoryServer.GetStat(const aMethod: RawUTF8): TSynMonitorInputOutput; begin result := fStats[fInterface.CheckMethodIndex(aMethod)]; end; destructor TServiceFactoryServer.Destroy; var i: integer; begin if fInstanceCount>0 then Rest.InternalLog('%.Destroy for I% %: fInstanceCount=%',[ClassType,fInterfaceURI, ToText(InstanceCreation)^,fInstanceCount],sllDebug); try EnterCriticalSection(fInstanceLock); try // release any internal instance (should have been done by client) try for i := 0 to fInstanceCapacity-1 do if fInstances[i].Instance<>nil then fInstances[i].SafeFreeInstance(self); finally {$ifndef LVCL} FreeAndNil(fBackgroundThread); {$endif} end; except ; // better ignore any error in business logic code end; finally LeaveCriticalSection(fInstanceLock); end; DeleteCriticalSection(fInstanceLock); ObjArrayClear(fStats,true); inherited Destroy; end; function TServiceFactoryServer.Get(out Obj): Boolean; var Inst: TServiceFactoryServerInstance; begin result := false; if self=nil then exit; case fInstanceCreation of sicShared: if fSharedInterface<>nil then begin IInterface(Obj) := fSharedInterface; // copy implementation interface result := true; end; sicPerThread: begin Inst.Instance := nil; Inst.InstanceID := PtrUInt(GetCurrentThreadId); if (InternalInstanceRetrieve(Inst,SERVICE_PSEUDO_METHOD_COUNT,0)>=0) and (Inst.Instance<>nil) then result := GetInterfaceFromEntry(Inst.Instance,fImplementationClassInterfaceEntry,Obj); end; else begin // no user/group/session on pure server-side -> always sicSingle Inst.Instance := CreateInstance(false); if Inst.Instance<>nil then result := GetInterfaceFromEntry(Inst.Instance,fImplementationClassInterfaceEntry,Obj); end; end; if result then with PServiceRunningContext(@ServiceContext)^ do if Factory=nil then Factory := self; end; function TServiceFactoryServer.RetrieveSignature: RawUTF8; begin if self=nil then result := '' else result := Contract; // just return the current value end; function TServiceFactoryServer.RenewSession(aSession: cardinal): integer; var tix: Int64; i: integer; P: ^TServiceFactoryServerInstance; begin result := 0; if (self=nil) or (fInstanceCount=0) or (aSession<=CONST_AUTHENTICATION_NOT_USED) or not(fInstanceCreation in [sicClientDriven,sicPerSession]) then exit; tix := GetTickCount64; EnterCriticalSection(fInstanceLock); try P := pointer(fInstances); for i := 1 to fInstanceCapacity do begin if P^.Session=aSession then begin P^.LastAccess64 := tix; inc(result); end; inc(P); end; finally LeaveCriticalSection(fInstanceLock); end; end; function TServiceFactoryServer.RunOnAllInstances(const aEvent: TOnServiceFactoryServerOne; var aOpaque): integer; var i: integer; P: ^TServiceFactoryServerInstance; begin result := 0; if (self = nil) or not Assigned(aEvent) or (fInstanceCount=0) then exit; EnterCriticalSection(fInstanceLock); try P := pointer(fInstances); for i := 1 to fInstanceCapacity do begin if (P^.InstanceID<>0) and (P^.Instance<>nil) then inc(result,aEvent(self,P^,aOpaque)); inc(P); end; finally LeaveCriticalSection(fInstanceLock); end; end; procedure TServiceFactoryServerInstance.SafeFreeInstance(Factory: TServiceFactoryServer); var Obj: TInterfacedObject; begin if Instance=nil then exit; // nothing to release dec(Factory.fInstanceCount); InstanceID := 0; Session := 0; Obj := Instance; Instance := nil; try {$ifndef LVCL} if (optFreeInMainThread in Factory.fAnyOptions) and (GetCurrentThreadID<>MainThreadID) then BackgroundExecuteInstanceRelease(Obj,nil) else {$endif} if (optFreeInPerInterfaceThread in Factory.fAnyOptions) and Assigned(Factory.fBackgroundThread) then BackgroundExecuteInstanceRelease(Obj,Factory.fBackgroundThread) else IInterface(Obj)._Release; except on E: Exception do Factory.fRest.Internallog('SafeFreeInstance: Ignored % exception '+ 'during %._Release',[E.ClassType,Factory.InterfaceURI],sllDebug); end; end; function TServiceFactoryServer.InternalInstanceRetrieve( var Inst: TServiceFactoryServerInstance; aMethodIndex,aSession: integer): integer; procedure AddNew; var i: integer; P: ^TServiceFactoryServerInstance; begin Inst.Session := aSession; Inst.Instance := CreateInstance(true); if Inst.Instance=nil then exit; result := aMethodIndex; // notify caller inc(fInstanceCount); fRest.InternalLog('%.InternalInstanceRetrieve: Adding %(%) instance (id=%) count=%', [ClassType,fInterfaceURI,pointer(Inst.Instance),Inst.InstanceID,fInstanceCount],sllDebug); P := pointer(fInstances); for i := 1 to fInstanceCapacity do if P^.InstanceID=0 then begin P^ := Inst; // found an empty entry -> re-use it exit; end else inc(P); fInstance.Add(Inst); // append a new entry end; var i: integer; P: ^TServiceFactoryServerInstance; begin result := -1; EnterCriticalSection(fInstanceLock); try Inst.LastAccess64 := GetTickCount64; // first release any deprecated instances if (fInstanceTimeout<>0) and (fInstanceCount>0) then begin P := pointer(fInstances); for i := 1 to fInstanceCapacity do begin if (P^.InstanceID<>0) and (Inst.LastAccess64>P^.LastAccess64+fInstanceTimeOut) then begin fRest.InternalLog('%.InternalInstanceRetrieve: Delete %(%) instance '+ '(id=%) after % minutes timeout (max % minutes)',[ClassType,fInterfaceURI, pointer(Inst.Instance),P^.InstanceID,(Inst.LastAccess64-P^.LastAccess64)div 60000, fInstanceTimeOut div 60000],sllInfo); P^.SafeFreeInstance(self); end; inc(P); end; end; if Inst.InstanceID=0 then begin // initialize a new sicClientDriven instance if (InstanceCreation<>sicClientDriven) or ((cardinal(aMethodIndex-SERVICE_PSEUDO_METHOD_COUNT)>=fInterface.fMethodsCount) and (aMethodIndex<>ord(imInstance))) then exit; inc(fInstanceCurrentID); Inst.InstanceID := fInstanceCurrentID; AddNew; end else begin // search the instance corresponding to Inst.InstanceID if fInstanceCount>0 then begin P := pointer(fInstances); for i := 1 to fInstanceCapacity do if P^.InstanceID=Inst.InstanceID then begin result := aMethodIndex; // notify caller if aMethodIndex=ord(imFree) then begin P^.SafeFreeInstance(self); // {"method":"_free_", "params":[], "id":1234} exit; end; P^.LastAccess64 := Inst.LastAccess64; Inst.Instance := P^.Instance; exit; end else inc(P); end; // add any new session/user/group/thread instance if necessary if (InstanceCreation<>sicClientDriven) and (cardinal(aMethodIndex-SERVICE_PSEUDO_METHOD_COUNT)nil then result := TSQLRestServer(fRest) else result := nil; end; function TServiceFactoryServer.CreateInstance(AndIncreaseRefCount: boolean): TInterfacedObject; var dummyObj: pointer; begin case fImplementationClassKind of ickWithCustomCreate: result := TInterfacedObjectWithCustomCreateClass(fImplementationClass).Create; ickInjectable: result := TInjectableObjectClass(fImplementationClass). CreateWithResolver(Rest.Services,true); ickInjectableRest: result := TInjectableObjectRestClass(fImplementationClass). CreateWithResolverAndRest(Rest.Services,self,RestServer,true); ickFromInjectedResolver: begin dummyObj := nil; if not TSQLRestServer(Rest).Services. TryResolveInternal(fInterface.fInterfaceTypeInfo,dummyObj) then raise EInterfaceFactoryException.CreateUTF8( 'ickFromInjectedResolver: TryResolveInternal(%)=false',[fInterface.fInterfaceName]); result := TInterfacedObject(ObjectFromInterface(IInterface(dummyObj))); if AndIncreaseRefCount then // RefCount=1 after TryResolveInternal() AndIncreaseRefCount := false else dec(TInterfacedObjectHooked(result).FRefCount); end; else result := fImplementationClass.Create; end; if Assigned(TSQLRestServer(Rest).OnServiceCreateInstance) then TSQLRestServer(Rest).OnServiceCreateInstance(self,result); if AndIncreaseRefCount then IInterface(result)._AddRef; // allow passing self to sub-methods end; procedure TServiceFactoryServer.OnLogRestExecuteMethod(Sender: TServiceMethodExecute; Step: TServiceMethodExecuteEventStep); var W: TTextWriter; a, len: integer; begin W := Sender.TempTextWriter; with Sender.Method^ do case Step of smsBefore: begin W.CancelAll; W.AddShort('"POST",{Method:"'); W.AddString(InterfaceDotMethodName); W.AddShort('",Input:{'); // as TSQLPropInfoRTTIVariant.GetJSONValues if not (optNoLogInput in Sender.fOptions) then begin for a := ArgsInFirst to ArgsInLast do with Args[a] do if (ValueDirection<>smdOut) and (ValueType<>smvInterface) and not IsDefault(Sender.Values[a]) then begin W.AddShort(ParamName^); // in JSON_OPTIONS_FAST_EXTENDED format W.Add(':'); if vIsSPI in ValueKindAsm then W.AddShort('"****",') else AddJSON(W,Sender.Values[a],SERVICELOG_WRITEOPTIONS); end; W.CancelLastComma; end; end; smsAfter: begin W.AddShort('},Output:{'); if not (optNoLogOutput in Sender.fOptions) then if ArgsResultIsServiceCustomAnswer then with PServiceCustomAnswer(Sender.Values[ArgsResultIndex])^ do begin len := length(Content); W.AddShort('len:'); W.AddU(len); if (Status<>0) and (Status<>HTTP_SUCCESS) then begin W.AddShort(',status:'); W.AddU(Status); end; if not fExcludeServiceLogCustomAnswer and (len>0) and (len<=1024) then begin W.AddShort(',result:"'); W.WrBase64(pointer(content),len,false); // up to 1KB of base-64 W.Add('"'); end; end else begin for a := ArgsOutFirst to ArgsOutLast do with Args[a] do if (ValueDirection in [smdVar,smdOut,smdResult]) and not IsDefault(Sender.Values[a]) then begin W.AddShort(ParamName^); W.Add(':'); if vIsSPI in ValueKindAsm then W.AddShort('"****",') else AddJSON(W,Sender.Values[a],SERVICELOG_WRITEOPTIONS); end; W.CancelLastComma; end; end; smsError: begin W.AddShort('},Output:{'); W.AddClassName(Sender.LastException.ClassType); W.Add(':','"'); W.AddJSONEscapeString(Sender.LastException.Message); W.Add('"'); end; end; end; procedure TServiceFactoryServer.ExecuteMethod(Ctxt: TSQLRestServerURIContext); var Inst: TServiceFactoryServerInstance; WR: TJSONSerializer; entry: PInterfaceEntry; instancePtr: pointer; // weak IInvokable reference dolock, execres: boolean; opt: TServiceMethodOptions; exec: TServiceMethodExecute; timeStart,timeEnd: Int64; stats: TSynMonitorInputOutput; m: integer; err: shortstring; temp: TTextWriterStackBuffer; function GetFullMethodName: RawUTF8; begin if Ctxt.ServiceMethod<>nil then result := PServiceMethod(Ctxt.ServiceMethod)^.InterfaceDotMethodName else result := fInterface.fInterfaceName; end; procedure Error(const Msg: RawUTF8; Status: integer); begin Ctxt.Error('% % for %',[ToText(InstanceCreation)^,Msg,GetFullMethodName],Status); end; function StatsCreate: TSynMonitorInputOutput; begin result := TSynMonitorInputOutput.Create(GetFullMethodName); end; procedure FinalizeLogRest; var W: TTextWriter; begin W := exec.TempTextWriter; if exec.CurrentStepCONST_AUTHENTICATION_NOT_USED then case InstanceCreation of // authenticated user -> handle context sicPerSession: Inst.InstanceID := Ctxt.Session; sicPerUser: Inst.InstanceID := Ctxt.SessionUser; sicPerGroup: Inst.InstanceID := Ctxt.SessionGroup; end else begin Error('mode expects an authenticated session',HTTP_UNAUTHORIZED); exit; end; end; case InternalInstanceRetrieve(Inst,Ctxt.ServiceMethodIndex,Ctxt.Session) of ord(imFree): begin Ctxt.Success; // {"method":"_free_", "params":[], "id":1234} exit; end; ord(imInstance): begin // from TServiceFactoryClient.CreateFakeInstance Ctxt.Results([Inst.InstanceID],HTTP_SUCCESS); exit; end; end; end; end; if Inst.Instance=nil then begin Error('instance not found or deprecated',HTTP_UNAUTHORIZED); exit; end; Ctxt.ServiceInstanceID := Inst.InstanceID; // 2. call method implementation if (Ctxt.ServiceExecution=nil) or (Ctxt.ServiceMethod=nil) then begin Error('ServiceExecution=nil',HTTP_SERVERERROR); exit; end; stats := nil; if (mlInterfaces in TSQLRestServer(Rest).StatLevels) then begin m := Ctxt.ServiceMethodIndex-SERVICE_PSEUDO_METHOD_COUNT; if m>=0 then begin stats := fStats[m]; if stats=nil then begin EnterCriticalSection(fInstanceLock); try stats := fStats[m]; if stats=nil then begin stats := StatsCreate; fStats[m] := stats; end; finally LeaveCriticalSection(fInstanceLock); end; end; stats.Processing := true; end; end; err := ''; exec := nil; try if fImplementationClassKind=ickFake then if Inst.Instance<>fSharedInstance then exit else instancePtr := @TInterfacedObjectFake(Inst.Instance).fVTable else begin if PClass(Inst.Instance)^=fImplementationClass then entry := fImplementationClassInterfaceEntry else begin entry := Inst.Instance.GetInterfaceEntry(fInterface.fInterfaceIID); if entry=nil then exit; end; instancePtr := PAnsiChar(Inst.Instance)+entry^.IOffset; end; opt := Ctxt.ServiceExecution^.Options; if optExecInPerInterfaceThread in opt then if fBackgroundThread=nil then fBackgroundThread := Rest.NewBackgroundThreadMethod( '% %',[self,fInterface.fInterfaceName]); WR := TJSONSerializer.CreateOwnedStream(temp); try Ctxt.fThreadServer^.Factory := self; if not(optForceStandardJSON in opt) and ((Ctxt.Call.InHead='') or (Ctxt.ClientKind=ckFramework)) then include(WR.fCustomOptions,twoForceJSONExtended) else include(WR.fCustomOptions,twoForceJSONStandard); // AJAX if optDontStoreVoidJSON in opt then include(WR.fCustomOptions,twoIgnoreDefaultInRecord); // root/calculator {"method":"add","params":[1,2]} -> {"result":[3],"id":0} Ctxt.ServiceResultStart(WR); dolock := optExecLockedPerInterface in opt; if dolock then EnterCriticalSection(fInstanceLock); exec := TServiceMethodExecute.Create(Ctxt.ServiceMethod); try exec.fOptions := opt; {$ifndef LVCL} exec.fBackgroundExecutionThread := fBackgroundThread; {$endif} exec.fOnCallback := Ctxt.ExecuteCallback; if fOnExecute<>nil then MultiEventMerge(exec.fOnExecute,fOnExecute); if Ctxt.ServiceExecution^.LogRest<>nil then exec.AddInterceptor(OnLogRestExecuteMethod); if (fImplementationClassKind=ickFake) and ((Ctxt.ServiceParameters=nil) or (Ctxt.ServiceParameters^='[')) and {$ifndef LVCL}not ((optExecInMainThread in exec.fOptions) or (optExecInPerInterfaceThread in exec.fOptions)) and {$endif} (exec.fMethod^.ArgsOutputValuesCount=0) then // bypass JSON marshalling execres := exec.ExecuteJsonFake(Inst.Instance,Ctxt.ServiceParameters) else execres := exec.ExecuteJson([instancePtr],Ctxt.ServiceParameters,WR, @err,Ctxt.ForceServiceResultAsJSONObject); if not execres then begin if err<>'' then Ctxt.Error('%',[err],HTTP_NOTACCEPTABLE) else Error('execution failed (probably due to bad input parameters)',HTTP_NOTACCEPTABLE); exit; // wrong request end; Ctxt.Call.OutHead := exec.ServiceCustomAnswerHead; Ctxt.Call.OutStatus := exec.ServiceCustomAnswerStatus; finally if dolock then LeaveCriticalSection(fInstanceLock); end; if Ctxt.Call.OutHead='' then begin // <>'' for TServiceCustomAnswer Ctxt.ServiceResultEnd(WR,Inst.InstanceID); Ctxt.Call.OutHead := JSON_CONTENT_TYPE_HEADER_VAR; Ctxt.Call.OutStatus := HTTP_SUCCESS; end; WR.SetText(Ctxt.Call.OutBody); finally Ctxt.fThreadServer^.Factory := nil; WR.Free; end; finally try if InstanceCreation=sicSingle then Inst.SafeFreeInstance(self); // always release single shot instance if stats<>nil then begin {$ifdef LINUX}QueryPerformanceMicroSeconds{$else}QueryPerformanceCounter{$endif}(timeEnd); dec(timeEnd,timeStart); Ctxt.StatsFromContext(stats,timeEnd,false); if Ctxt.Server.StatUsage<>nil then Ctxt.Server.StatUsage.Modified(stats,[]); if (mlSessions in TSQLRestServer(Rest).StatLevels) and (Ctxt.fSession<>nil) then begin if Ctxt.fSession.fInterfaces=nil then begin EnterCriticalSection(fInstanceLock); try if Ctxt.fSession.fInterfaces=nil then SetLength(Ctxt.fSession.fInterfaces,length(Rest.Services.fInterfaceMethod)); finally LeaveCriticalSection(fInstanceLock); end; end; m := Ctxt.fServiceListInterfaceMethodIndex; if m<0 then m := Rest.Services.fInterfaceMethods.FindHashed( PServiceMethod(Ctxt.ServiceMethod)^.InterfaceDotMethodName); if m>=0 then with Ctxt.fSession do begin stats := fInterfaces[m]; if stats=nil then begin EnterCriticalSection(fInstanceLock); try stats := fInterfaces[m]; if stats=nil then begin stats := StatsCreate; fInterfaces[m] := stats; end; finally LeaveCriticalSection(fInstanceLock); end; end; Ctxt.StatsFromContext(stats,timeEnd,true); // mlSessions stats are not yet tracked per Client end; end; end else timeEnd := 0; finally if exec<>nil then begin if Ctxt.ServiceExecution^.LogRest<>nil then FinalizeLogRest; exec.Free; end; end; end; end; function TServiceFactoryServer.AllowAll: TServiceFactoryServer; var m: integer; begin if self<>nil then for m := 0 to fInterface.fMethodsCount-1 do FillcharFast(fExecution[m].Denied,SizeOf(fExecution[m].Denied),0); result := self; end; function TServiceFactoryServer.AllowAllByID(const aGroupID: array of TID): TServiceFactoryServer; var m,g: integer; begin if self<>nil then for m := 0 to fInterface.fMethodsCount-1 do with fExecution[m] do for g := 0 to high(aGroupID) do exclude(Denied,aGroupID[g]-1); result := self; end; function TServiceFactoryServer.AllowAllByName(const aGroup: array of RawUTF8): TServiceFactoryServer; var IDs: TIDDynArray; begin if self<>nil then if RestServer.MainFieldIDs(RestServer.fSQLAuthGroupClass,aGroup,IDs) then AllowAllByID(IDs); result := self; end; function TServiceFactoryServer.DenyAll: TServiceFactoryServer; var m: integer; begin if self<>nil then for m := 0 to fInterface.fMethodsCount-1 do FillcharFast(fExecution[m].Denied,SizeOf(fExecution[m].Denied),255); result := self; end; function TServiceFactoryServer.DenyAllByID(const aGroupID: array of TID): TServiceFactoryServer; var m,g: integer; begin if self<>nil then for m := 0 to fInterface.fMethodsCount-1 do with fExecution[m] do for g := 0 to high(aGroupID) do include(Denied,aGroupID[g]-1); result := self; end; function TServiceFactoryServer.DenyAllByName(const aGroup: array of RawUTF8): TServiceFactoryServer; var IDs: TIDDynArray; begin if self<>nil then if RestServer.MainFieldIDs(RestServer.fSQLAuthGroupClass,aGroup,IDs) then DenyAllByID(IDs); result := self; end; function TServiceFactoryServer.Allow(const aMethod: array of RawUTF8): TServiceFactoryServer; var m: integer; begin if self<>nil then for m := 0 to high(aMethod) do FillcharFast(fExecution[fInterface.CheckMethodIndex(aMethod[m])].Denied, SizeOf(fExecution[0].Denied),0); result := self; end; function TServiceFactoryServer.AllowByID(const aMethod: array of RawUTF8; const aGroupID: array of TID): TServiceFactoryServer; var m,g: integer; begin if self<>nil then if high(aGroupID)>=0 then for m := 0 to high(aMethod) do with fExecution[fInterface.CheckMethodIndex(aMethod[m])] do for g := 0 to high(aGroupID) do exclude(Denied,aGroupID[g]-1); result := self; end; function TServiceFactoryServer.AllowByName(const aMethod: array of RawUTF8; const aGroup: array of RawUTF8): TServiceFactoryServer; var IDs: TIDDynArray; begin if self<>nil then if RestServer.MainFieldIDs(RestServer.fSQLAuthGroupClass,aGroup,IDs) then AllowByID(aMethod,IDs); result := self; end; function TServiceFactoryServer.Deny(const aMethod: array of RawUTF8): TServiceFactoryServer; var m: integer; begin if self<>nil then for m := 0 to high(aMethod) do FillcharFast(fExecution[fInterface.CheckMethodIndex(aMethod[m])].Denied, SizeOf(fExecution[0].Denied),255); result := self; end; function TServiceFactoryServer.DenyByID(const aMethod: array of RawUTF8; const aGroupID: array of TID): TServiceFactoryServer; var m,g: integer; begin if self<>nil then for m := 0 to high(aMethod) do with fExecution[fInterface.CheckMethodIndex(aMethod[m])] do for g := 0 to high(aGroupID) do include(Denied,aGroupID[g]-1); result := self; end; function TServiceFactoryServer.DenyByName(const aMethod: array of RawUTF8; const aGroup: array of RawUTF8): TServiceFactoryServer; var IDs: TIDDynArray; begin if self<>nil then if RestServer.MainFieldIDs(RestServer.fSQLAuthGroupClass,aGroup,IDs) then DenyByID(aMethod,IDs); result := self; end; function TServiceFactoryServer.SetOptions(const aMethod: array of RawUTF8; aOptions: TServiceMethodOptions; aAction: TServiceMethodOptionsAction): TServiceFactoryServer; begin if self<>nil then begin if (fInstanceCreation=sicPerThread) and (optExecLockedPerInterface in aOptions) then raise EServiceException.CreateUTF8('%.SetOptions(I%,optExecLockedPerInterface)'+ ' not compatible with sicPerThread',[self,fInterfaceURI]); if (fInstanceCreation=sicPerThread) and ([{$ifndef LVCL}optExecInMainThread,optFreeInMainThread,{$endif} optExecInPerInterfaceThread,optFreeInPerInterfaceThread]*aOptions<>[]) then raise EServiceException.CreateUTF8('%.SetOptions(I%,opt*In*Thread) '+ 'not compatible with sicPerThread',[self,fInterfaceURI]); {$ifndef LVCL} if (optExecLockedPerInterface in aOptions) and ([optExecInMainThread,optFreeInMainThread, optExecInPerInterfaceThread,optFreeInPerInterfaceThread]*aOptions<>[]) then raise EServiceException.CreateUTF8('%.SetOptions(I%,optExecLockedPerInterface)'+ ' with opt*In*Thread options',[self,fInterfaceURI]); {$endif} ExecutionAction(aMethod,aOptions,aAction); if (optFreeInPerInterfaceThread in fAnyOptions) and not (optExecInPerInterfaceThread in fAnyOptions) then raise EServiceException.CreateUTF8('%.SetOptions(I%,optFreeInPerInterfaceThread)'+ ' without optExecInPerInterfaceThread',[self,fInterfaceURI]); {$ifndef LVCL} if ([optExecInMainThread,optFreeInMainThread]*fAnyOptions<>[]) and ([optExecInPerInterfaceThread,optFreeInPerInterfaceThread]*fAnyOptions<>[]) then raise EServiceException.CreateUTF8('%.SetOptions(I%): concurrent '+ 'opt*InMainThread and opt*InPerInterfaceThread',[self,fInterfaceURI]); {$endif} end; result := self; end; function TServiceFactoryServer.SetTimeoutSec(value: cardinal): TServiceFactoryServer; begin SetTimeoutSecInt(value); result := self; end; function TServiceFactoryServer.SetServiceLog(const aMethod: array of RawUTF8; aLogRest: TSQLRest; aLogClass: TSQLRecordServiceLogClass): TServiceFactoryServer; var bits: TInterfaceFactoryMethodBits; begin if self<>nil then begin fInterface.CheckMethodIndexes(aMethod,true,bits); SetServiceLogByIndex(bits,aLogRest,aLogClass); end; result := self; end; procedure TServiceFactoryServer.SetServiceLogByIndex( const aMethods: TInterfaceFactoryMethodBits; aLogRest: TSQLRest; aLogClass: TSQLRecordServiceLogClass); var m: integer; begin if aLogRest=nil then aLogClass := nil else begin if aLogClass=nil then aLogClass := TSQLRecordServiceLog; aLogRest.Model.GetTableIndexExisting(aLogClass); end; for m := 0 to fInterface.fMethodsCount-1 do if m in aMethods then with fExecution[m] do begin LogRest := aLogRest; LogClass := aLogClass; end; if aLogRest<>nil then // write every second or after 500 rows in background aLogRest.AsynchBatchStart(aLogClass,1,500,1000); // do nothing if already set end; procedure TServiceFactoryServer.AddInterceptor(const Hook: TServiceMethodExecuteEvent); begin MultiEventAdd(fOnExecute,TMethod(Hook)); end; { TServiceRecordVersion } function TServiceRecordVersion.Subscribe(const SQLTableName: RawUTF8; const revision: TRecordVersion; const callback: IServiceRecordVersionCallback): boolean; var tableIndex: integer; tableRemote: TSQLRest; tableServer: TSQLRestServer; begin if Server<>nil then begin tableIndex := Server.Model.GetTableIndex(SQLTableName); if tableIndex>=0 then begin tableRemote := Server.GetRemoteTable(tableIndex); if (tableRemote=nil) or not tableRemote.InheritsFrom(TSQLRestServer) then tableServer := Server else tableServer := TSQLRestServer(tableRemote); result := tableServer.RecordVersionSynchronizeSubscribeMaster( Server.Model.Tables[tableindex],revision,callback); exit; end; end; result := false; end; { TServiceMethodArgument } {$ifndef FPC} procedure TServiceMethodArgument.SetFromRTTI(var P: PByte); var PS: PShortString absolute P; PP: ^PPTypeInfo absolute P; begin ArgTypeName := PS; P := @PS^[ord(PS^[0])+1]; if PP^=nil then {$ifndef ISDELPHI2010} if IdemPropName(ArgTypeName^,'TGUID') then ArgTypeInfo := @GUID_FAKETYPEINFO else {$endif} raise EInterfaceFactoryException.CreateUTF8( '"%: %" parameter has no RTTI',[ParamName^,ArgTypeName^]) else ArgTypeInfo := PP^^; inc(PP); end; {$endif FPC} procedure TServiceMethodArgument.SerializeToContract(WR: TTextWriter); const ARGDIRTOJSON: array[TServiceMethodValueDirection] of string[4] = ( // convert into generic in/out direction (assume result is out) 'in','both','out','out'); // AnsiString (Delphi <2009) may loose data depending on the client ARGTYPETOJSON: array[TServiceMethodValueType] of string[8] = ( '??','self','boolean', '', '','integer','cardinal','int64', 'double','datetime','currency','utf8','utf8','utf8','utf8','utf8','', {$ifndef NOVARIANTS}'variant',{$endif}'','json','',''); begin WR.AddShort('{"argument":"'); WR.AddShort(ParamName^); WR.AddShort('","direction":"'); WR.AddShort(ARGDIRTOJSON[ValueDirection]); WR.AddShort('","type":"'); if ARGTYPETOJSON[ValueType]='' then WR.AddShort(ArgTypeInfo^.Name) else WR.AddShort(ARGTYPETOJSON[ValueType]); {$ifdef SOA_DEBUG} WR.Add('"',','); WR.AddPropJSONInt64('index',IndexVar); WR.AddPropJSONString('var',GetEnumNameTrimed(TypeInfo(TServiceMethodValueVar),ValueVar)); WR.AddPropJSONInt64('stackoffset',InStackOffset); WR.AddPropJSONInt64('reg',RegisterIdent); WR.AddPropJSONInt64('fpreg',FPRegisterIdent); WR.AddPropJSONInt64('stacksize',SizeInStack); WR.AddPropJSONInt64('storsize',SizeInStorage); if ValueType=smvBinary then WR.AddPropJSONInt64('binsize',SizeInBinary); WR.AddPropName('asm'); WR.AddString(GetSetNameCSV(TypeInfo(TServiceMethodValueAsm),ValueKindAsm)); WR.AddShort('},'); {$else} WR.AddShort('"},'); {$endif SOA_DEBUG} end; function TServiceMethodArgument.IsDefault(V: pointer): boolean; begin result := false; case ValueType of smvBoolean..smvCurrency: case SizeInStorage of 1: result := PByte(V)^=0; 2: result := PWord(V)^=0; 4: result := PInteger(V)^=0; 8: result := PInt64(V)^=0; end; smvRawUTF8..smvWideString, smvObject..smvInterface: result := PPointer(V)^=nil; smvBinary, smvRecord: result := IsZeroSmall(V,SizeInStorage); {$ifndef NOVARIANTS} smvVariant: result := PVarData(V)^.vtype<=varNull; {$endif} end; end; function TServiceMethodArgument.FromJSON(const MethodName: RawUTF8; var R: PUTF8Char; V: pointer; Error: PShortString{$ifndef NOVARIANTS}; DVO: TDocVariantOptions{$endif}): boolean; procedure RaiseError(const msg: RawUTF8); var tmp: shortstring; begin FormatShort('I% failed on %:% [%]',[MethodName,ParamName^,ArgTypeName^,msg],tmp); if Error=nil then raise EInterfaceFactoryException.CreateUTF8('%',[tmp]); result := false; Error^ := tmp; end; var parser: TJSONToObject; // inlined JSONToObject() Val: PUTF8Char; ValLen: integer; wasString: boolean; wrapper: TDynArray; label doint; begin result := true; case ValueType of smvObject: begin if PInteger(R)^=NULL_LOW then inc(R,4) else begin // null from TInterfacedStub -> stay untouched parser.From := R; // inlined JSONToObject() parser.Options := JSONTOOBJECT_TOLERANTOPTIONS; parser.TObjectListItemClass := nil; parser.Value := PObject(V)^; parser.Parse; if parser.Valid then R := parser.Dest else RaiseError('returned object'); end; IgnoreComma(R); end; smvInterface: RaiseError('unexpected var/out interface'); smvRawJSON: if (R<>nil) and (R^=']') then PRawUTF8(V)^ := '' else begin GetJSONItemAsRawJSON(R,PRawJSON(V)^); if R=nil then RaiseError('returned RawJSON'); end; smvDateTime: begin Val := GetJSONField(R,R,@wasString,nil,@ValLen); if (Val=nil) or (ValLen=0) then PInt64(V)^ := 0 else if wasString then Iso8601ToDateTimePUTF8CharVar(Val,ValLen,PDateTime(V)^) else unaligned(PDouble(V)^) := GetExtended(Val); // allow JSON number decoding end; smvBoolean..smvDouble, smvCurrency..smvWideString: begin Val := GetJSONField(R,R,@wasString,nil,@ValLen); if (Val=nil) or (wasString<>(vIsString in ValueKindAsm)) then begin RaiseError('missing or invalid value'); exit; end; if (ValueType=smvBoolean) and (PInteger(Val)^=TRUE_LOW) then Val := pointer(SmallUInt32UTF8[1]); // normalize 'true' to '1' case ValueType of smvBoolean, smvEnum, smvSet, smvCardinal: doint:case SizeInStorage of 1: PByte(V)^ := GetCardinal(Val); 2: PWord(V)^ := GetCardinal(Val); 4: PCardinal(V)^ := GetCardinal(Val); 8: SetQWord(Val,PQWord(V)^); end; smvInteger: PInteger(V)^ := GetInteger(Val); smvInt64: if vIsQword in ValueKindAsm then SetQWord(Val,PQWord(V)^) else SetInt64(Val,PInt64(V)^); smvDouble: unaligned(PDouble(V)^) := GetExtended(Val); smvCurrency: PInt64(V)^ := StrToCurr64(Val); smvRawUTF8: FastSetString(PRawUTF8(V)^,Val,ValLen); smvString: UTF8DecodeToString(Val,ValLen,PString(V)^); smvRawByteString: Base64ToBin(PAnsiChar(Val),ValLen,PRawByteString(V)^); smvWideString: UTF8ToWideString(Val,ValLen,PWideString(V)^); else RaiseError('ValueType?'); end; end; smvBinary: begin Val := GetJSONField(R,R,@wasString,nil,@ValLen); if Val=nil then begin RaiseError('missing or invalid binary value'); exit; end; if wasString then begin // decode hexadecimal string if ValLen=SizeInStorage*2 then HexDisplayToBin(PAnsiChar(Val),PByte(V),SizeInStorage); end else // allow fallback to read plain numbers (e.g. on API upgrade) goto doint; end; smvRecord: begin R := RecordLoadJSON(V^,R,ArgTypeInfo); if R=nil then RaiseError('returned record'); end; {$ifndef NOVARIANTS} smvVariant: begin R := VariantLoadJSON(PVariant(V)^,R,nil,@DVO); if R=nil then RaiseError('returned variant'); end; {$endif} smvDynArray: begin if vIsObjArray in ValueKindAsm then ObjArrayClear(V^); wrapper.InitFrom(DynArrayWrapper,V^); R := wrapper.LoadFromJSON(R); if R=nil then RaiseError('returned array'); IgnoreComma(R); end; end; end; procedure TServiceMethodArgument.AddJSON(WR: TTextWriter; V: pointer; ObjectOptions: TTextWriterWriteObjectOptions); var wrapper: TDynArray; // faster than WR.AddDynArrayJSON(ArgTypeInfo,V^) begin if vIsString in ValueKindAsm then WR.Add('"'); case ValueType of smvEnum..smvInt64: case SizeInStorage of 1: WR.Add(PByte(V)^); 2: WR.Add(PWord(V)^); 4: if ValueType=smvInteger then WR.Add(PInteger(V)^) else WR.AddU(PCardinal(V)^); 8: if vIsQword in ValueKindAsm then WR.AddQ(PQWord(V)^) else WR.Add(PInt64(V)^); end; smvBoolean: WR.Add(PBoolean(V)^); smvDouble: WR.AddDouble(unaligned(PDouble(V)^)); smvDateTime: WR.AddDateTime(PDateTime(V)^,vIsDateTimeMS in ValueKindAsm); smvCurrency: WR.AddCurr64(PInt64(V)^); smvRawUTF8: WR.AddJSONEscape(PPointer(V)^); smvRawJSON: WR.AddRawJSON(PRawJSON(V)^); smvString: {$ifdef UNICODE} WR.AddJSONEscapeW(pointer(PString(V)^)); {$else} WR.AddJSONEscapeAnsiString(PString(V)^); {$endif} smvRawByteString: WR.WrBase64(PPointer(V)^,length(PRawBytestring(V)^),false); smvWideString: WR.AddJSONEscapeW(PPointer(V)^); smvBinary: if not IsZeroSmall(V,SizeInStorage) then // leave "" for zero WR.AddBinToHexDisplayLower(V,SizeInStorage); smvObject: WR.WriteObject(PPointer(V)^,ObjectOptions); smvInterface: WR.AddShort('null'); // or written by InterfaceWrite() smvRecord: WR.AddRecordJSON(V^,ArgTypeInfo); smvDynArray: if vIsObjArray in ValueKindAsm then WR.AddObjArrayJSON(V^,ObjectOptions) else begin wrapper.InitFrom(DynArrayWrapper,V^); WR.AddDynArrayJSON(wrapper); end; {$ifndef NOVARIANTS} smvVariant: WR.AddVariant(PVariant(V)^,twJSONEscape); {$endif} end; if vIsString in ValueKindAsm then WR.Add('"',',') else WR.Add(','); end; procedure TServiceMethodArgument.AsJson(var DestValue: RawUTF8; V: pointer); var W: TTextWriter; temp: TTextWriterStackBuffer; begin case ValueType of // some direct conversion of simple types smvBoolean: DestValue := BOOL_UTF8[PBoolean(V)^]; smvEnum..smvInt64: case SizeInStorage of 1: UInt32ToUtf8(PByte(V)^,DestValue); 2: UInt32ToUtf8(PWord(V)^,DestValue); 4: if ValueType=smvInteger then Int32ToUtf8(PInteger(V)^,DestValue) else UInt32ToUtf8(PCardinal(V)^,DestValue); 8: if vIsQword in ValueKindAsm then UInt64ToUtf8(PQword(V)^,DestValue) else Int64ToUtf8(PInt64(V)^,DestValue); end; smvDouble: DoubleToStr(unaligned(PDouble(V)^),DestValue); smvCurrency: Curr64ToStr(PInt64(V)^,DestValue); smvRawJSON: DestValue := PRawUTF8(V)^; else begin // use generic AddJSON() method for complex "..." content W := TJSONSerializer.CreateOwnedStream(temp); try AddJSON(W,V); W.SetText(DestValue); finally W.Free; end; end; end; end; procedure TServiceMethodArgument.AddJSONEscaped(WR: TTextWriter; V: pointer); var W: TTextWriter; begin if ValueType in [smvBoolean..smvCurrency,smvInterface] then // no need to escape those AddJSON(WR,V) else begin W := WR.InternalJSONWriter; AddJSON(W,V); WR.AddJSONEscape(W); end; end; procedure TServiceMethodArgument.AddValueJSON(WR: TTextWriter; const Value: RawUTF8); begin if vIsString in ValueKindAsm then begin WR.Add('"'); WR.AddJSONEscape(pointer(Value)); WR.Add('"',','); end else begin WR.AddString(Value); WR.Add(','); end; end; procedure TServiceMethodArgument.AddDefaultJSON(WR: TTextWriter); begin case ValueType of smvBoolean: WR.AddShort('false,'); smvObject: WR.AddShort('null,'); // may raise an error on the client side smvInterface: WR.AddShort('0,'); smvDynArray: WR.AddShort('[],'); smvRecord: begin WR.AddVoidRecordJSON(ArgTypeInfo); WR.Add(','); end; {$ifndef NOVARIANTS} smvVariant: WR.AddShort('null,'); {$endif} else if vIsString in ValueKindAsm then WR.AddShort('"",') else WR.AddShort('0,'); end; end; {$ifndef NOVARIANTS} procedure TServiceMethodArgument.AsVariant(var DestValue: variant; V: pointer; Options: TDocVariantOptions); var tmp: RawUTF8; begin case ValueType of // some direct conversion of simple types smvBoolean: DestValue := PBoolean(V)^; smvEnum..smvInt64: case SizeInStorage of 1: DestValue := PByte(V)^; 2: DestValue := PWord(V)^; 4: if ValueType=smvInteger then DestValue := PInteger(V)^ else DestValue := PCardinal(V)^; 8: if vIsQword in ValueKindAsm then DestValue := PQWord(V)^ else DestValue := PInt64(V)^; end; smvDouble, smvDateTime: DestValue := unaligned(PDouble(V)^); smvCurrency: DestValue := PCurrency(V)^; smvRawUTF8: RawUTF8ToVariant(PRawUTF8(V)^,DestValue); smvString: begin StringToUTF8(PString(V)^,tmp); RawUTF8ToVariant(tmp,DestValue); end; smvWideString: begin RawUnicodeToUtf8(PPointer(V)^,length(PWideString(V)^),tmp); RawUTF8ToVariant(tmp,DestValue); end; smvVariant: DestValue := PVariant(V)^; else begin // use generic AddJSON() method AsJson(tmp,V); VariantLoadJSON(DestValue,pointer(tmp),nil,@Options); end; end; end; procedure TServiceMethodArgument.AddAsVariant(var Dest: TDocVariantData; V: pointer); var tmp: variant; begin AsVariant(tmp,V,Dest.Options); if dvoIsArray in Dest.Options then Dest.AddItem(tmp) else Dest.AddValue(ShortStringToAnsi7String(ParamName^),tmp); end; procedure TServiceMethodArgument.FixValueAndAddToObject(const Value: variant; var DestDoc: TDocVariantData); var tempCopy: variant; begin tempCopy := Value; FixValue(tempCopy); DestDoc.AddValue(ShortStringToAnsi7String(ParamName^),tempCopy); end; procedure TServiceMethodArgument.FixValue(var Value: variant); var enum: Int64; obj: TObject; arr: pointer; dyn: TDynArray; rec: TByteDynArray; json: RawUTF8; begin case ValueType of smvEnum: if VariantToInt64(Value,enum) then Value := PTypeInfo(ArgTypeInfo)^.EnumBaseType^.GetEnumNameOrd(enum)^; smvSet: if VariantToInt64(Value,enum) then Value := PTypeInfo(ArgTypeInfo)^.SetEnumType^.GetSetNameAsDocVariant(enum); smvObject: begin obj := ArgTypeInfo^.ClassCreate; try if DocVariantToObject(_Safe(Value)^,obj) then Value := _ObjFast(obj,[woEnumSetsAsText]); finally obj.Free; end; end; smvDynArray: if _Safe(Value)^.Kind=dvArray then begin arr := nil; dyn.InitFrom(DynArrayWrapper,arr); try VariantSaveJSON(Value,twJSONEscape,json); dyn.LoadFromJSON(pointer(json)); json := dyn.SaveToJSON(true); _Json(json,Value,JSON_OPTIONS_FAST); finally dyn.Clear; end; end; smvRecord: if _Safe(Value)^.Kind=dvObject then begin SetLength(rec,ArgTypeInfo^.RecordType^.Size); try VariantSaveJSON(Value,twJSONEscape,json); RecordLoadJSON(rec[0],pointer(json),ArgTypeInfo); json := RecordSaveJSON(rec[0],ArgTypeInfo,true); _Json(json,Value,JSON_OPTIONS_FAST); finally RecordClear(rec[0],ArgTypeInfo); end; end; end; end; {$endif NOVARIANTS} { TAutoCreateFields } type // use AutoTable VMT entry to store a cache of the needed fields RTTI TAutoCreateFields = class public ClassesCount: integer; ObjArraysCount: integer; Classes: array of record Offset: cardinal; Instance: TClassInstance; end; ObjArraysOffset: TCardinalDynArray; InterfacesOffset: TCardinalDynArray; constructor Create(aClass: TClass); end; constructor TAutoCreateFields.Create(aClass: TClass); var i: integer; P: PPropInfo; begin repeat for i := 1 to InternalClassPropInfo(aClass,P) do begin case P^.PropType^.Kind of tkClass: begin if (P^.SetProc<>0) or not P^.GetterIsField then raise EModelException.CreateUTF8('%.%: % is an auto-created instance '+ 'so should not have any "write" defined',[aClass,P^.Name,P^.PropType^.Name]); SetLength(Classes,ClassesCount+1); with Classes[ClassesCount] do begin Offset := PtrUInt(P^.GetterAddr(nil)); Instance.Init(P^.PropType^.ClassType^.ClassType); end; inc(ClassesCount); end; tkDynArray: if (ObjArraySerializers.Find(P^.TypeInfo)<>nil) and P^.GetterIsField then begin SetLength(ObjArraysOffset,ObjArraysCount+1); ObjArraysOffset[ObjArraysCount] := PtrUInt(P^.GetterAddr(nil)); inc(ObjArraysCount); end; tkInterface: if P^.GetterIsField then AddInteger(TIntegerDynArray(InterfacesOffset),PtrUInt(P^.GetterAddr(nil))); end; P := P^.Next; end; aClass := GetClassParent(aClass); until aClass=TObject; end; function SetAutoCreateFields(PVMT: pointer; c: TClass): TAutoCreateFields; begin EnterCriticalSection(vmtAutoTableLock); // protect from concurrent access try result := PPointer(PVMT)^; if result=nil then begin // first time access: compute RTTI cache result := TAutoCreateFields.Create(c); // store the RTTI cache into the AutoTable VMT entry of this class PatchCodePtrUInt(pointer(PVMT),PtrUInt(result),true); GarbageCollectorFreeAndNil(PVMT^,result); end; finally LeaveCriticalSection(vmtAutoTableLock); end; end; procedure AutoCreateFields(self: TObject); var fields: TAutoCreateFields; PVMT: PPointer; i: integer; begin PVMT := pointer(PPtrInt(self)^+vmtAutoTable); fields := PVMT^; if fields=nil then fields := SetAutoCreateFields(PVMT,PClass(self)^) else if PClass(fields)^<>TAutoCreateFields then raise EModelException.CreateUTF8('%.AutoTable VMT entry already set',[self]); // auto-create published persistent class instances for i := 0 to fields.ClassesCount-1 do with fields.Classes[i] do PObject(PtrUInt(self)+Offset)^ := Instance.CreateNew; end; function AutoDestroyFields(self: TObject): TAutoCreateFields; {$ifdef HASINLINENOTX86}inline;{$endif} var i: integer; begin result := PPointer(PPtrInt(self)^+vmtAutoTable)^; if result=nil then exit; // may happen in a weird finalization code // auto-release published persistent class instances for i := 0 to result.ClassesCount-1 do FreeAndNil(PObject(PtrUInt(self)+result.Classes[i].Offset)^); // auto-release published T*ObjArray instances for i := 0 to result.ObjArraysCount-1 do ObjArrayClear(pointer(PtrUInt(self)+result.ObjArraysOffset[i])^); end; { TPersistentAutoCreateFields } constructor TPersistentAutoCreateFields.Create; begin AutoCreateFields(self); inherited Create; // always call the virtual constructor end; destructor TPersistentAutoCreateFields.Destroy; begin AutoDestroyFields(self); inherited Destroy; end; { TSynAutoCreateFields } {$ifdef FPC_OR_PUREPASCAL} constructor TSynAutoCreateFields.Create; begin AutoCreateFields(self); inherited Create; // always call the virtual constructor end; {$else} class function TSynAutoCreateFields.NewInstance: TObject; asm push eax // class mov eax, [eax].vmtInstanceSize push eax // size call System.@GetMem pop edx // size push eax // self mov cl, 0 call dword ptr[FillcharFast] pop eax // self pop edx // class mov [eax], edx // store VMT push eax call AutoCreateFields pop eax end; // ignore vmtIntfTable for this class hierarchy (won't implement interfaces) {$endif} destructor TSynAutoCreateFields.Destroy; begin AutoDestroyFields(self); inherited Destroy; end; procedure TSynAutoCreateFields.AfterLoad; begin end; { TSynAutoCreateFieldsLocked } constructor TSynAutoCreateFieldsLocked.Create; begin inherited Create; fSafe.Init; end; destructor TSynAutoCreateFieldsLocked.Destroy; begin inherited Destroy; fSafe.Done; end; procedure TSynAutoCreateFieldsLocked.Lock; begin if self<>nil then fSafe.Lock; end; procedure TSynAutoCreateFieldsLocked.UnLock; begin if self<>nil then fSafe.UnLock; end; { TInterfacedObjectAutoCreateFields } constructor TInterfacedObjectAutoCreateFields.Create; begin AutoCreateFields(self); inherited Create; // always call the virtual constructor end; destructor TInterfacedObjectAutoCreateFields.Destroy; begin AutoDestroyFields(self); inherited Destroy; end; { TInjectableAutoCreateFields } constructor TInjectableAutoCreateFields.Create; var Inject: IAutoCreateFieldsResolve; begin AutoCreateFields(self); inherited Create; // overriden method will inject its dependencies (DI/IoC) if TryResolve(TypeInfo(IAutoCreateFieldsResolve),Inject) then Inject.SetProperties(self); end; destructor TInjectableAutoCreateFields.Destroy; begin AutoDestroyFields(self); inherited Destroy; end; { TSynJsonFileSettings } function TSynJsonFileSettings.LoadFromJson(var aJson: RawUTF8): boolean; begin result := JSONSettingsToObject(aJson, self); end; function TSynJsonFileSettings.LoadFromFile(const aFileName: TFileName): boolean; begin fFileName := aFileName; fInitialJsonContent := StringFromFile(aFileName); result := LoadFromJson(fInitialJsonContent); end; procedure TSynJsonFileSettings.SaveIfNeeded; var saved: RawUTF8; begin if (self = nil) or (fFileName = '') then exit; saved := ObjectToJSON(self, SETTINGS_WRITEOPTIONS); if saved = fInitialJsonContent then exit; FileFromString(saved, fFileName); fInitialJsonContent := saved; end; {$ifndef LVCL} { TInterfacedCollection } constructor TInterfacedCollection.Create; begin inherited Create(GetClass); end; { TCollectionItemAutoCreateFields } constructor TCollectionItemAutoCreateFields.Create(Collection: TCollection); begin AutoCreateFields(self); inherited Create(Collection); end; destructor TCollectionItemAutoCreateFields.Destroy; begin AutoDestroyFields(self); inherited Destroy; end; {$endif LVCL} { TRawUTF8ObjectCacheSettings } constructor TRawUTF8ObjectCacheSettings.Create; begin inherited Create; // release after 2 minutes of inactivity by default fTimeOutMS := 2 * 60 * 1000; // 1 second periodicity of purge is small enough to be painless fPurgePeriodMS := 1000; end; { TRawUTF8ObjectCache } constructor TRawUTF8ObjectCache.Create(aOwner: TRawUTF8ObjectCacheList; const aKey: RawUTF8); begin inherited Create; fOwner := aOwner; fKey := aKey; fOwner.Log('%.Create(%)', [ClassType, fKey]); fTimeoutMS := fOwner.fSettings.TimeOutMS; end; destructor TRawUTF8ObjectCache.Destroy; begin fOwner.Log('%.Destroy %', [ClassType, fKey]); CacheClear; inherited Destroy; end; procedure TRawUTF8ObjectCache.CacheSet; begin // gives some addition TTL time fTimeoutTix := GetTickCount64 + fTimeoutMS; end; procedure TRawUTF8ObjectCache.CacheClear; begin fTimeoutTix := 0; // indicates no service is available end; function TRawUTF8ObjectCache.Resolve(const aInterface: TGUID; out Obj): boolean; begin if Assigned(fOwner.OnKeyResolve) then result := fOwner.OnKeyResolve(aInterface,fKey,Obj) else result := false; end; { TRawUTF8ObjectCacheList } constructor TRawUTF8ObjectCacheList.Create(aClass: TRawUTF8ObjectCacheClass; aSettings: TRawUTF8ObjectCacheSettings; aLog: TSynLogFamily; aLogEvent: TSynLogInfo; const aOnKeyResolve: TOnKeyResolve); begin inherited Create([fObjectsOwned,fNoDuplicate,fCaseSensitive]); fClass := aClass; fSettings := aSettings; if (fClass = nil) or (fClass = TRawUTF8ObjectCache) or (fSettings = nil) then raise ESynException.CreateUTF8('%.Create(nil)', [self]); if (fSettings.PurgePeriodMS > 0) and (fSettings.TimeOutMS > 0) then fNextPurgeTix := GetTickCount64 + fSettings.PurgePeriodMS; fLog := aLog; fLogEvent := aLogEvent; fOnKeyResolve := aOnKeyResolve; fPurgeForceList := TRawUTF8List.Create([fCaseSensitive]); end; destructor TRawUTF8ObjectCacheList.Destroy; begin inherited Destroy; fPurgeForceList.Free; end; procedure TRawUTF8ObjectCacheList.Log(const TextFmt: RawUTF8; const TextArgs: array of const; Level: TSynLogInfo); begin {$ifdef WITHLOG} if (self=nil) or (fLog=nil) then exit; if Level=sllNone then Level := fLogEvent; fLog.SynLog.Log(Level, TextFmt, TextArgs, self); {$endif} end; function TRawUTF8ObjectCacheList.NewObjectCache(const Key: RawUTF8): TRawUTF8ObjectCache; begin result := fClass.Create(self, Key); end; procedure TRawUTF8ObjectCacheList.TryPurge; begin fSafe.Lock; try if ((fNextPurgeTix <> 0) and (GetTickCount64 > fNextPurgeTix)) or (fPurgeForceList.Count > 0) then DoPurge; finally fSafe.UnLock; end; end; procedure TRawUTF8ObjectCacheList.AddToPurge(const Key: RawUTF8); var i: PtrInt; begin fSafe.Lock; try i := IndexOf(Key); if i>=0 then fPurgeForceList.Add(Key); finally fSafe.UnLock; end; end; procedure TRawUTF8ObjectCacheList.ForceCacheClear; var i: integer; cache: TRawUTF8ObjectCache; {$ifdef WITHLOG} log: ISynLog; // for Enter auto-leave to work with FPC {$endif} begin fSafe.Lock; try {$ifdef WITHLOG} log := fLog.SynLog.Enter('ForceCacheClear of % entries',[fCount],self); {$endif} for i := 0 to fCount - 1 do begin cache := TRawUTF8ObjectCache(fObjects[i]); cache.fSafe.Lock; try cache.CacheClear; finally cache.fSafe.UnLock; end; end; finally fSafe.UnLock; end; end; procedure TRawUTF8ObjectCacheList.DoPurge; var tix: Int64; i,n: integer; purged: RawUTF8; cache: TRawUTF8ObjectCache; {$ifdef WITHLOG} log: ISynLog; // for Enter auto-leave to work with FPC {$endif} procedure InternalPurge(nochecktimeout: boolean); begin // test again the timeout after acquiring the TRawUTF8ObjectCache lock try cache.Safe.Lock; if nochecktimeout or ((cache.fTimeoutTix > 0) and (tix > cache.fTimeoutTix)) then begin {$ifdef WITHLOG} if log = nil then log := fLog.SynLog.Enter('DoPurge(%)', [fClass], self); {$endif} cache.CacheClear; purged := purged + ' ' + cache.fKey; end; finally cache.Safe.UnLock; end; end; begin // called within fSafe.Lock tix := GetTickCount64; try n := fPurgeForceList.Count; if n > 0 then begin for i := 0 to n - 1 do begin cache := GetObjectFrom(fPurgeForceList.Strings[i]); InternalPurge(true); end; fPurgeForceList.Clear; end; for i := 0 to fCount - 1 do begin cache := TRawUTF8ObjectCache(fObjects[i]); if (cache.fTimeoutTix > 0) and (tix > cache.fTimeoutTix) then InternalPurge({checktimeout=}true); end; {$ifdef WITHLOG} if log <> nil then log.Log(fLogEvent, '%.ReleaseServices:% - count=%', [fClass, purged, fCount], self); {$endif} finally fNextPurgeTix := tix + fSettings.PurgePeriodMS; end; end; function TRawUTF8ObjectCacheList.GetLocked(const Key: RawUTF8; out cache: TRawUTF8ObjectCache; onlyexisting: boolean): boolean; begin result := false; if Key = '' then exit; fSafe.Lock; try if ((fNextPurgeTix <> 0) and (GetTickCount64 > fNextPurgeTix)) or (fPurgeForceList.Count > 0) then DoPurge; // inline TryPurge within the locked list cache := GetObjectFrom(Key); if cache = nil then begin if onlyexisting then begin Log('GetLocked(%): onlyexisting=true -> no new %', [Key, fClass]); exit; end; cache := NewObjectCache(Key); if cache = nil then begin Log('GetLocked: Invalid key - NewObjectCache(%) returned no %', [Key, fClass]); exit; end; AddObjectUnique(Key,@cache); Log('GetLocked: Added %[%] - count=%', [fClass, Key, fCount]) end else if cache.fTimeOutTix = 0 then Log('GetLocked: Using blank %[%]', [fClass, Key]) else Log('GetLocked: Using %[%] with timeout in % ms', [fClass, Key, cache.fTimeOutTix - GetTickCount64]); cache.fSafe.Lock; result := true; finally fSafe.UnLock; end; end; { TAlgoDeflate } type // implements the AlgoDeflate global variable TAlgoDeflate = class(TAlgoCompressWithNoDestLen) protected fDeflateLevel: integer; function RawProcess(src,dst: pointer; srcLen,dstLen,dstMax: integer; process: TAlgoCompressWithNoDestLenProcess): integer; override; public constructor Create; override; function AlgoID: byte; override; function AlgoCompressDestLen(PlainLen: integer): integer; override; end; constructor TAlgoDeflate.Create; begin inherited Create; fDeflateLevel := 6; end; function TAlgoDeflate.AlgoID: byte; begin result := 2; end; function TAlgoDeflate.RawProcess(src,dst: pointer; srcLen,dstLen,dstMax: integer; process: TAlgoCompressWithNoDestLenProcess): integer; begin case process of doCompress: result := SynZip.CompressMem(src,dst,srcLen,dstLen,fDeflateLevel); doUnCompress, doUncompressPartial: result := SynZip.UnCompressMem(src,dst,srcLen,dstLen); else result := 0; end; end; function TAlgoDeflate.AlgoCompressDestLen(PlainLen: integer): integer; begin result := PlainLen+256+PlainLen shr 3; end; { TAlgoDeflateFast } type // implements the AlgoDeflateFast global variable TAlgoDeflateFast = class(TAlgoDeflate) public constructor Create; override; function AlgoID: byte; override; end; function TAlgoDeflateFast.AlgoID: byte; begin result := 3; end; constructor TAlgoDeflateFast.Create; begin inherited Create; fDeflateLevel := 1; end; { TServiceMethod } function TServiceMethod.ArgIndex(ArgName: PUTF8Char; ArgNameLen: integer; Input: boolean): integer; begin if ArgNameLen>0 then if Input then begin for result := ArgsInFirst to ArgsInLast do with Args[result] do if IdemPropName(ParamName^,ArgName,ArgNameLen) then if ValueDirection in [smdConst,smdVar] then exit else // found break; // right name, but wrong direction end else for result := ArgsOutFirst to ArgsOutLast do with Args[result] do if IdemPropName(ParamName^,ArgName,ArgNameLen) then if ValueDirection in [smdVar,smdOut,smdResult] then exit else // found break; // right name, but wrong direction result := -1; end; function TServiceMethod.ArgNext(var arg: integer; Input: boolean): boolean; begin result := true; inc(arg); if Input then while arg<=ArgsInLast do if Args[arg].ValueDirection in [smdConst,smdVar] then exit else inc(arg) else while arg<=ArgsOutLast do if Args[arg].ValueDirection in [smdVar,smdOut,smdResult] then exit else inc(arg); result := false; end; function TServiceMethod.ArgsArrayToObject(P: PUTF8Char; Input: boolean): RawUTF8; var i: integer; W: TTextWriter; Value: PUTF8Char; temp: TTextWriterStackBuffer; begin W := TTextWriter.CreateOwnedStream(temp); try W.Add('{'); if (P=nil) or (P^<>'[') then P := nil else inc(P); for i := 1 to length(Args)-1 do if P=nil then break else with Args[i] do begin if Input then begin if ValueDirection in [smdOut,smdResult] then continue; end else if ValueDirection=smdConst then continue; W.AddPropName(ParamName^); P := GotoNextNotSpace(P); Value := P; P := GotoEndJSONItem(P); if P^=',' then inc(P); // include ending ',' W.AddNoJsonEscape(Value,P-Value); end; W.CancelLastComma; W.Add('}'); W.SetText(result); finally W.Free; end; end; function TServiceMethod.ArgsCommandLineToObject(P: PUTF8Char; Input, RaiseExceptionOnUnknownParam: boolean): RawUTF8; var i: integer; W: TTextWriter; B: PUTF8Char; arginfo: PServiceMethodArgument; arg, value: RawUTF8; ok: boolean; temp: TTextWriterStackBuffer; begin W := TTextWriter.CreateOwnedStream(temp); try W.Add('{'); while (P<>nil) and GetNextFieldProp(P,arg) and (P<>nil) and (arg<>'') do begin ok := true; i := ArgIndex(pointer(arg),length(arg),Input); if i<0 then if RaiseExceptionOnUnknownParam then raise EServiceException.CreateUTF8('Unexpected [%] parameter for %', [arg,InterfaceDotMethodName]) else ok := false; arginfo := @Args[i]; if ok then W.AddPropName(arginfo^.ParamName^); if not (P^ in [':','=']) then raise EServiceException.CreateUTF8('"%" parameter has no = for %', [arg,InterfaceDotMethodName]); P := GotoNextNotSpace(P+1); if P^ in ['"','[','{'] then begin // name='"value"' or name='{somejson}' B := P; P := GotoEndJSONItem(P); if P = nil then raise EServiceException.CreateUTF8('%= parameter has invalid content for %', [arg,InterfaceDotMethodName]); if not ok then continue; W.AddNoJSONEscape(B,P-B); end else begin // name=value GetNextItem(P,' ',value); if not ok then continue; if arginfo^.ValueType=smvDynArray then // write [value] or ["value"] W.Add('['); if arginfo^.ValueKindAsm*[vIsString,vIsDynArrayString]<>[] then W.AddJSONString(value) else W.AddNoJSONEscape(pointer(value),length(value)); if arginfo^.ValueType=smvDynArray then W.Add(']'); end; W.Add(','); end; W.CancelLastComma; W.Add('}'); W.SetText(result); finally W.Free; end; end; function TServiceMethod.ArgsNames(Input: Boolean): TRawUTF8DynArray; var a,n: integer; begin result := nil; if Input then begin SetLength(result,ArgsInputValuesCount); n := 0; for a := ArgsInFirst to ArgsInLast do if Args[a].ValueDirection in [smdConst,smdVar] then begin ShortStringToAnsi7String(Args[a].ParamName^,result[n]); inc(n); end; end else begin SetLength(result,ArgsOutputValuesCount); n := 0; for a := ArgsOutFirst to ArgsOutLast do if Args[a].ValueDirection in [smdVar,smdOut,smdResult] then begin ShortStringToAnsi7String(Args[a].ParamName^,result[n]); inc(n); end; end; end; {$ifndef NOVARIANTS} procedure TServiceMethod.ArgsStackAsDocVariant(const Values: TPPointerDynArray; out Dest: TDocVariantData; Input: Boolean); var a: integer; begin if Input then begin Dest.InitFast(ArgsInputValuesCount,dvObject); for a := ArgsInFirst to ArgsInLast do if Args[a].ValueDirection in [smdConst,smdVar] then Args[a].AddAsVariant(Dest,Values[a]); end else begin Dest.InitFast(ArgsOutputValuesCount,dvObject); for a := ArgsOutFirst to ArgsOutLast do if Args[a].ValueDirection in [smdVar,smdOut,smdResult] then Args[a].AddAsVariant(Dest,Values[a]); end; end; procedure TServiceMethod.ArgsValuesAsDocVariant(Kind: TServiceMethodParamsDocVariantKind; out Dest: TDocVariantData; const Values: TVariantDynArray; Input: boolean; Options: TDocVariantOptions); begin case Kind of pdvObject, pdvObjectFixed: begin Dest.InitObjectFromVariants(ArgsNames(Input),Values,Options); if Kind=pdvObjectFixed then ArgsAsDocVariantFix(Dest,Input); end; pdvArray: Dest.InitArrayFromVariants(Values,Options); else Dest.Init(Options); end; end; procedure TServiceMethod.ArgsAsDocVariantObject(const ArgsParams: TDocVariantData; var ArgsObject: TDocVariantData; Input: boolean); var a,n: integer; begin if (ArgsParams.Count=0) or (ArgsParams.Kind<>dvArray) then exit; if ArgsObject.Kind=dvUndefined then ArgsObject.Init(ArgsParams.Options); ArgsObject.Capacity := ArgsObject.Count+ArgsParams.Count; n := 0; if Input then begin if ArgsParams.Count=integer(ArgsInputValuesCount) then for a := ArgsInFirst to ArgsInLast do if Args[a].ValueDirection in [smdConst,smdVar] then begin ArgsObject.AddValue(ShortStringToAnsi7String(Args[a].ParamName^), ArgsParams.Values[n]); inc(n); end; end else begin if ArgsParams.Count=integer(ArgsOutputValuesCount) then for a := ArgsOutFirst to ArgsOutLast do if Args[a].ValueDirection in [smdVar,smdOut,smdResult] then begin ArgsObject.AddValue(ShortStringToAnsi7String(Args[a].ParamName^), ArgsParams.Values[n]); inc(n); end; end; end; procedure TServiceMethod.ArgsAsDocVariantFix(var ArgsObject: TDocVariantData; Input: boolean); var a,ndx: integer; doc: TDocVariantData; begin if ArgsObject.Count>0 then case ArgsObject.Kind of dvObject: for a := 0 to ArgsObject.Count-1 do begin ndx := ArgIndex(pointer(ArgsObject.Names[a]),length(ArgsObject.Names[a]),Input); if ndx>=0 then Args[ndx].FixValue(ArgsObject.Values[a]); end; dvArray: if Input then begin if ArgsObject.Count<>integer(ArgsInputValuesCount) then exit; doc.Init(ArgsObject.Options); for a := ArgsInFirst to ArgsInLast do if Args[a].ValueDirection in [smdConst,smdVar] then Args[a].FixValueAndAddToObject(ArgsObject.Values[doc.Count],doc); ArgsObject := doc; end else begin if ArgsObject.Count<>integer(ArgsOutputValuesCount) then exit; doc.Init(ArgsObject.Options); for a := ArgsOutFirst to ArgsOutLast do if Args[a].ValueDirection in [smdVar,smdOut,smdResult] then Args[a].FixValueAndAddToObject(ArgsObject.Values[doc.Count],doc); ArgsObject := doc; end; end; end; {$endif NOVARIANTS} { TServiceMethodExecute } constructor TServiceMethodExecute.Create(aMethod: PServiceMethod); var a: integer; begin with aMethod^ do begin if ArgsUsedCount[smvv64]>0 then SetLength(fInt64s,ArgsUsedCount[smvv64]); if ArgsUsedCount[smvvObject]>0 then SetLength(fObjects,ArgsUsedCount[smvvObject]); if ArgsUsedCount[smvvInterface]>0 then SetLength(fInterfaces,ArgsUsedCount[smvvInterface]); if ArgsUsedCount[smvvRecord]>0 then SetLength(fRecords,ArgsUsedCount[smvvRecord]); if ArgsUsedCount[smvvDynArray]>0 then SetLength(fDynArrays,ArgsUsedCount[smvvDynArray]); SetLength(fValues,length(Args)); for a := ArgsManagedFirst to ArgsManagedLast do with Args[a] do case ValueType of smvDynArray: with fDynArrays[IndexVar] do Wrapper.InitFrom(DynArrayWrapper,Value); smvRecord: SetLength(fRecords[IndexVar],ArgTypeInfo^.RecordType^.Size); {$ifndef NOVARIANTS} smvVariant: SetLength(fRecords[IndexVar],SizeOf(Variant)); {$endif} end; end; fMethod := aMethod; end; destructor TServiceMethodExecute.Destroy; begin fTempTextWriter.Free; inherited Destroy; end; procedure TServiceMethodExecute.AddInterceptor(const Hook: TServiceMethodExecuteEvent); begin MultiEventAdd(fOnExecute,TMethod(Hook)); end; procedure TServiceMethodExecute.BeforeExecute; var a: integer; Value: PPointer; begin with fMethod^ do begin if ArgsUsedCount[smvvRawUTF8]>0 then SetLength(fRawUTF8s,ArgsUsedCount[smvvRawUTF8]); if ArgsUsedCount[smvvString]>0 then SetLength(fStrings,ArgsUsedCount[smvvString]); if ArgsUsedCount[smvvWideString]>0 then SetLength(fWideStrings,ArgsUsedCount[smvvWideString]); if fAlreadyExecuted then begin if ArgsUsedCount[smvvObject]>0 then FillCharFast( fObjects,ArgsUsedCount[smvvObject]*SizeOf(TObject),0); if ArgsUsedCount[smvv64]>0 then FillCharFast(fInt64s,ArgsUsedCount[smvv64]*SizeOf(Int64),0); if ArgsUsedCount[smvvInterface]>0 then FillCharFast(fInterfaces,ArgsUsedCount[smvvInterface]*SizeOf(pointer),0); end; Value := @fValues[1]; for a := 1 to high(Args) do with Args[a] do begin case ValueVar of smvv64: Value^ := @fInt64s[IndexVar]; smvvRawUTF8: Value^ := @fRawUTF8s[IndexVar]; smvvString: Value^ := @fStrings[IndexVar]; smvvWideString: Value^ := @fWideStrings[IndexVar]; smvvObject: begin Value^ := @fObjects[IndexVar]; PPointer(Value^)^ := ArgTypeInfo^.ClassCreate; end; smvvInterface: Value^ := @fInterfaces[IndexVar]; smvvRecord: begin Value^ := pointer(fRecords[IndexVar]); if fAlreadyExecuted then FillCharFast(Value^^,ArgTypeInfo^.RecordType^.Size,0); end; smvvDynArray: Value^ := @fDynArrays[IndexVar].Value; else raise EInterfaceFactoryException.CreateUTF8('I%.%:% ValueType=%', [InterfaceDotMethodName,ParamName^,ArgTypeName^,ord(ValueType)]); end; inc(Value); end; if optInterceptInputOutput in Options then begin Input.InitFast(ArgsInputValuesCount,dvObject); Output.InitFast(ArgsOutputValuesCount,dvObject); end; end; fAlreadyExecuted := true; end; procedure TServiceMethodExecute.RawExecute(const Instances: PPointerArray; InstancesLast: integer); var Value: pointer; a,i,e: integer; call: TCallMethodArgs; Stack: packed array[0..MAX_EXECSTACK-1] of byte; begin FillCharFast(call,SizeOf(call),0); with fMethod^ do begin // create the stack and register content {$ifdef CPUX86} call.StackAddr := PtrInt(@Stack[0]); call.StackSize := ArgsSizeInStack; {$ifndef MSWINDOWS} // ensure always aligned by 16 bytes on POSIX while call.StackSize and 15<>0 do inc(call.StackSize,PTRSIZ); // needed for Darwin and Linux i386 {$endif MSWINDOWS} {$else} {$ifdef CPUINTEL} call.StackSize := ArgsSizeInStack shr 3; // ensure stack aligned on 16 bytes (paranoid) if call.StackSize and 1 <> 0 then inc(call.StackSize); // stack is filled reversed (RTL) call.StackAddr := PtrInt(@Stack[call.StackSize*8-8]); {$else} // stack is filled normally (LTR) call.StackAddr := PtrInt(@Stack[0]); {$ifdef CPUAARCH64} call.StackSize := ArgsSizeInStack shr 3; // ensure stack aligned on 16 bytes (mandatory: needed for correct low level asm) if call.StackSize and 1 <> 0 then inc(call.StackSize); {$else} call.StackSize := ArgsSizeInStack shr 2; {$endif CPUAARCH64} {$endif CPUINTEL} {$endif CPUX86} for a := 1 to length(Args)-1 do with Args[a] do begin Value := fValues[a]; if (ValueDirection<>smdConst) or (ValueType in [smvRecord{$ifndef NOVARIANTS},smvVariant{$endif}]) then begin // pass by reference if (RegisterIdent=0) and (FPRegisterIdent=0) and (SizeInStack>0) then MoveFast(Value,Stack[InStackOffset],SizeInStack) else begin if RegisterIdent>0 then call.ParamRegs[RegisterIdent] := PtrInt(Value); if FPRegisterIdent>0 then raise EInterfaceFactoryException.CreateUTF8('Unexpected % FPReg=%', [ParamName^,FPRegisterIdent]); // should never happen end; end else begin // pass by value if (RegisterIdent=0) AND (FPRegisterIdent=0) AND (SizeInStack>0) then MoveFast(Value^,Stack[InStackOffset],SizeInStack) else begin if (RegisterIdent>0) then begin call.ParamRegs[RegisterIdent] := PPtrInt(Value)^; {$ifdef CPUARM} // for e.g. INT64 on 32-bit ARM systems; these are also passed in the normal registers if SizeInStack>PTRSIZ then call.ParamRegs[RegisterIdent+1] := PPtrInt(Value+PTRSIZ)^; {$endif} end; {$ifndef CPUX86} if FPRegisterIdent>0 then call.FPRegs[FPRegisterIdent] := unaligned(PDouble(Value)^); {$endif} if (RegisterIdent>0) and (FPRegisterIdent>0) then raise EInterfaceFactoryException.CreateUTF8('Unexpected % reg=% FP=%', [ParamName^,RegisterIdent,FPRegisterIdent]); // should never happen end; end; end; // execute the method for i := 0 to InstancesLast do begin // handle method execution interception fCurrentStep := smsBefore; if fOnExecute<>nil then begin if (Input.Count=0) and (optInterceptInputOutput in Options) then ArgsStackAsDocVariant(fValues,fInput,true); for e := 0 to length(fOnExecute)-1 do try fOnExecute[e](self,smsBefore); except // ignore any exception during interception end; end; // prepare the low-level call context for the asm stub //Pass the Self (also named $this) call.ParamRegs[PARAMREG_FIRST] := PtrInt(Instances[i]); call.method := PPtrIntArray(PPointer(Instances[i])^)^[ExecutionMethodIndex]; if ArgsResultIndex>=0 then call.resKind := Args[ArgsResultIndex].ValueType else call.resKind := smvNone; // launch the asm stub in the expected execution context try {$ifndef LVCL} if (optExecInMainThread in Options) and (GetCurrentThreadID<>MainThreadID) then BackgroundExecuteCallMethod(@call,nil) else {$endif} if optExecInPerInterfaceThread in Options then if Assigned(BackgroundExecutionThread) then BackgroundExecuteCallMethod(@call,BackgroundExecutionThread) else raise EInterfaceFactoryException.Create('optExecInPerInterfaceThread'+ ' with BackgroundExecutionThread=nil') else CallMethod(call); if (ArgsResultIndex>=0) and (Args[ArgsResultIndex].ValueVar=smvv64) then PInt64Rec(fValues[ArgsResultIndex])^ := call.res64; // handle method execution interception fCurrentStep := smsAfter; if fOnExecute<>nil then begin if (Output.Count=0) and (optInterceptInputOutput in Options) then ArgsStackAsDocVariant(fValues,fOutput,false); for e := 0 to length(fOnExecute)-1 do try fOnExecute[e](self,smsAfter); except // ignore any exception during interception end; end; except // also intercept any error during method execution on Exc: Exception do begin fCurrentStep := smsError; if fOnExecute<>nil then begin fLastException := Exc; for e := 0 to length(fOnExecute)-1 do try fOnExecute[e](self,smsError); except // ignore any exception during interception end; fLastException := nil; end; if (InstancesLast=0) and not (optIgnoreException in Options) then raise; // single caller expects exception to be propagated if fExecutedInstancesFailed=nil then // multiple Instances[] execution SetLength(fExecutedInstancesFailed,InstancesLast+1); fExecutedInstancesFailed[i] := ObjectToJSONDebug(Exc); end; end; end; end; end; function TServiceMethodExecute.TempTextWriter: TJSONSerializer; begin if fTempTextWriter=nil then begin fTempTextWriter := TJSONSerializer.CreateOwnedStream; fTempTextWriter.fCustomOptions := fTempTextWriter.fCustomOptions + [twoForceJSONExtended,twoIgnoreDefaultInRecord]; // shorter end; result := fTempTextWriter; end; procedure TServiceMethodExecute.AfterExecute; var i,a: integer; begin Finalize(fRawUTF8s); Finalize(fStrings); Finalize(fWideStrings); with fMethod^ do if ArgsManagedFirst>=0 then begin for i := 0 to ArgsUsedCount[smvvObject]-1 do fObjects[i].Free; for i := 0 to ArgsUsedCount[smvvInterface]-1 do IUnknown(fInterfaces[i]) := nil; for i := 0 to ArgsUsedCount[smvvDynArray]-1 do fDynArrays[i].Wrapper.Clear; // will handle T*ObjArray, and set Value^=nil if fRecords<>nil then begin i := 0; for a := ArgsManagedFirst to ArgsManagedLast do with Args[a] do case ValueType of smvRecord: begin RecordClear(fRecords[i][0],ArgTypeInfo); inc(i); end; {$ifndef NOVARIANTS} smvVariant: begin VarClear(PVariant(fRecords[i])^); // fast, even for simple types inc(i); end; {$endif} end; end; end; end; function TServiceMethodExecute.ExecuteJsonCallback(Instance: pointer; const params: RawUTF8; output: PRawUTF8): boolean; var tmp: TSynTempBuffer; fake: TInterfacedObjectFake; WR: TTextWriter; n: integer; begin result := false; if Instance=nil then exit; if (PCardinal(PPointer(PPointer(Instance)^)^)^= PCardinal(@TInterfacedObjectFake.FakeQueryInterface)^) then begin fake := TInterfacedObjectFake(Instance).SelfFromInterface; if Assigned(fake.fInvoke) then begin // call SOA/fake interface? -> bypass all JSON marshalling if (output=nil) and (fMethod^.ArgsOutputValuesCount>0) then exit; // ensure a function has a TOnAsynchRedirectResult callback result := fake.fInvoke(fMethod^,params,output,nil,nil,nil); exit; end; end; n := length(params); tmp.Init(n+2); try PAnsiChar(tmp.buf)[0] := '['; MoveFast(pointer(params)^,PAnsiChar(tmp.buf)[1],n); PWord(PAnsiChar(tmp.buf)+n+1)^ := ord(']'); // ']'#0 if output<>nil then begin WR := TempTextWriter; WR.CancelAll; end else if fMethod^.ArgsOutputValuesCount>0 then exit else // ensure a function has a TOnAsynchRedirectResult callback WR := nil; result := ExecuteJson([Instance],tmp.buf,WR); if WR<>nil then WR.SetText(output^); finally tmp.Done; end; end; function TServiceMethodExecute.ExecuteJsonFake(Instance: pointer; params: PUTF8Char): boolean; var tmp: RawUTF8; len: integer; begin result := false; if not Assigned(TInterfacedObjectFake(Instance).fInvoke) then exit; if params<>nil then begin if params^<>'[' then exit; inc(params); len := StrLen(params); if params[len-1]=']' then dec(len); FastSetString(tmp,params,len); end; result := TInterfacedObjectFake(Instance).fInvoke(fMethod^,tmp,nil,nil,nil,nil); end; function TServiceMethodExecute.ExecuteJson(const Instances: array of pointer; Par: PUTF8Char; Res: TTextWriter; Error: PShortString; ResAsJSONObject: boolean): boolean; var a,a1: integer; Val, Name: PUTF8Char; NameLen: integer; EndOfObject: AnsiChar; opt: array[{smdVar=}boolean] of TTextWriterWriteObjectOptions; ParObjValuesUsed: boolean; ParObjValues: array[0..MAX_METHOD_ARGS-1] of PUTF8Char; begin result := false; BeforeExecute; with fMethod^ do try // validate input parameters ParObjValuesUsed := false; if (ArgsInputValuesCount<>0) and (Par<>nil) then begin if Par^ in [#1..' '] then repeat inc(Par) until not(Par^ in [#1..' ']); case Par^ of '[': // input arguments as a JSON array , e.g. '[1,2,"three"]' (default) inc(Par); '{': begin // retrieve parameters values from JSON object repeat inc(Par) until not(Par^ in [#1..' ']); if Par<>'}' then begin ParObjValuesUsed := true; FillCharFast(ParObjValues,(ArgsInLast+1)*SizeOf(pointer),0); // := nil a1 := ArgsInFirst; repeat Name := GetJSONPropName(Par,@NameLen); if Name=nil then exit; // invalid JSON object in input Val := Par; Par := GotoNextJSONItem(Par,1,@EndOfObject); for a := a1 to ArgsInLast do with Args[a] do if ValueDirection<>smdOut then if IdemPropName(ParamName^,Name,NameLen) then begin ParObjValues[a] := Val; // fast redirection, without allocation if a=a1 then inc(a1); // enable optimistic O(1) search for in-order input break; end; until (Par=nil) or (EndOfObject='}'); end; Par := nil; end; else if PInteger(Par)^=NULL_LOW then Par := nil else exit; // only support JSON array or JSON object as input end; end; // decode input parameters (if any) in f*[] if (Par=nil) and not ParObjValuesUsed then begin if (ArgsInputValuesCount>0) and (optErrorOnMissingParam in Options) then exit; // paranoid setting end else for a := ArgsInFirst to ArgsInLast do with Args[a] do if ValueDirection<>smdOut then begin if ParObjValuesUsed then if ParObjValues[a]=nil then // missing parameter in input JSON if optErrorOnMissingParam in Options then exit else continue else // ignore and leave void value by default Par := ParObjValues[a] else // value to be retrieved from JSON object if Par=nil then break; // premature end of ..] (ParObjValuesUsed=false) case ValueType of smvInterface: if Assigned(OnCallback) then OnCallback(Par,ArgTypeInfo,fInterfaces[IndexVar]) else raise EInterfaceFactoryException.CreateUTF8('OnCallback=nil for %(%: %)', [InterfaceDotMethodName,ParamName^,ArgTypeName^]); smvDynArray: begin Par := fDynArrays[IndexVar].Wrapper.LoadFromJSON(Par); if Par=nil then exit; IgnoreComma(Par); end; else if not FromJSON(InterfaceDotMethodName,Par,fValues[a],Error {$ifndef NOVARIANTS},JSON_OPTIONS[optVariantCopiedByReference in Options]{$endif}) then exit; end; end; // execute the method, using prepared values in f*[] RawExecute(@Instances[0],high(Instances)); // send back any result if Res<>nil then begin // handle custom content (not JSON array/object answer) if ArgsResultIsServiceCustomAnswer then with PServiceCustomAnswer(fValues[ArgsResultIndex])^ do if Header<>'' then begin fServiceCustomAnswerHead := Header; Res.ForceContent(Content); if Status=0 then // Values[]=@Records[] is filled with 0 by default fServiceCustomAnswerStatus := HTTP_SUCCESS else fServiceCustomAnswerStatus := Status; Result := true; exit; end; // write the '{"result":[...' array or object opt[{smdVar=}false] := DEFAULT_WRITEOPTIONS[optDontStoreVoidJSON in Options]; opt[{smdVar=}true] := []; // let var params override void/default values for a := ArgsOutFirst to ArgsOutLast do with Args[a] do if ValueDirection in [smdVar,smdOut,smdResult] then begin if ResAsJSONObject then Res.AddPropName(ParamName^); case ValueType of smvDynArray: begin if vIsObjArray in ValueKindAsm then Res.AddObjArrayJSON(fValues[a]^,opt[ValueDirection=smdVar]) else Res.AddDynArrayJSON(fDynArrays[IndexVar].Wrapper); Res.Add(','); end; else AddJSON(Res,fValues[a],opt[ValueDirection=smdVar]); end; end; Res.CancelLastComma; end; Result := true; finally AfterExecute; end; end; { TSQLRecordServiceLog } class procedure TSQLRecordServiceLog.InitializeTable( Server: TSQLRestServer; const FieldName: RawUTF8; Options: TSQLInitializeTableOptions); begin inherited; if FieldName='' then Server.CreateSQLMultiIndex(Self,['Method','MicroSec'],false); end; { TSQLRecordServiceNotifications } class procedure TSQLRecordServiceNotifications.InitializeTable( Server: TSQLRestServer; const FieldName: RawUTF8; Options: TSQLInitializeTableOptions); begin inherited; if (FieldName='') or (FieldName='Sent') then Server.CreateSQLMultiIndex(Self,['Sent'],false); end; class function TSQLRecordServiceNotifications.LastEventsAsObjects(Rest: TSQLRest; LastKnownID: TID; Limit: integer; Service: TInterfaceFactory; out Dest: TDocVariantData; const MethodName: RawUTF8; IDAsHexa: boolean): boolean; var res: TSQLRecordServiceNotifications; begin res := CreateAndFillPrepare(Rest,'ID > ? order by ID limit %',[Limit], [LastKnownID],'ID,Method,Input'); try if res.FillTable.RowCount > 0 then begin res.SaveFillInputsAsObjects(Service,Dest,MethodName,IDAsHexa); result := true; end else result := false; finally res.Free; end; end; function TSQLRecordServiceNotifications.SaveInputAsObject(Service: TInterfaceFactory; const MethodName: RawUTF8; IDAsHexa: boolean): variant; var m: integer; begin VarClear(result); with TDocVariantData(result) do if IDAsHexa then InitObject(['ID',Int64ToHex(fID),MethodName,Method],JSON_OPTIONS_FAST) else InitObject(['ID',fID,MethodName,Method],JSON_OPTIONS_FAST); m := Service.FindMethodIndex(Method); if m>=0 then Service.Methods[m].ArgsAsDocVariantObject(_Safe(fInput)^,TDocVariantData(result),true); end; procedure TSQLRecordServiceNotifications.SaveFillInputsAsObjects(Service: TInterfaceFactory; out Dest: TDocVariantData; const MethodName: RawUTF8; IDAsHexa: boolean); begin Dest.InitFast(FillTable.RowCount,dvArray); while FillOne do Dest.AddItem(SaveInputAsObject(Service,MethodName,IDAsHexa)); end; { TServiceContainerClient } function TServiceContainerClient.Info(aTypeInfo: PTypeInfo): TServiceFactory; begin result := inherited Info(aTypeInfo); if (result=nil) and not fDisableAutoRegisterAsClientDriven then result := AddInterface(aTypeInfo,sicClientDriven); end; function TServiceContainerClient.CallBackUnRegister(const Callback: IInvokable): boolean; begin if Assigned(Callback) then result := (fRest as TSQLRestClientURI).fFakeCallbacks.UnRegister(pointer(Callback)) else result := false; end; { TInterfacedCallback } constructor TInterfacedCallback.Create(aRest: TSQLRest; const aGUID: TGUID); begin inherited Create; fRest := aRest; fInterface := aGUID; end; procedure TInterfacedCallback.CallbackRestUnregister; var Obj: pointer; // to avoid unexpected (recursive) Destroy call begin if (fRest<>nil) and (fRest.Services<>nil) and not IsNullGUID(fInterface) then if GetInterface(fInterface,Obj) then begin fRest.Services.CallBackUnRegister(IInvokable(Obj)); dec(fRefCount); // GetInterface() did increase the refcount fRest := nil; // notify once end; end; destructor TInterfacedCallback.Destroy; begin CallbackRestUnregister; inherited Destroy; end; { TBlockingCallback } constructor TBlockingCallback.Create(aTimeOutMs: integer; aRest: TSQLRest; const aGUID: TGUID); begin inherited Create(aRest,aGUID); fProcess := TBlockingProcess.Create(aTimeOutMs,fSafe); end; destructor TBlockingCallback.Destroy; begin FreeAndNil(fProcess); inherited Destroy; end; procedure TBlockingCallback.CallbackFinished(aRestForLog: TSQLRest; aServerUnregister: boolean); begin if fProcess.NotifyFinished then begin {$ifdef WITHLOG} if aRestForLog<>nil then aRestForLog.LogClass.Add.Log(sllTrace,self); {$endif} if aServerUnregister then CallbackRestUnregister; end; end; function TBlockingCallback.WaitFor: TBlockingEvent; begin result := fProcess.WaitFor; end; function TBlockingCallback.Reset: boolean; begin result := fProcess.Reset; end; function TBlockingCallback.GetEvent: TBlockingEvent; begin result := fProcess.Event; end; { TServiceRecordVersionCallback } constructor TServiceRecordVersionCallback.Create(aSlave: TSQLRestServer; aMaster: TSQLRestClientURI; aTable: TSQLRecordClass; aOnNotify: TOnBatchWrite); begin if aSlave=nil then raise EServiceException.CreateUTF8('%.Create(%): Slave=nil',[self,aTable]); fSlave := aSlave; fRecordVersionField := aTable.RecordProps.RecordVersionField; if fRecordVersionField=nil then raise EServiceException.CreateUTF8('%.Create: % has no TRecordVersion field', [self,aTable]); fTableDeletedIDOffset := Int64(fSlave.Model.GetTableIndexExisting(aTable)) shl SQLRECORDVERSION_DELETEID_SHIFT; inherited Create(aMaster,IServiceRecordVersionCallback); fTable := aTable; fOnNotify := aOnNotify; end; procedure TServiceRecordVersionCallback.SetCurrentRevision( const Revision: TRecordVersion; Event: TSQLOccasion); begin if (RevisionsoInsert)) then raise EServiceException.CreateUTF8('%.SetCurrentRevision(%) on %: previous was %', [self,Revision,fTable,fSlave.fRecordVersionMax]); fSlave.fRecordVersionMax := Revision; end; procedure TServiceRecordVersionCallback.Added(const NewContent: RawJSON); var rec: TSQLRecord; fields: TSQLFieldBits; begin rec := fTable.Create; try rec.FillFrom(NewContent,@fields); if fBatch=nil then fSlave.Add(rec,true,true,true) else fBatch.Add(rec,true,true,fields,true); SetCurrentRevision(fRecordVersionField.PropInfo.GetInt64Prop(rec),soInsert); if Assigned(fOnNotify) then fOnNotify(fBatch,soInsert,fTable,rec.IDValue,rec,fields); finally rec.Free; end; end; procedure TServiceRecordVersionCallback.Updated(const ModifiedContent: RawJSON); var rec: TSQLRecord; fields: TSQLFieldBits; begin rec := fTable.Create; try rec.FillFrom(ModifiedContent,@fields); if fBatch=nil then fSlave.Update(rec,fields,true) else fBatch.Update(rec,fields,true); SetCurrentRevision(fRecordVersionField.PropInfo.GetInt64Prop(rec),soUpdate); if Assigned(fOnNotify) then fOnNotify(fBatch,soUpdate,fTable,rec.IDValue,rec,fields); finally rec.Free; end; end; procedure TServiceRecordVersionCallback.Deleted(const ID: TID; const Revision: TRecordVersion); var del: TSQLRecordTableDeleted; begin del := TSQLRecordTableDeleted.Create; try del.IDValue := fTableDeletedIDOffset+Revision; del.Deleted := ID; if fBatch=nil then try fSlave.fAcquireExecution[execORMWrite].fSafe.Lock; fSlave.fRecordVersionDeleteIgnore := true; fSlave.Add(del,true,true,true); fSlave.Delete(fTable,ID); finally fSlave.fRecordVersionDeleteIgnore := false; fSlave.fAcquireExecution[execORMWrite].Safe.UnLock; end else begin fBatch.Add(del,true,true); fBatch.Delete(fTable,ID); end; SetCurrentRevision(Revision,soDelete); if Assigned(fOnNotify) then fOnNotify(fBatch,soDelete,fTable,ID,nil,[]); finally del.Free; end; end; procedure TServiceRecordVersionCallback.CurrentFrame(isLast: boolean); procedure Error(const msg: RawUTF8); begin fRest.InternalLog('%.CurrentFrame(%) on %: %',[self,isLast,fTable,msg],sllError); end; begin if isLast then begin if fBatch=nil then Error('unexpected last frame'); end else if fBatch<>nil then Error('previous active BATCH -> send pending'); if fBatch<>nil then try fSlave.fAcquireExecution[execORMWrite].fSafe.Lock; fSlave.fRecordVersionDeleteIgnore := true; fSlave.BatchSend(fBatch); finally fSlave.fRecordVersionDeleteIgnore := false; fSlave.fAcquireExecution[execORMWrite].Safe.UnLock; FreeAndNil(fBatch); end; if not isLast then fBatch := TSQLRestBatch.Create(fSlave,nil,10000); end; destructor TServiceRecordVersionCallback.Destroy; var timeOut: Int64; begin try if fBatch<>nil then begin timeOut := GetTickCount64+2000; repeat SleepHiRes(1); // allow 2 seconds to process all pending frames if fBatch=nil then exit; until GetTickCount64>timeOut; fSlave.InternalLog('%.Destroy on %: active BATCH',[self,fTable],sllError); fSlave.BatchSend(fBatch); fBatch.Free; end; finally inherited Destroy; end; end; { TServiceFactoryClient } function TServiceFactoryClient.CreateFakeInstance: TInterfacedObject; var notify: TOnFakeInstanceDestroy; id: RawUTF8; begin if fInstanceCreation=sicClientDriven then notify := NotifyInstanceDestroyed else notify := nil; result := TInterfacedObjectFakeClient.Create(self,Invoke,notify); if not fDelayedInstance and (fInstanceCreation=sicClientDriven) and InternalInvoke(SERVICE_PSEUDO_METHOD[imInstance],'',@id) then // thread-safe initialization of the fClientDrivenID TInterfacedObjectFakeClient(result).fClientDrivenID := GetCardinal(pointer(id)); end; type TServiceFactoryClientNotificationThread = class(TSQLRestThread) protected fClient: TServiceFactoryClient; fRemote: TSQLRestClientURI; fRetryPeriodSeconds: Integer; procedure InternalExecute; override; procedure ProcessPendingNotification; function GetPendingCountFromDB: Int64; public constructor Create(aClient: TServiceFactoryClient; aRemote: TSQLRestClientURI; aRetryPeriodSeconds: Integer); reintroduce; end; constructor TServiceFactoryClientNotificationThread.Create( aClient: TServiceFactoryClient; aRemote: TSQLRestClientURI; aRetryPeriodSeconds: Integer); begin fClient := aClient; // cross-platform may run Execute as soon as Create is called if (fClient=nil) or (fClient.fSendNotificationsRest=nil) or (fClient.fSendNotificationsLogClass=nil) then raise EServiceException.CreateUTF8('%.Create(fClient.fSendNotifications=nil)',[self]); if aRetryPeriodSeconds<=0 then fRetryPeriodSeconds := 1 else fRetryPeriodSeconds := aRetryPeriodSeconds; if aRemote=nil then fRemote := fClient.fClient else fRemote := aRemote; inherited Create(fClient.fClient,false,false); end; function TServiceFactoryClientNotificationThread.GetPendingCountFromDB: Int64; begin if not fClient.fSendNotificationsRest.OneFieldValue( fClient.fSendNotificationsLogClass,'count(*)','Sent is null',[],[],result) then result := 0; end; procedure TServiceFactoryClientNotificationThread.ProcessPendingNotification; var pending: TSQLRecordServiceNotifications; params,error: RawUTF8; client: cardinal; pendings,count: integer; timer: TPrecisionTimer; begin // one at a time, since InternalInvoke() is the bottleneck pending := fClient.fSendNotificationsLogClass.Create( fClient.fSendNotificationsRest,'Sent is null order by id limit 1'); try if pending.IDValue=0 then begin pendings := GetPendingCountFromDB; fSafe.LockedInt64[0] := pendings; if pendings=0 then exit else raise EServiceException.CreateUTF8( '%.ProcessPendingNotification pending=% with no DB row',[self,pendings]); end; pendings := fSafe.LockedInt64[0]; timer.Start; VariantSaveJson(pending.Input,twJSONEscape,params); if (params<>'') and (params[1]='[') then params := copy(params,2,length(params)-2); // trim [..] for URI call client := pending.Session; if not fClient.InternalInvoke(pending.Method,params,nil,@error,@client,nil,fRemote) then begin if _Safe(pending.fOutput)^.GetAsInteger('errorcount',count) then inc(count) else count := 1; VarClear(pending.fOutput); TDocVariantData(pending.fOutput).InitObject(['errorcount',count, 'lasterror',error,'lasttime',NowUTCToString(true,'T'), 'lastelapsed',timer.Stop],JSON_OPTIONS_FAST_EXTENDED); fClient.fSendNotificationsRest.Update(pending,'Output',true); raise EServiceException.CreateUTF8( '%.ProcessPendingNotification failed for %(%) [ID=%,pending=%] on %: %', [self,pending.Method,params,pending.IDValue,pendings,fRemote,error]); end; fClient.fClient.InternalLog('ProcessPendingNotification %(%) in % [ID=%,pending=%]', [pending.Method,params,timer.Stop,pending.IDValue,pendings]); pending.Sent := TimeLogNowUTC; pending.MicroSec := timer.LastTimeInMicroSec; fClient.fSendNotificationsRest.Update(pending,'MicroSec,Sent',true); fSafe.LockedInt64Increment(0,-1); finally pending.Free; end; end; procedure TServiceFactoryClientNotificationThread.InternalExecute; var delay: integer; begin fSafe.LockedInt64[0] := GetPendingCountFromDB; delay := 50; while not Terminated do begin while fSafe.LockedInt64[0]>0 do try ProcessPendingNotification; delay := 0; if Terminated then exit; except SleepOrTerminated(fRetryPeriodSeconds*1000); // wait before retry end; if Terminated then exit; if delay<50 then inc(delay); SleepHiRes(delay); end; end; function TServiceFactoryClient.Invoke(const aMethod: TServiceMethod; const aParams: RawUTF8; aResult: PRawUTF8; aErrorMsg: PRawUTF8; aClientDrivenID: PCardinal; aServiceCustomAnswer: PServiceCustomAnswer): boolean; procedure SendNotificationsLog; var pending: TSQLRecordServiceNotifications; json: RawUTF8; begin pending := fSendNotificationsLogClass.Create; try pending.Method := aMethod.URI; json := '['+aParams+']'; TDocVariantData(pending.fInput).InitJSONInPlace(pointer(json),JSON_OPTIONS_FAST_EXTENDED); if (aClientDrivenID<>nil) and (aClientDrivenID^<>0) then begin pending.Session := aClientDrivenID^; fSendNotificationsRest.Add(pending,'Method,Input,Session'); end else fSendNotificationsRest.Add(pending,'Method,Input'); finally pending.Free; end; end; begin if (fSendNotificationsRest<>nil) and (aMethod.ArgsOutputValuesCount=0) then begin SendNotificationsLog; if fSendNotificationsThread<>nil then TServiceFactoryClientNotificationThread(fSendNotificationsThread). Safe.LockedInt64Increment(0,1); result := true; end else result := InternalInvoke( aMethod.URI,aParams,aResult,aErrorMsg,aClientDrivenID,aServiceCustomAnswer); end; class function TServiceFactoryClient.GetErrorMessage(status: integer): RawUTF8; begin case status of HTTP_UNAVAILABLE: result := 'Check the communication parameters and network config'; HTTP_NOTIMPLEMENTED: result := 'Server not reachable or broken connection'; HTTP_NOTALLOWED: result := 'Method forbidden for this User group'; HTTP_UNAUTHORIZED: result := 'No active session'; HTTP_FORBIDDEN: result := 'Security error'; HTTP_NOTACCEPTABLE: result := 'Invalid input parameters'; HTTP_NOTFOUND: result := 'Network problem or request timeout'; else result := ''; end; end; function TServiceFactoryClient.InternalInvoke(const aMethod: RawUTF8; const aParams: RawUTF8; aResult: PRawUTF8; aErrorMsg: PRawUTF8; aClientDrivenID: PCardinal; aServiceCustomAnswer: PServiceCustomAnswer; aClient: TSQLRestClientURI): boolean; var baseuri,uri,sent,resp,clientDrivenID,head,error,ct: RawUTF8; Values: array[0..1] of TValuePUTF8Char; status,m: integer; service: PServiceMethod; ctxt: TSQLRestServerURIContextClientInvoke; withinput: boolean; {$ifdef WITHLOG} log: ISynLog; // for Enter auto-leave to work with FPC p: RawUTF8; {$endif} procedure DoClientCall; begin uri := baseuri; fRest.ServicesRouting.ClientSideInvoke(uri,ctxt,aMethod,aParams,clientDrivenID,sent,head); if service<>nil then begin // ParamsAsJSONObject won't apply to _signature_ e.g. if fParamsAsJSONObject and (clientDrivenID='') then sent := service^.ArgsArrayToObject(Pointer(sent),true); if fNonBlockWithoutAnswer and (head='') and (Service^.ArgsOutputValuesCount=0) then aClient.CallbackNonBlockingSetHeader(head); end; status := aClient.URI(uri,'POST',@resp,@head,@sent).Lo; end; begin result := false; if Self=nil then exit; if fClient=nil then fClient := fRest as TSQLRestClientURI; if aClient=nil then aClient := fClient; if (aClientDrivenID<>nil) and (aClientDrivenID^>0) then UInt32ToUTF8(aClientDrivenID^,clientDrivenID); m := fInterface.FindMethodIndex(aMethod); if m<0 then service := nil else service := @fInterface.Methods[m]; withinput := ((service=nil) or ([smdConst,smdVar]*service^.HasSPIParams=[])) and not(optNoLogInput in fExecution[m].Options); {$ifdef WITHLOG} if withinput then p := aParams; // include non-sensitive input in log log := fRest.LogClass.Enter('InternalInvoke I%.%(%) %', [fInterfaceURI,aMethod,p,clientDrivenID],self); {$endif} // call remote server according to current routing scheme if fForcedURI<>'' then baseuri := fForcedURI else if fRest.Services.ExpectMangledURI then baseuri := aClient.Model.Root+'/'+fInterfaceMangledURI else baseuri := aClient.Model.Root+'/'+fInterfaceURI; ctxt := []; if (service<>nil) and not ParamsAsJSONObject and service^.ArgsInputIsOctetStream then include(ctxt,csiAsOctetStream); DoClientCall; if (status=HTTP_UNAUTHORIZED) and (clientDrivenID<>'') and (fInstanceCreation=sicClientDriven) and (aClientDrivenID<>nil) then begin {$ifdef WITHLOG} if log<>nil then log.Log(sllClient,'% -> try to recreate ClientDrivenID',[resp],self); {$endif} clientDrivenID := ''; aClientDrivenID^ := 0; DoClientCall; end; // decode result if aServiceCustomAnswer=nil then begin // handle errors at REST level if not StatusCodeIsSuccess(status) then begin if aErrorMsg<>nil then begin if resp='' then begin StatusCodeToErrorMessage(status,resp); error := GetErrorMessage(status); if error<>'' then error := ' - '+error; if not withinput then sent := ''; // exclude sensitive input in error text aErrorMsg^ := FormatUTF8('URI % % returned status ''%'' (%%)', [uri,sent,resp,status,error]); end else aErrorMsg^ := resp; end; exit; // leave result=false end; // decode JSON object {$ifdef WITHLOG} if (log<>nil) and (resp<>'') and not(optNoLogOutput in fExecution[m].Options) and ((service=nil) or ([smdConst,smdVar]*service^.HasSPIParams=[])) then with fRest.fLogFamily do if sllServiceReturn in Level then log.Log(sllServiceReturn,resp,self,MAX_SIZE_RESPONSE_LOG); {$endif WITHLOG} if fResultAsJSONObject then begin if aResult<>nil then aResult^ := resp; if aClientDrivenID<>nil then aClientDrivenID^ := 0; end else if (resp<>'') and (aClientDrivenID=nil) and not IdemPChar(GotoNextNotSpace(pointer(resp)),'{"RESULT":') then begin if aResult<>nil then aResult^ := resp; // e.g. when client retrieves the contract end else begin if (JSONDecode(pointer(resp),['result','id'],@Values,true)=nil) or (Values[0].Value=nil) then begin // no "result":... layout if aErrorMsg<>nil then begin UniqueRawUTF8ZeroToTilde(resp,1 shl 10); aErrorMsg^ := 'Invalid returned JSON content: expects {result:...}, got '+resp; end; exit; // leave result=false end; if aResult<>nil then Values[0].ToUTF8(aResult^); if (aClientDrivenID<>nil) and (Values[1].Value<>nil) then // keep ID if no "id":... aClientDrivenID^ := Values[1].ToCardinal; end; end else begin // custom answer returned in TServiceCustomAnswer {$ifdef WITHLOG} if (log<>nil) and (resp<>'') then with fRest.fLogFamily do if sllServiceReturn in Level then begin FindNameValue(head,HEADER_CONTENT_TYPE_UPPER,ct); if (resp[1] in ['[','{','"']) and IdemPChar(pointer(ct), JSON_CONTENT_TYPE_UPPER) then log.Log(sllServiceReturn,resp,self,MAX_SIZE_RESPONSE_LOG) else log.Log(sllServiceReturn,'TServiceCustomAnswer=% % len=% %', [status,ct,length(resp),EscapeToShort(resp)],self); end; {$endif WITHLOG} aServiceCustomAnswer^.Status := status; aServiceCustomAnswer^.Header := head; aServiceCustomAnswer^.Content := resp; // no "id" field returned, but aClientDrivenID^ should not change end; result := true; end; procedure TServiceFactoryClient.NotifyInstanceDestroyed(aClientDrivenID: cardinal); begin if aClientDrivenID<>0 then InternalInvoke(SERVICE_PSEUDO_METHOD[imFree],'',nil,nil,@aClientDrivenID); end; constructor TServiceFactoryClient.Create(aRest: TSQLRest; aInterface: PTypeInfo; aInstanceCreation: TServiceInstanceImplementation; const aContractExpected: RawUTF8); var Error, RemoteContract: RawUTF8; begin // extract interface RTTI and create fake interface (and any shared instance) if not aRest.InheritsFrom(TSQLRestClientURI) then EServiceException.CreateUTF8('%.Create(): % interface needs a Client connection', [self,aInterface^.Name]); if fClient=nil then fClient := TSQLRestClientURI(aRest); inherited Create(aRest,aInterface,aInstanceCreation,aContractExpected); // initialize a shared instance (if needed) case fInstanceCreation of sicShared,sicPerSession,sicPerUser,sicPerGroup,sicPerThread: begin // the instance shall remain active during the whole client session fSharedInstance := CreateFakeInstance; TInterfacedObjectFake(fSharedInstance)._AddRef; // force stay alive end; end; // check if this interface is supported on the server if PosEx(SERVICE_CONTRACT_NONE_EXPECTED,ContractExpected)=0 then begin if not InternalInvoke(SERVICE_PSEUDO_METHOD[imContract], TSQLRestClientURI(fRest).fServicePublishOwnInterfaces,@RemoteContract,@Error) then raise EServiceException.CreateUTF8('%.Create(): I% interface or % routing not '+ 'supported by server [%]',[self,fInterfaceURI,fRest.ServicesRouting,Error]); if ('['+ContractExpected+']'<>RemoteContract) and ('{"contract":'+ContractExpected+'}'<>RemoteContract) then raise EServiceException.CreateUTF8('%.Create(): server''s I% contract '+ 'differs from client''s: expected [%], received % - you may need to '+ 'upgrade your % client to match % server expectations', [self,fInterfaceURI,ContractExpected,RemoteContract, ExeVersion.Version.DetailedOrVoid,fClient.fSessionVersion]); end; end; destructor TServiceFactoryClient.Destroy; begin FreeAndNil(fSendNotificationsThread); if fSharedInstance<>nil then with TInterfacedObjectFake(fSharedInstance) do if fRefCount<>1 then raise EServiceException.CreateUTF8('%.Destroy with RefCount=%: you must release '+ 'I% interface (setting := nil) before Client.Free',[self,fRefCount,fInterfaceURI]) else _Release; // bonne nuit les petits inherited; end; function TServiceFactoryClient.RetrieveSignature: RawUTF8; begin result := ''; if InternalInvoke(SERVICE_PSEUDO_METHOD[imSignature],'',@result) and (result<>'') then if result[1]='[' then result := copy(result,2,length(result)-2) else if IdemPChar(pointer(result),'{"SIGNATURE":') then result := copy(result,14,length(result)-14); end; function TServiceFactoryClient.Get(out Obj): Boolean; var O: TInterfacedObjectFake; begin result := false; if Self=nil then exit; case fInstanceCreation of sicShared, sicPerSession, sicPerUser, sicPerGroup, sicPerThread: O := TInterfacedObjectFake(fSharedInstance); sicSingle, sicClientDriven: O := TInterfacedObjectFake(CreateFakeInstance); else exit; end; if O=nil then exit; pointer(Obj) := @O.fVTable; O._AddRef; result := true; end; procedure TServiceFactoryClient.StoreNotifications(aRest: TSQLRest; aLogClass: TSQLRecordServiceNotificationsClass); var c: TClass; begin if (aRest=fSendNotificationsRest) and (aLogClass=fSendNotificationsLogClass) then exit; fSendNotificationsRest := aRest; fSendNotificationsLogClass := aLogClass; if aRest=nil then c := nil else c := aRest.ClassType; fClient.InternalLog('%.StoreNotifications(%,%) for I%', [ClassType,c,aLogClass,fInterfaceURI]); end; procedure TServiceFactoryClient.SendNotifications(aRest: TSQLRest; aLogClass: TSQLRecordServiceNotificationsClass; aRetryPeriodSeconds: Integer; aRemote: TSQLRestClientURI); begin if (self=nil) or (aRest=nil) or (aLogClass=nil) then raise EServiceException.CreateUTF8('%.SendNotifications invalid call',[self]); if fSendNotificationsThread<>nil then if (aRest=fSendNotificationsRest) and (aLogClass=fSendNotificationsLogClass) then begin fClient.InternalLog('%.SendNotifications(%,%) I% twice -> ignored', [ClassType,aRest.ClassType,aLogClass,fInterfaceURI],sllInfo); exit; end else raise EServiceException.CreateUTF8('%.SendNotifications twice',[self]); StoreNotifications(aRest,aLogClass); fSendNotificationsThread := TServiceFactoryClientNotificationThread.Create(self,aRemote,aRetryPeriodSeconds); end; function TServiceFactoryClient.SendNotificationsPending: integer; begin if (self=nil) or (fSendNotificationsThread=nil) then result := 0 else result := TServiceFactoryClientNotificationThread(fSendNotificationsThread). GetPendingCountFromDB; end; procedure TServiceFactoryClient.SendNotificationsWait(aTimeOutSeconds: integer); var timeOut: Int64; {$ifdef WITHLOG} log: ISynLog; // for Enter auto-leave to work with FPC {$endif} begin if SendNotificationsPending=0 then exit; {$ifdef WITHLOG} log := fClient.LogClass.Enter; {$endif} timeOut := GetTickCount64+aTimeOutSeconds*1000; repeat SleepHiRes(5); if SendNotificationsPending=0 then exit; until GetTickCount64>timeOut; end; procedure TServiceFactoryClient.SetOptions(const aMethod: array of RawUTF8; aOptions: TServiceMethodOptions; aAction: TServiceMethodOptionsAction); var o: TServiceMethodOption; begin for o := low(o) to high(o) do if (o in aOptions) and not (o in [optNoLogInput..optErrorOnMissingParam]) then raise EServiceException.CreateUTF8('%.SetOptions(%) not supported', [self,GetEnumName(TypeInfo(TServiceMethodOption),ord(o))^]); ExecutionAction(aMethod,aOptions,aAction); end; function ObjectFromInterface(const aValue: IInterface): TObject; {$ifndef HASINTERFACEASTOBJECT} type TObjectFromInterfaceStub = packed record Stub: cardinal; case integer of 0: (ShortJmp: shortint); 1: (LongJmp: longint) end; PObjectFromInterfaceStub = ^TObjectFromInterfaceStub; {$endif} begin if aValue<>nil then {$ifdef HASINTERFACEASTOBJECT} result := aValue as TObject else // slower but always working {$else} with PObjectFromInterfaceStub(PPointer(PPointer(aValue)^)^)^ do case Stub of // address of VMT[0] entry, i.e. QueryInterface $04244483: begin result := pointer(PtrInt(aValue)+ShortJmp); exit; end; $04244481: begin result := pointer(PtrInt(aValue)+LongJmp); exit; end; else // recognize TInterfaceFactory.CreateFakeInstance() stub/mock if Stub=PCardinal(@TInterfacedObjectFake.FakeQueryInterface)^ then begin result := TInterfacedObjectFake(pointer(aValue)).SelfFromInterface; exit; end else begin result := nil; exit; end; end else {$endif} result := nil; end; function ObjectFromInterfaceImplements(const aValue: IInterface; const aInterface: TGUID): boolean; var obj: TObject; begin obj := ObjectFromInterface(aValue); if obj=nil then result := false else result := obj.GetInterfaceEntry(aInterface)<>nil; end; procedure SetWeak(aInterfaceField: PIInterface; const aValue: IInterface); begin PPointer(aInterfaceField)^ := Pointer(aValue); end; type TSetWeakZeroInstance = class(TObjectListHashed) protected fInstance: TObject; public constructor Create(aObject: TObject; aReference: pointer); destructor Destroy; override; property Instance: TObject read fInstance; end; TSetWeakZeroClass = class(TObjectListPropertyHashed) protected fHookedFreeInstance: PtrUInt; fLock: TRTLCriticalSection; procedure HookedFreeInstance; public constructor Create(aClass: TClass); destructor Destroy; override; function Find(aObject: TObject): TSetWeakZeroInstance; function FindOrAdd(aObject: TObject; aReference: pointer): TSetWeakZeroInstance; end; { TSetWeakZeroInstance } constructor TSetWeakZeroInstance.Create(aObject: TObject; aReference: pointer); var wasAdded: boolean; begin inherited Create(false); fInstance := aObject; Add(aReference,wasAdded); //assert(IndexOf(aReference)>=0); end; destructor TSetWeakZeroInstance.Destroy; var i: integer; begin for i := 0 to Count-1 do PPointer(List[i])^ := nil; inherited; end; { TSetWeakZeroClass } function WeakZeroClassSubProp(aObject: TObject): TObject; begin result := TSetWeakZeroInstance(aObject).fInstance; end; constructor TSetWeakZeroClass.Create(aClass: TClass); var PVMT: ^TObject; P: PPtrUInt; begin inherited Create(@WeakZeroClassSubProp); PVMT := pointer(PtrInt(PtrUInt(aClass))+vmtAutoTable); if PVMT^=nil then begin EnterCriticalSection(vmtAutoTableLock); // protect from concurrent access try if PVMT^=nil then begin PatchCodePtrUInt(pointer(PVMT),PtrUInt(self),true); // LeaveUnprotected=true GarbageCollectorFreeAndNil(PVMT^,self); // set to nil at finalization end; finally LeaveCriticalSection(vmtAutoTableLock); end; end; if (PVMT^<>nil) and (PVMT^<>self) then if TClass(PPointer(PVMT^)^)=TSQLRecordProperties then GarbageCollectorFreeAndNil( // set to nil at finalization TSQLRecordProperties(PVMT^).fWeakZeroClass,self) else raise EORMException.CreateUTF8('%.Create: %.AutoTable VMT entry already used',[self,aClass]); InitializeCriticalSection(fLock); EnterCriticalSection(fLock); {$WARN SYMBOL_DEPRECATED OFF} P := pointer(PtrInt(PtrUInt(aClass))+vmtFreeInstance); {$WARN SYMBOL_DEPRECATED ON} fHookedFreeInstance := P^; PatchCodePtrUInt(P,PtrUInt(@TSetWeakZeroClass.HookedFreeInstance)); end; destructor TSetWeakZeroClass.Destroy; begin DeleteCriticalSection(fLock); inherited; end; function EnterWeakZeroClass(aObject: TObject; CreateIfNonExisting: boolean): TSetWeakZeroClass; {$ifdef HASINLINENOTX86}inline;{$endif} begin result := PPointer(PPtrInt(aObject)^+vmtAutoTable)^; if (result<>nil) and (TClass(PPointer(result)^)=TSQLRecordProperties) then result := TSetWeakZeroClass(TSQLRecordProperties(pointer(result)).fWeakZeroClass); if result<>nil then EnterCriticalSection(result.fLock) else if CreateIfNonExisting then result := TSetWeakZeroClass.Create(PPointer(aObject)^); end; type TSimpleMethodCall = procedure(self: TObject); procedure TSetWeakZeroClass.HookedFreeInstance; begin with EnterWeakZeroClass(self,false) do begin // if hooked -> never nil try Delete(self); finally LeaveCriticalSection(fLock); end; TSimpleMethodCall(fHookedFreeInstance)(self); end; end; function TSetWeakZeroClass.Find(aObject: TObject): TSetWeakZeroInstance; var i: integer; begin i := IndexOf(aObject); // search List[i].fInstance=aObject if i>=0 then result := TSetWeakZeroInstance(List[i]) else result := nil; end; function TSetWeakZeroClass.FindOrAdd(aObject: TObject; aReference: pointer): TSetWeakZeroInstance; var wasAdded: boolean; i: integer; begin i := inherited Add(aObject,wasAdded); if wasAdded then begin result := TSetWeakZeroInstance.Create(aObject,aReference); List[i] := result; //assert(IndexOf(aObject)>=0); end else begin result := TSetWeakZeroInstance(List[i]); result.Add(aReference,wasAdded); end; //assert(result.IndexOf(aReference)>=0); //assert(result.fInstance=aObject); end; procedure SetWeakZero(aObject: TObject; aObjectInterfaceField: PIInterface; const aValue: IInterface); var aObjectWeakClass, aObjectInterfaceWeakClass: TSetWeakZeroClass; aObjectInterfaceObject, aValueObject: TObject; begin if (aObjectInterfaceField=nil) or (aObject=nil) or (aObjectInterfaceField^=aValue) then exit; aObjectWeakClass := EnterWeakZeroClass(aObject,false); try if aObjectInterfaceField^<>nil then begin if aValue=nil then aObjectWeakClass.Delete(TObject(aObjectInterfaceField)); aObjectInterfaceObject := ObjectFromInterface(aObjectInterfaceField^); if aObjectInterfaceObject<>nil then begin aObjectInterfaceWeakClass := EnterWeakZeroClass(aObjectInterfaceObject,false); if aObjectInterfaceWeakClass<>nil then try aObjectInterfaceWeakClass.Find(aObjectInterfaceObject).Delete(TObject(aObjectInterfaceField)); finally LeaveCriticalSection(aObjectInterfaceWeakClass.fLock); end; end; SetWeak(aObjectInterfaceField,nil); if aValue=nil then exit; end; if aObjectWeakClass=nil then // for faster Delete() just above aObjectWeakClass := TSetWeakZeroClass.Create(PPointer(aObject)^); aObjectWeakClass.FindOrAdd(aObject,aObjectInterfaceField); aValueObject := ObjectFromInterface(aValue); if aValueObject<>nil then with EnterWeakZeroClass(aValueObject,true) do try FindOrAdd(aValueObject,aObjectInterfaceField); finally LeaveCriticalSection(fLock); end; SetWeak(aObjectInterfaceField,aValue); finally if aObjectWeakClass<>nil then LeaveCriticalSection(aObjectWeakClass.fLock); end; end; {$ifdef ISDELPHIXE} procedure TWeakZeroInterfaceHelper.SetWeak0(aObjectInterfaceField: PIInterface; const aValue: IInterface); begin SetWeakZero(self,aObjectInterfaceField,aValue); end; {$endif} function ObjArraySearch(const aSQLRecordObjArray; aID: TID): TSQLRecord; var i: integer; a: TSQLRecordObjArray absolute aSQLRecordObjArray; r: PSQLRecord; begin r := pointer(a); for i := 1 to length(a) do begin result := r^; if result.fID=aID then exit; inc(r); end; result := nil; end; procedure ObjArrayRecordIDs(const aSQLRecordObjArray; out result: TInt64DynArray); var i, n: PtrInt; a: TSQLRecordObjArray absolute aSQLRecordObjArray; begin n := length(a); SetLength(result,n); for i := 0 to n-1 do result[i] := a[i].fID; end; procedure ObjArrayCopy(const aSourceObjArray; var aDestObjArray; aDestObjArrayClear: boolean); var s: TObjectDynArray absolute aSourceObjArray; d: TObjectDynArray absolute aDestObjArray; slen,dlen: integer; i: PtrInt; dinst: TClassInstance; o: TObject; begin if aDestObjArrayClear and (d<>nil) then ObjArrayClear(d); slen := length(s); if slen=0 then exit; dlen := length(d); SetLength(d,dlen+slen); DInst.ItemClass := nil; for i := 0 to slen-1 do if s[i]<>nil then begin // inlined CopyObject() if DInst.ItemClass<>PClass(s[i])^ then DInst.Init(PClass(s[i])^); // very unlikely to change o := DInst.CreateNew; CopyObject(s[i],o); d[dlen+i] := o; end; end; procedure InterfaceArrayDeleteAfterException(var aInterfaceArray; const aItemIndex: integer; aLog: TSynLogFamily; const aLogMsg: RawUTF8; aInstance: TObject); begin try {$ifdef WITHLOG} aLog.SynLog.Log(sllWarning,'InterfaceArrayDeleteAfterException % (index=%)', [aLogMsg,aItemIndex],aInstance); {$endif} InterfaceArrayDelete(aInterfaceArray,aItemIndex); except on E: Exception do aLog.SynLog.Log(sllDebug,'Callback % unstability at deletion: %', [aLogMsg,E],aInstance); end; end; procedure SetThreadNameWithLog(ThreadID: TThreadID; const Name: RawUTF8); begin {$ifdef WITHLOG} if (SetThreadNameLog<>nil) and (ThreadID=GetCurrentThreadId) then SetThreadNameLog.Add.LogThreadName(Name); {$endif} SetThreadNameDefault(ThreadID,Name); end; initialization pointer(@SQLFieldTypeComp[sftAnsiText]) := @AnsiIComp; pointer(@SQLFieldTypeComp[sftUTF8Custom]) := @AnsiIComp; pointer(@SQLFieldTypeComp[sftObject]) := @StrComp; {$ifndef NOVARIANTS} pointer(@SQLFieldTypeComp[sftVariant]) := @StrComp; pointer(@SQLFieldTypeComp[sftNullable]) := @StrComp; {$endif NOVARIANTS} {$ifndef USENORMTOUPPER} pointer(@SQLFieldTypeComp[sftUTF8Text]) := @AnsiIComp; {$endif} {$ifdef MSWINDOWS} // don't change the main process name under Linux SetThreadNameDefault(GetCurrentThreadID,'Main Thread'); {$endif} SetThreadNameInternal := SetThreadNameWithLog; StatusCodeToErrorMessage := StatusCodeToErrorMsgBasic; GarbageCollectorFreeAndNil(JSONCustomParsers,TSynDictionary.Create( TypeInfo(TClassDynArray),TypeInfo(TJSONCustomParsers))); DefaultTextWriterSerializer := TJSONSerializer; TJSONSerializer.RegisterObjArrayForJSON( [TypeInfo(TSQLModelRecordPropertiesObjArray),TSQLModelRecordProperties]); TJSONSerializer.RegisterCustomJSONSerializerFromText( [TypeInfo(TServicesPublishedInterfaces),_TServicesPublishedInterfaces, TypeInfo(TSQLRestServerURI),_TSQLRestServerURI]); SynCommons.DynArrayIsObjArray := InternalIsObjArray; InitializeCriticalSection(GlobalInterfaceResolutionLock); InitializeCriticalSection(vmtAutoTableLock); TInterfaceResolverInjected.RegisterGlobal(TypeInfo(IAutoLocker),TAutoLocker); TInterfaceResolverInjected.RegisterGlobal(TypeInfo(ILockedDocVariant),TLockedDocVariant); assert(SizeOf(TServiceMethod)and 3=0,'wrong padding'); AlgoDeflate := TAlgoDeflate.Create; AlgoDeflateFast := TAlgoDeflateFast.Create; TSQLRestServerFullMemory.RegisterClassNameForDefinition; {$ifdef MSWINDOWS} TSQLRestClientURINamedPipe.RegisterClassNameForDefinition; TSQLRestClientURIMessage.RegisterClassNameForDefinition; {$endif} {$ifdef GSSAPIAUTH} LoadGSSAPI; {$endif} finalization FinalizeGlobalInterfaceResolution; end.