/// 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) 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)
  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 MSWINDOWS}


{$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)<ValuesInlinedMax
// - PropName can be used as a prefix to the 'in ()' clause, in conjunction
// with optional Suffix value
function SelectInClause(const PropName: RawUTF8; const Values: array of RawUTF8;
  const Suffix: RawUTF8=''; ValuesInlinedMax: integer=0): RawUTF8; overload;

/// 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 bigger than 1
// - if Values has more than one value, returns 'PropName in (Values0,Values1,...)'
// or 'PropName in (:(Values0):,:(Values1):,...)' if length(Values)<ValuesInlinedMax
// - PropName can be used as a prefix to the 'in ()' clause, in conjunction
// with optional Suffix value
function SelectInClause(const PropName: RawUTF8; const Values: array of Int64;
  const Suffix: RawUTF8=''; ValuesInlinedMax: integer=0): RawUTF8; overload;

/// naive search of '... FROM TableName ...' pattern in the supplied SQL
function GetTableNameFromSQLSelect(const SQL: RawUTF8;
  EnsureUniqueTableInFrom: boolean): RawUTF8;

/// naive search of '... FROM Table1,Table2 ...' pattern in the supplied SQL
function GetTableNamesFromSQLSelect(const SQL: RawUTF8): TRawUTF8DynArray;

/// guess the content type of an UTF-8 encoded field value, as used in TSQLTable.Get()
// - if P if nil or 'null', return sftUnknown
// - otherwise, guess its type from its value characters
// - sftBlob is returned if the field is encoded as SQLite3 BLOB literals
// (X'53514C697465' e.g.) or with '\uFFF0' magic code
// - since P is PUTF8Char, string type is sftUTF8Text only
// - sftFloat is returned for any floating point value, even if it was
// declared as sftCurrency type
// - sftInteger is returned for any INTEGER stored value, even if it was declared
// as sftEnumerate, sftSet, sftID, sftTID, sftRecord, sftRecordVersion,
// sftSessionUserID, sftBoolean, sftModTime/sftCreateTime/sftTimeLog or
// sftUnixTime/sftUnixMSTime type
function UTF8ContentType(P: PUTF8Char): TSQLFieldType;

/// guess the number type of an UTF-8 encoded field value, as used in TSQLTable.Get()
// - if P if nil or 'null', return sftUnknown
// - will return sftInteger or sftFloat if the supplied text is a number
// - will return sftUTF8Text for any non numerical content
function UTF8ContentNumberType(P: PUTF8Char): TSQLFieldType;
  {$ifdef HASINLINE}inline;{$endif}


/// read an object properties, as saved by TINIWriter.WriteObject() method
// - i.e. only Integer, Int64, enumerates (including boolean), floating point,
// variant and (Ansi/Wide/Unicode)String properties (excluding shortstring)
// - read only the published properties of the current class level (do NOT
// read the properties content published in the parent classes)
// - "From" must point to the [section] containing the object properties
// - for integers and enumerates, if no value is stored in From (or From is ''),
// the default value from the property definition is set
procedure ReadObject(Value: TObject; From: PUTF8Char; const SubCompName: RawUTF8=''); overload;

/// read an object properties, as saved by TINIWriter.WriteObject() method
// - i.e. only Integer, Int64, enumerates (including boolean), floating point values
// and (Ansi/Wide/Unicode)String properties (excluding shortstring)
// - read only the published properties of the current class level (do NOT
// read the properties content published in the parent classes)
// - for integers, if no value is stored in FromContent, the default value is set
// - this version gets the appropriate section from [Value.ClassName]
// - this version doesn't handle embedded objects
procedure ReadObject(Value: TObject; const FromContent: RawUTF8;
  const SubCompName: RawUTF8=''); overload;

/// write an object properties, as saved by TINIWriter.WriteObject() method
// - i.e. only Integer, Int64, enumerates (including boolean), floating point values
// and (Ansi/Wide/Unicode)String properties (excluding shortstring)
// - write only the published properties of the current class level (do NOT
// write the properties content published in the parent classes)
// - direct update of INI-like content
// - for integers, value is always written, even if matches the default value
procedure WriteObject(Value: TObject; var IniContent: RawUTF8;
  const Section: RawUTF8; const SubCompName: RawUTF8=''); overload;

/// write an object properties, as saved by TINIWriter.WriteObject() method
// - i.e. only Integer, Int64, enumerates (including boolean), floating point values
// and (Ansi/Wide/Unicode)String properties (excluding shortstring)
// - write only the published properties of the current class level (do NOT
// write the properties content published in the parent classes)
// - return the properties as text Name=Values pairs, with no section
// - for integers, if the value matches the default value, it is not added to the result
function WriteObject(Value: TObject): RawUTF8; overload;

/// copy object properties
// - copy Integer, Int64, enumerates (including boolean), variant, records,
// dynamic arrays, classes and any string properties (excluding shortstring)
// - TCollection items can be copied also, if they are of the same exact class
// - object properties instances are created in aTo if the objects are not
// TSQLRecord children (in this case, these are not class instances, but
// INTEGER reference to records, so only the integer value is copied), that is
// for regular Delphi classes
procedure CopyObject(aFrom, aTo: TObject); overload;

/// create a new object instance, from an existing one
// - will create a new instance of the same class, then call the overloaded
// CopyObject() procedure to copy its values
function CopyObject(aFrom: TObject): TObject; overload;

/// copy two TStrings instances
// - will just call Dest.Assign(Source) in practice
procedure CopyStrings(Source, Dest: TStrings);

{$ifndef LVCL}
/// copy two TCollection instances
// - will call CopyObject() in loop to repopulate the Dest collection,
// which will work even if Assign() method was not overriden
procedure CopyCollection(Source, Dest: TCollection);
{$endif}

/// set any default integer or enumerates (including boolean) published
// properties values for a TPersistent/TSynPersistent
// - set only the values set as "property ... default ..." at class type level
// - will also reset the published properties of the nested classes
procedure SetDefaultValuesObject(Value: TObject);

/// returns TRUE on a nil instance or if all its published properties are default/0
// - calls internally TPropInfo.IsDefaultOrVoid()
function IsObjectDefaultOrVoid(Value: TObject): boolean;

/// will reset all the object properties to their default
// - strings will be set to '', numbers to 0
// - if FreeAndNilNestedObjects is the default FALSE, will recursively reset
// all nested class properties values
// - if FreeAndNilNestedObjects is TRUE, will FreeAndNil() all the nested
// class properties
// - for a TSQLRecord, use its ClearProperties method instead, which will
// handle the ID property, and any nested JOINed instances
procedure ClearObject(Value: TObject; FreeAndNilNestedObjects: boolean=false);

/// persist a class instance into a JSON file
// - returns TRUE on success, false on error (e.g. the file name is invalid
// or the file is existing and could not be overwritten)
function ObjectToJSONFile(Value: TObject; const JSONFile: TFileName;
  Options: TTextWriterWriteObjectOptions=[woHumanReadable]): boolean;

/// will serialize any TObject into its expanded UTF-8 JSON representation
// - includes debugger-friendly information, similar to TSynLog, i.e.
// class name and sets/enumerates as text
// - could be used to create a TDocVariant object with full information
// - wrapper around ObjectToJSON(Value,[woDontStoreDefault,woFullExpand])
// also able to serialize plain Exception as a simple '{"Exception":"Message"}',
// and append .map/.mab source code line number for ESynException
function ObjectToJSONDebug(Value: TObject; Options: TTextWriterWriteObjectOptions=
  [woDontStoreDefault,woHumanReadable,woStoreClassName,woStorePointer]): RawUTF8;

/// will serialize any TObject into a TDocVariant debugging document
// - just a wrapper around _JsonFast(ObjectToJSONDebug()) with an optional
// "Context":"..." text message
// - if the supplied context format matches '{....}' then it will be added
// as a corresponding TDocVariant JSON object
function ObjectToVariantDebug(Value: TObject;
  const ContextFormat: RawUTF8; const ContextArgs: array of const;
  const ContextName: RawUTF8='context'): variant; overload;

/// will serialize any TObject into a TDocVariant debugging document
// - just a wrapper around _JsonFast(ObjectToJSONDebug())
function ObjectToVariantDebug(Value: TObject): variant; overload;

/// add the property values of a TObject to a document-based object content
// - if Obj is a TDocVariant object, then all Values's published
// properties will be added at the root level of Obj
procedure _ObjAddProps(Value: TObject; var Obj: variant); overload;

/// is able to compare two objects by value
// - both instances may or may not be of the same class, but properties
// should match
// - will use direct RTTI access of property values, or TSQLRecord.SameValues()
// if available to make the comparison as fast and accurate as possible
// - if you want only to compare the plain fields with no getter function,
// e.g. if they are just some conversion of the same information, you can
// set ignoreGetterFields=TRUE
function ObjectEquals(Value1,Value2: TObject; ignoreGetterFields: boolean=false): boolean;

type
  /// available options for JSONToObject() parsing process
  // - by default, function will fail if a JSON field name is not part of the
  // object published properties, unless j2oIgnoreUnknownProperty is defined -
  // this option will also ignore read-only properties (i.e. with only a getter)
  // - by default, function will check that the supplied JSON value will
  // be a JSON string when the property is a string, unless j2oIgnoreStringType
  // is defined and JSON numbers are accepted and stored as text
  // - by default any unexpected value for enumerations will be marked as
  // invalid, unless j2oIgnoreUnknownEnum is defined, so that in such case the
  // ordinal 0 value is left, and loading continues
  // - by default, only simple kind of variant types (string/numbers) are
  // handled: set j2oHandleCustomVariants if you want to handle any custom -
  // in this case , it will handle direct JSON [array] of {object}: but if you
  // also define j2oHandleCustomVariantsWithinString, it will also try to
  // un-escape a JSON string first, i.e. handle "[array]" or "{object}" content
  // (may be used e.g. when JSON has been retrieved from a database TEXT column)
  // - 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: set j2oSetterExpectsToFreeTempInstance to let JSONToObject
  // (and TPropInfo.ClassFromJSON) release it when the setter returns, and
  // j2oSetterNoCreate to avoid the published field instance creation
  // - set j2oAllowInt64Hex to let Int64/QWord fields accept hexadecimal string
  // (as generated e.g. via the woInt64AsHex option)
  TJSONToObjectOption = (
    j2oIgnoreUnknownProperty, j2oIgnoreStringType, j2oIgnoreUnknownEnum,
    j2oHandleCustomVariants, j2oHandleCustomVariantsWithinString,
    j2oSetterExpectsToFreeTempInstance, j2oSetterNoCreate, j2oAllowInt64Hex);
  /// set of options for JSONToObject() parsing process
  TJSONToObjectOptions = set of TJSONToObjectOption;

const
  /// some open-minded options for JSONToObject() parsing
  // - won't block JSON unserialization due to some minor class type definitions
  // - used e.g. by TObjArraySerializer.CustomReader and
  // TInterfacedObjectFake.FakeCall/TServiceMethodExecute.ExecuteJson methods
  JSONTOOBJECT_TOLERANTOPTIONS = [j2oHandleCustomVariants,j2oIgnoreUnknownEnum,
    j2oIgnoreUnknownProperty,j2oIgnoreStringType,j2oAllowInt64Hex];

/// read an object properties, as saved by ObjectToJSON function
// - ObjectInstance must be an existing TObject instance
// - the data inside From^ is modified in-place (unescaped and transformed):
// calling JSONToObject(pointer(JSONRawUTF8)) will change the JSONRawUTF8
// variable content, which may not be what you expect - consider using the
// ObjectLoadJSON() function instead
// - handle Integer, Int64, enumerate (including boolean), set, floating point,
// TDateTime, TCollection, TStrings, TRawUTF8List, variant, and string properties
// (excluding ShortString, but including WideString and UnicodeString under
// Delphi 2009+)
// - TList won't be handled since it may leak memory when calling TList.Clear
// - won't handle TObjectList (even if ObjectToJSON is able to serialize
// them) since has no way of knowing the object type to add (TCollection.Add
// is missing), unless: 1. you set the TObjectListItemClass property as expected,
// and provide a TObjectList object, or 2. woStoreClassName option has been
// used at ObjectToJSON() call and the corresponding classes have been previously
// registered by TJSONSerializer.RegisterClassForJSON() (or Classes.RegisterClass)
// - will clear any previous TCollection objects, and convert any null JSON
// basic type into nil - e.g. if From='null', will call FreeAndNil(Value)
// - you can add some custom (un)serializers for ANY Delphi class, via the
// TJSONSerializer.RegisterCustomSerializer() class method
// - set Valid=TRUE on success, Valid=FALSE on error, and the main function
// will point in From at the syntax error place (e.g. on any unknown property name)
// - caller should explicitely perform a SetDefaultValuesObject(Value) if
// the default values are expected to be set before JSON parsing
function JSONToObject(var ObjectInstance; From: PUTF8Char; out Valid: boolean;
  TObjectListItemClass: TClass=nil; Options: TJSONToObjectOptions=[]): PUTF8Char;

/// parse the supplied JSON with some tolerance about Settings format
// - will make a TSynTempBuffer copy for parsing, and un-comment it
// - returns true if the supplied JSON was successfully retrieved
// - returns false and set InitialJsonContent := '' on error
function JSONSettingsToObject(var InitialJsonContent: RawUTF8; Instance: TObject): boolean;

/// read an object properties, as saved by ObjectToJSON function
// - ObjectInstance must be an existing TObject instance
// - this overloaded version will make a private copy of the supplied JSON
// content (via TSynTempBuffer), to ensure the original buffer won't be modified
// during process, before calling safely JSONToObject()
// - will return TRUE on success, or FALSE if the supplied JSON was invalid
function ObjectLoadJSON(var ObjectInstance; const JSON: RawUTF8;
  TObjectListItemClass: TClass=nil; Options: TJSONToObjectOptions=[]): boolean;

/// create a new object instance, as saved by ObjectToJSON(...,[...,woStoreClassName,...]);
// - JSON input should be either 'null', either '{"ClassName":"TMyClass",...}'
// - woStoreClassName option shall have been used at ObjectToJSON() call
// - and the corresponding class shall have been previously registered by
// TJSONSerializer.RegisterClassForJSON(), in order to retrieve the class type
// from it name - or, at least, by a Classes.RegisterClass() function call
// - the data inside From^ is modified in-place (unescaped and transformed):
// don't call JSONToObject(pointer(JSONRawUTF8)) but makes a temporary copy of
// the JSON text buffer before calling this function, if want to reuse it later
function JSONToNewObject(var From: PUTF8Char; var Valid: boolean;
  Options: TJSONToObjectOptions=[]): TObject;

/// decode a specified parameter compatible with URI encoding into its original
// object contents
// - ObjectInstance must be an existing TObject instance
// - will call internaly JSONToObject() function to unserialize its content
// - UrlDecodeExtended('price=20.45&where=LastName%3D%27M%C3%B4net%27','PRICE=',P,@Next)
// will return Next^='where=...' and P=20.45
// - if Upper is not found, Value is not modified, and result is FALSE
// - if Upper is found, Value is modified with the supplied content, and result is TRUE
function UrlDecodeObject(U: PUTF8Char; Upper: PAnsiChar; var ObjectInstance; Next: PPUTF8Char=nil;
  Options: TJSONToObjectOptions=[]): boolean;

/// fill the object properties from a JSON file content
// - ObjectInstance must be an existing TObject instance
// - this function will call RemoveCommentsFromJSON() before process
function JSONFileToObject(const JSONFile: TFileName; var ObjectInstance;
  TObjectListItemClass: TClass=nil; Options: TJSONToObjectOptions=[]): boolean;



{ ************ some RTTI and SQL mapping routines }

type
  /// the class kind as handled by TClassInstance object
  TClassInstanceItemCreate = (
    cicUnknown,cicTSQLRecord,cicTObjectList,cicTPersistentWithCustomCreate,
    cicTSynPersistent,cicTInterfacedCollection,cicTInterfacedObjectWithCustomCreate,
    cicTCollection,cicTCollectionItem,cicTComponent,cicTObject);

  /// store information about a class, able to easily create new instances
  // - using this temporary storage will speed up the creation process
  // - any virtual constructor will be used, including for TCollection types
  {$ifdef USERECORDWITHMETHODS}TClassInstance = record
    {$else}TClassInstance = object{$endif}
  public
    /// the class type itself
    ItemClass: TClass;
    // how the class instance is expected to be created
    ItemCreate: TClassInstanceItemCreate;
    {$ifndef LVCL}
    /// for TCollection instances, the associated TCollectionItem class
    CollectionItemClass: TCollectionItemClass;
    {$endif}
    /// fill the internal information fields for a given class type
    procedure Init(C: TClass);
    /// create a new instance of the registered class
    function CreateNew: TObject;
    /// compute the custom JSON commentary corresponding to this class
    procedure SetCustomComment(var CustomComment: RawUTF8);
  end;
  /// points to information about a class, able to create new instances
  PClassInstance = ^TClassInstance;

{$ifdef FPC}
  {$ifdef FPC_REQUIRES_PROPER_ALIGNMENT} // e.g. for ARM
    {$PACKRECORDS C}
  {$else}
    {$A-}
  {$endif}
{$else}
  {$A-} // Delphi compiler use packed/unaligned structs for most internal types
{$endif FPC}

type
  {$ifndef FPC}
  PMethod = ^TMethod; // not defined on older Delphi revisions
  {$endif FPC}
  PTypeInfo = ^TTypeInfo;
  {$ifdef HASDIRECTTYPEINFO} // you should use inlined Deref() function below
  PPTypeInfo = PTypeInfo;
  {$else}
  PPTypeInfo = ^PTypeInfo;
  {$endif HASDIRECTTYPEINFO}
  PTypeInfoDynArray = array of PTypeInfo;
  PPropInfo = ^TPropInfo;
  PMethodInfo = ^TMethodInfo;
  TClassArray = array[0..MaxInt div SizeOf(TClass)-1] of TClass;
  PClassArray = ^TClassArray;

  /// used to store a chain of properties RTTI
  // - could be used e.g. by TSQLPropInfo to handled flattened properties
  PPropInfoDynArray = array of PPropInfo;

  /// pointer to TClassProp
  PClassProp = ^TClassProp;
  /// a wrapper to published properties of a class
  // - start enumeration by getting a PClassProp with ClassProp()
  // - use PropCount, P := @PropList to get the first PPropInfo, and then P^.Next
  // - this enumeration is very fast and doesn't require any temporary memory,
  //  as in the TypInfo.GetPropInfos() PPropList usage
  // - for TSQLRecord, you should better use the RecordProps.Fields[] array,
  // which is faster and contains the properties published in parent classes
  {$ifdef USERECORDWITHMETHODS}TClassProp = record
    {$else}TClassProp = object{$endif}
  public
    /// number of published properties in this object
    PropCount: Word;
    /// point to a TPropInfo packed array
    // - layout is as such, with variable TPropInfo storage size:
    // ! PropList: array[1..PropCount] of TPropInfo
    // - use TPropInfo.Next to get the next one:
    // ! P := @PropList;
    // ! for i := 1 to PropCount do begin
    // !   // ... do something with P
    // !   P := P^.Next;
    // ! end;
    PropList: record end;
    /// retrieve a Field property RTTI information from a Property Name
    function FieldProp(const PropName: shortstring): PPropInfo;
  end;

  PClassType = ^TClassType;
  /// a wrapper to class type information, as defined by the Delphi RTTI
  {$ifdef USERECORDWITHMETHODS}TClassType = record
    {$else}TClassType = object{$endif}
  public
    /// the class type
    ClassType: TClass;
    /// the parent class type information
    ParentInfo: PPTypeInfo;
    /// the number of published properties
    PropCount: SmallInt;
    /// the name (without .pas extension) of the unit were the class was defined
    // - then the PClassProp follows: use the method ClassProp to retrieve its
    // address
    UnitName: string[255];
    /// get the information about the published properties of this class
    // - stored after UnitName memory
    function ClassProp: PClassProp;
      {$ifdef HASINLINE}inline;{$endif}
    /// fast and easy find if this class inherits from a specific class type
    // - you should rather consider using TTypeInfo.InheritsFrom directly
    function InheritsFrom(AClass: TClass): boolean;
    /// return the size (in bytes) of this class type information
    // - can be used to create class types at runtime
    function RTTISize: integer;
  end;

  PEnumType = ^TEnumType;
  /// a wrapper to enumeration type information, as defined by the Delphi RTTI
  // - we use this to store the enumeration values as integer, but easily provide
  // a text equivalent, translated if necessary, from the enumeration type
  // definition itself
  {$ifdef USERECORDWITHMETHODS}TEnumType = record
    {$else}TEnumType = object{$endif}
  public
    /// specify ordinal storage size and sign
    // - is prefered to MaxValue to identify the number of stored bytes
    OrdType: TOrdType;
    {$ifdef FPC_REQUIRES_PROPER_ALIGNMENT}
    EnumDummy: DWORD; // needed on ARM for correct alignment !!??
    {$endif}
    { this seemingly extraneous inner record is here for alignment purposes, so
    that its data gets aligned properly (if FPC_REQUIRES_PROPER_ALIGNMENT is set) }
    {$ifdef FPC_ENUMHASINNER}
    inner:
    {$ifndef FPC_REQUIRES_PROPER_ALIGNMENT}
    packed
    {$endif FPC_REQUIRES_PROPER_ALIGNMENT}
    record
    {$endif}
    {$ifdef FPC_ENUMHASINNER}
      iMinValue: Longint;
      iMaxValue: Longint;
      iBaseType: PPTypeInfo;
    end;
    {$else}
    /// first value of enumeration type, typicaly 0
    MinValue: Longint;
    /// same as ord(high(type)): not the enumeration count, but the highest index
    MaxValue: Longint;
    /// the base type of this enumeration
    /// - always use PEnumType(typeinfo(TEnumType))^.BaseType or more useful
    // method PTypeInfo(typeinfo(TEnumType))^.EnumBaseType before calling
    // any of the methods below
    BaseType: PPTypeInfo;
    {$endif FPC_ENUMHASINNER}
    /// a concatenation of shortstrings, containing the enumeration names
    // - those shortstrings are not aligned whatsoever (even if
    // FPC_REQUIRES_PROPER_ALIGNMENT is set)
    NameList: string[255];
    {$ifdef FPC_ENUMHASINNER}
    function MinValue: Longint; inline;
    function MaxValue: Longint; inline;
    function BaseType: PPTypeInfo; inline;
    {$endif FPC_ENUMHASINNER}
    /// get the corresponding enumeration name
    // - return the first one if Value is invalid (>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 TAlgoCompress 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)
  // - 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
  // - 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
  // - 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;
    /// decoded URI for rsoMethodUnderscoreAsSlashURI in Server.Options
    // - e.g. 'Method_Name' from 'ModelRoot/Method/Name' URI
    URIUnderscoreAsSlash: 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 <content ...>
    // </content> 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 <token>" 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 <JWT>" 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
  // - is called TOrm in mORMot 2
  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<?',[],[1000,2000]));
    //  or call the overloaded contructor with BoundsSQLWhere array of parameters
    constructor Create(aClient: TSQLRest; const aSQLWhere: RawUTF8); overload;
    /// this constructor initializes the object as above, and fills its content
    // from a client or server connection, using a specified WHERE clause
    // with parameters
    // - 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
    // - note that this method prototype changed with revision 1.17 of the
    // framework: array of const used to be ParamsSQLWhere and '%' in the
    // FormatSQLWhere statement, whereas it now expects bound parameters as '?'
    constructor Create(aClient: TSQLRest; const FormatSQLWhere: RawUTF8;
      const BoundsSQLWhere: array of const); overload;
    /// this constructor initializes the object as above, and fills its content
    // from a client or server connection, using a specified WHERE clause
    // with parameters
    // - the FormatSQLWhere clause will replace all '%' chars with the supplied
    // ParamsSQLWhere[] values, and all '?' chars with BoundsSQLWhere[] values,
    // as :(...): inlined parameters - you should either call:
    // ! Rec := TSQLMyRecord.Create(aClient,'Count=:(%):'[aCount],[]);
    // or (letting the inlined parameters being computed by FormatUTF8)
    // !  Rec := TSQLMyRecord.Create(aClient,'Count=?',[],[aCount]);
    // or even better, using the other Create overloaded constructor:
    // !  Rec := TSQLMyRecord.Create(aClient,'Count=?',[aCount]);
    // - using '?' and BoundsSQLWhere[] is perhaps more readable in your code, and
    // will in all case create a request with :(..): inline parameters, with
    // automatic RawUTF8 quoting if necessary
    constructor Create(aClient: TSQLRest; const FormatSQLWhere: RawUTF8;
      const ParamsSQLWhere, BoundsSQLWhere: array of const); overload;
    /// this constructor initializes the object as above, and fills its content
    // from a supplied JSON content
    // - is a wrapper around Create + FillFrom() methods
    // - use JSON data, as exported by GetJSONValues(), expanded or not
    // - make an internal copy of the JSONTable RawUTF8 before calling
    // FillFrom() below
    constructor CreateFrom(const JSONRecord: RawUTF8); overload;
    /// this constructor initializes the object as above, and fills its content
    // from a supplied JSON buffer
    // - is a wrapper around Create + FillFrom() methods
    // - use JSON data, as exported by GetJSONValues(), expanded or not
    // - the data inside P^ is modified (unescaped and transformed in-place):
    // don't call CreateFrom(pointer(JSONRecord)) but CreateFrom(JSONRecord) which
    // makes a temporary copy of the JSONRecord text variable before parsing
    constructor CreateFrom(P: PUTF8Char); overload;
    {$ifndef NOVARIANTS}
    /// this constructor initializes the object as above, and fills its content
    // from a supplied TDocVariant object document
    // - is a wrapper around Create + FillFrom() methods
    constructor CreateFrom(const aDocVariant: variant); overload;
    {$endif}

    /// this constructor initializes the object as above, and prepares itself to
    // loop through a statement using a specified WHERE clause
    // - this method creates a TSQLTableJSON, retrieves all records corresponding
    // to the WHERE clause, then call FillPrepare - previous Create(aClient)
    // methods retrieve only one record, this one more multiple rows
    // - you should then loop for all rows using 'while Rec.FillOne do ...'
    // - the TSQLTableJSON will be freed by TSQLRecord.Destroy
    // - the WHERE clause should use inlined parameters (like 'Name=:('Arnaud'):')
    // for better server speed - note that you can use FormatUTF8() as such:
    // ! aRec := TSQLMyRec.CreateAndFillPrepare(Client,FormatUTF8('Salary>? AND Salary<?',[],[1000,2000]));
    // or call the overloaded CreateAndFillPrepare() contructor directly with
    // BoundsSQLWhere array of parameters
    // - aCustomFieldsCSV can be used to specify which fields must be retrieved
    // - default aCustomFieldsCSV='' will retrieve all simple table fields
    // - if aCustomFieldsCSV='*', it will retrieve all fields, including BLOBs
    // - aCustomFieldsCSV can also be set to a CSV field list to retrieve only
    // the needed fields, and save remote bandwidth - note that any later
    // Update() will update all simple fields, so potentially with wrong
    // values; but BatchUpdate() can be safely used since it will
    constructor CreateAndFillPrepare(aClient: TSQLRest; const aSQLWhere: RawUTF8;
      const aCustomFieldsCSV: RawUTF8=''); overload;
    /// this constructor initializes the object as above, and prepares itself to
    // loop through a statement using a specified WHERE clause
    // - this method creates a TSQLTableJSON, retrieves all records corresponding
    // to the WHERE clause, then call FillPrepare - previous Create(aClient)
    // methods retrieve only one record, this one more multiple rows
    // - you should then loop for all rows using 'while Rec.FillOne do ...'
    // - the TSQLTableJSON will be freed by TSQLRecord.Destroy
    // - 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
    // - note that this method prototype changed with revision 1.17 of the
    // framework: array of const used to be ParamsSQLWhere and '%' in the
    // FormatSQLWhere statement, whereas it now expects bound parameters as '?'
    // - aCustomFieldsCSV can be used to specify which fields must be retrieved
    // - default aCustomFieldsCSV='' will retrieve all simple table fields, but
    // you may need  to access only one or several fields, and will save remote
    // bandwidth by specifying the needed fields
    // - if aCustomFieldsCSV='*', it will retrieve all fields, including BLOBs
    // - note that you should not use this aCustomFieldsCSV optional parameter if
    // you want to Update the retrieved record content later, since any
    // missing fields will be left with previous values - but BatchUpdate() can be
    // safely used after FillPrepare (will set only ID, TModTime and mapped fields)
    constructor CreateAndFillPrepare(aClient: TSQLRest; const FormatSQLWhere: RawUTF8;
      const BoundsSQLWhere: array of const; const aCustomFieldsCSV: RawUTF8=''); overload;
    /// this constructor initializes the object as above, and prepares itself to
    // loop through a statement using a specified WHERE clause
    // - this method creates a TSQLTableJSON, retrieves all records corresponding
    // to the WHERE clause, then call FillPrepare - previous Create(aClient)
    // methods retrieve only one record, this one more multiple rows
    // - you should then loop for all rows using 'while Rec.FillOne do ...'
    // - the TSQLTableJSON will be freed by TSQLRecord.Destroy
    // - the FormatSQLWhere clause will replace all '%' chars with the supplied
    // ParamsSQLWhere[] supplied values, and bind all '?' chars as parameters
    // with BoundsSQLWhere[] values
    // - aCustomFieldsCSV can be used to specify which fields must be retrieved
    // - default aCustomFieldsCSV='' will retrieve all simple table fields, but
    // you may need  to access only one or several fields, and will save remote
    // bandwidth by specifying the needed fields
    // - if aCustomFieldsCSV='*', it will retrieve all fields, including BLOBs
    // - note that you should not use this aCustomFieldsCSV optional parameter if
    // you want to Update the retrieved record content later, since any
    // missing fields will be left with previous values - but BatchUpdate() can be
    // safely used after FillPrepare (will set only ID, TModTime and mapped fields)
    constructor CreateAndFillPrepare(aClient: TSQLRest; const FormatSQLWhere: RawUTF8;
      const ParamsSQLWhere, BoundsSQLWhere: array of const;
      const aCustomFieldsCSV: RawUTF8=''); overload;
    /// this constructor initializes the object as above, and prepares itself to
    // loop through a given list of IDs
    // - this method creates a TSQLTableJSON, retrieves all records corresponding
    // to the specified IDs, then call FillPrepare - previous Create(aClient)
    // methods retrieve only one record, this one more multiple rows
    // - you should then loop for all rows using 'while Rec.FillOne do ...'
    // - the TSQLTableJSON will be freed by TSQLRecord.Destroy
    // - aCustomFieldsCSV can be used to specify which fields must be retrieved
    // - default aCustomFieldsCSV='' will retrieve all simple table fields, but
    // you may need  to access only one or several fields, and will save remote
    // bandwidth by specifying the needed fields
    // - if aCustomFieldsCSV='*', it will retrieve all fields, including BLOBs
    // - note that you should not use this aCustomFieldsCSV optional parameter if
    // you want to Update the retrieved record content later, since any
    // missing fields will be left with previous values - but BatchUpdate() can be
    // safely used after FillPrepare (will set only ID, TModTime and mapped fields)
    constructor CreateAndFillPrepare(aClient: TSQLRest; const aIDs: array of Int64;
      const aCustomFieldsCSV: RawUTF8=''); overload;
    /// this constructor initializes the object, and prepares itself to loop
    // through a specified JSON table, which will use a private copy
    // - this method creates a TSQLTableJSON, fill it with the supplied JSON buffer,
    // then call FillPrepare - previous Create(aClient) methods retrieve only
    // one record, this one more multiple rows
    // - you should then loop for all rows using 'while Rec.FillOne do ...'
    // - the TSQLTableJSON will be freed by TSQLRecord.Destroy
    constructor CreateAndFillPrepare(const aJSON: RawUTF8); overload;
    /// this constructor initializes the object, and prepares itself to loop
    // through a specified JSON table buffer, which will be modified in-place
    // - this method creates a TSQLTableJSON, fill it with the supplied JSON buffer,
    // then call FillPrepare - previous Create(aClient) methods retrieve only
    // one record, this one more multiple rows
    // - you should then loop for all rows using 'while Rec.FillOne do ...'
    // - the TSQLTableJSON will be freed by TSQLRecord.Destroy
    constructor CreateAndFillPrepare(aJSON: PUTF8Char; aJSONLen: integer); overload;
    /// this constructor initializes the object from its ID, including all
    // nested TSQLRecord properties, through a JOINed statement
    // - by default, Create(aClient,aID) will return only the one-to-one
    // nested TSQLRecord published properties IDs trans-typed as pointer - this
    // constructor allow to retrieve the nested values in one statement
    // - use this constructor if you want all TSQLRecord published properties to
    // be allocated, and loaded with the corresponding values
    // - Free/Destroy will release these instances
    // - warning: if you call Update() after it, only the main object will be
    // updated, not the nested TSQLRecord properties
    constructor CreateJoined(aClient: TSQLRest; aID: TID);
    /// this constructor initializes the object, and prepares itself to loop
    // nested TSQLRecord properties, through a JOINed statement and a WHERE clause
    // - by default, CreateAndFillPrepare() will return only the one-to-one
    // nested TSQLRecord published properties IDs trans-typed as pointer - this
    // constructor allow to retrieve the nested values in one statement
    //  - this method creates a TSQLTableJSON, fill it with the supplied JSON buffer,
    // then call FillPrepare - previous CreateJoined() method retrieve only
    // one record, this one more multiple rows
    // - you should then loop for all rows using 'while Rec.FillOne do ...'
    // - use this constructor if you want all TSQLRecord published properties to
    // be allocated, and loaded with the corresponding values
    // - Free/Destroy will release these instances
    // - warning: if you call Update() after it, only the main object will be
    // updated, not the nested TSQLRecord properties
    constructor CreateAndFillPrepareJoined(aClient: TSQLRest;
      const aFormatSQLJoin: RawUTF8; const aParamsSQLJoin, aBoundsSQLJoin: array of const);
    /// this constructor initializes the object including all TSQLRecordMany properties,
    // and prepares itself to loop through a JOINed statement
    // - the created instance will have all its TSQLRecordMany Dest property allocated
    // with proper instance (and not only pointer(DestID) e.g.), ready to be
    // consumed during a while FillOne do... loop (those instances will be
    // freed by TSQLRecord.FillClose or Destroy) - and the Source property
    // won't contain pointer(SourceID) but the main TSQLRecord instance
    // - the aFormatSQLJoin clause will define a WHERE clause for an automated
    // JOINed statement, including TSQLRecordMany published properties (and
    // their nested properties)
    // - a typical use could be the following:
    // ! aProd := TSQLProduct.CreateAndFillPrepareMany(Database,
    // !   'Owner=? and Categories.Dest.Name=? and (Sizes.Dest.Name=? or Sizes.Dest.Name=?)',[],
    // !   ['mark','for boy','small','medium']);
    // ! if aProd<>nil 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<?',[],[1000,2000]));
    // or call the overloaded FillPrepare() method directly with  BoundsSQLWhere
    // array of parameters
    // - aCustomFieldsCSV can be used to specify which fields must be retrieved
    // - default aCustomFieldsCSV='' will retrieve all simple table fields, but
    // you may need  to access only one or several fields, and will save remote
    // bandwidth by specifying the needed fields
    // - if aCustomFieldsCSV='*', it will retrieve all fields, including BLOBs
    // - note that you should not use this aCustomFieldsCSV optional parameter if
    // you want to Update the retrieved record content later, since any
    // missing fields will be left with previous values - but BatchUpdate() can be
    // safely used after FillPrepare (will set only ID, TModTime and mapped fields)
    function FillPrepare(aClient: TSQLRest; const aSQLWhere: RawUTF8='';
      const aCustomFieldsCSV: RawUTF8=''; aCheckTableName: TSQLCheckTableName=ctnNoCheck): boolean; overload;
    /// prepare to get values using a specified WHERE clause with '%' parameters
    // - 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
    // - 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
    // - note that this method prototype changed with revision 1.17 of the
    // framework: array of const used to be ParamsSQLWhere and '%' in the
    // FormatSQLWhere statement, whereas it now expects bound parameters as '?'
    // - aCustomFieldsCSV can be used to specify which fields must be retrieved
    // - default aCustomFieldsCSV='' will retrieve all simple table fields, but
    // you may need  to access only one or several fields, and will save remote
    // bandwidth by specifying the needed fields
    // - if aCustomFieldsCSV='*', it will retrieve all fields, including BLOBs
    // - note that you should not use this aCustomFieldsCSV optional parameter if
    // you want to Update the retrieved record content later, since any
    // missing fields will be left with previous values - but BatchUpdate() can be
    // safely used after FillPrepare (will set only ID, TModTime and mapped fields)
    function FillPrepare(aClient: TSQLRest; const FormatSQLWhere: RawUTF8;
      const BoundsSQLWhere: array of const; const aCustomFieldsCSV: RawUTF8=''): boolean; overload;
    /// prepare to get values using a specified WHERE clause with '%' and '?' parameters
    // - 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
    // - the FormatSQLWhere clause will replace all '%' chars with the supplied
    // ParamsSQLWhere[] supplied values, and bind all '?' chars as bound
    // parameters with BoundsSQLWhere[] values
    // - aCustomFieldsCSV can be used to specify which fields must be retrieved
    // - default aCustomFieldsCSV='' will retrieve all simple table fields, but
    // you may need  to access only one or several fields, and will save remote
    // bandwidth by specifying the needed fields
    // - if aCustomFieldsCSV='*', it will retrieve all fields, including BLOBs
    // - note that you should not use this aCustomFieldsCSV optional parameter if
    // you want to Update the retrieved record content later, since any
    // missing fields will be left with previous values - but BatchUpdate() can be
    // safely used after FillPrepare (will set only ID, TModTime and mapped fields)
    function FillPrepare(aClient: TSQLRest; const FormatSQLWhere: RawUTF8;
      const ParamsSQLWhere, BoundsSQLWhere: array of const;
      const aCustomFieldsCSV: RawUTF8=''): boolean; overload;
    /// prepare to get values from a list of IDs
    // - 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
    // - aCustomFieldsCSV can be used to specify which fields must be retrieved
    // - default aCustomFieldsCSV='' will retrieve all simple table fields, but
    // you may need  to access only one or several fields, and will save remote
    // bandwidth by specifying the needed fields
    // - if aCustomFieldsCSV='*', it will retrieve all fields, including BLOBs
    // - note that you should not use this aCustomFieldsCSV optional parameter if
    // you want to Update the retrieved record content later, since any
    // missing fields will be left with previous values - but BatchUpdate() can be
    // safely used after FillPrepare (will set only ID, TModTime and mapped fields)
    function FillPrepare(aClient: TSQLRest; const aIDs: array of Int64;
      const aCustomFieldsCSV: RawUTF8=''): boolean; overload;
    // / prepare to loop through a JOINed statement including TSQLRecordMany fields
    // - all TSQLRecordMany.Dest published fields will now contain a true TSQLRecord
    // instance, ready to be filled with the JOINed statement results (these
    // instances will be released at FillClose) - the same for Source which will
    // point to the self instance
    // - the aFormatSQLJoin clause will define a WHERE clause for an automated
    // JOINed statement, including TSQLRecordMany published properties (and
    // their nested properties)
    // - returns true in case of success, false in case of an error during SQL request
    // - a typical use could be the following:
    // ! if aProd.FillPrepareMany(Database,
    // !    'Owner=? and Categories.Dest.Name=? and (Sizes.Dest.Name=? or Sizes.Dest.Name=?)',[],
    // !    ['mark','for boy','small','medium']) then
    // !   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)
    // 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')
    // - the FormatSQLWhere clause will replace all '%' chars with the supplied
    // ParamsSQLWhere[] supplied values, and bind all '?' chars as parameters
    // with BoundsSQLWhere[] values
    // - 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
    // - is used by TSQLRecord.CreateAndFillPrepareMany constructor
    function FillPrepareMany(aClient: TSQLRest; const aFormatSQLJoin: RawUTF8;
      const aParamsSQLJoin, aBoundsSQLJoin: array of const): boolean;
    /// compute a JOINed statement including TSQLRecordMany fields
    // - is called by FillPrepareMany() to retrieve the JSON of the corresponding
    // request: so you could use this method to retrieve directly the same
    // information, ready to be transmitted (e.g. as RawJSON) to a client
    function EnginePrepareMany(aClient: TSQLRest; const aFormatSQLJoin: RawUTF8;
      const aParamsSQLJoin, aBoundsSQLJoin: array of const;
      out ObjectsClass: TSQLRecordClassDynArray; out SQL: RawUTF8): RawUTF8;
    /// fill all published properties of an object from a TSQLTable prepared row
    // - FillPrepare() must have been called before
    // - if Dest is nil, this object values are filled
    // - if Dest is not nil, this object values will be filled, but it won't
    // work with TSQLRecordMany properties (i.e. after FillPrepareMany call)
    // - ID field is updated if first Field Name is 'ID'
    // - Row number is from 1 to Table.RowCount
    // - setter method (write Set*) is called if available
    // - handle UTF-8 SQL to Delphi values conversion (see TPropInfo mapping)
    // - this method has been made virtual e.g. so that a calculated value can be
    // used in a custom field
    function FillRow(aRow: integer; aDest: TSQLRecord=nil): boolean; virtual;
    /// fill all published properties of this object from the next available
    // TSQLTable prepared row
    // - FillPrepare() must have been called before
    // - the Row number is taken from property FillCurrentRow
    // - return true on success, false if no more Row data is available
    // - internally call FillRow() to update published properties values
    function FillOne(aDest: TSQLRecord=nil): boolean;
    /// go to the first prepared row, ready to loop through all rows with FillOne()
    // - the Row number (property FillCurrentRow) is reset to 1
    // - return true on success, false if no Row data is available
    // - you can use it e.g. as:
    // ! while Rec.FillOne do
    // !   dosomethingwith(Rec);
    // ! if Rec.FillRewind then
    // !   while Rec.FillOne do
    // !     dosomeotherthingwith(Rec);
    function FillRewind: boolean;
    /// close any previous FillPrepare..FillOne loop
    // - is called implicitely by FillPrepare() call to release any previous loop
    // - release the internal hidden TSQLTable instance if necessary
    // - is not mandatory if the TSQLRecord is released just after, since
    // TSQLRecord.Destroy will call it
    // - used e.g. by FillFrom methods below to avoid any GPF/memory confusion
    procedure FillClose;
    /// will iterate over all FillPrepare items, appending them as a JSON array
    // - creates a JSON array of all record rows, using
    // ! while FillOne do GetJSONValues(W)...
    procedure AppendFillAsJsonValues(W: TJSONSerializer);

    /// fill all published properties of this object from a TSQLTable result row
    // - call FillPrepare() then FillRow(Row)
    procedure FillFrom(Table: TSQLTable; Row: integer); overload;
    /// fill all published properties of this object from a JSON result row
    // - create a TSQLTable from the JSON data
    // - call FillPrepare() then FillRow(Row)
    procedure FillFrom(const JSONTable: RawUTF8; Row: integer); overload;
    /// fill all published properties of this object from a JSON object result
    // - use JSON data, as exported by GetJSONValues()
    // - JSON data may be expanded or not
    // - make an internal copy of the JSONTable RawUTF8 before calling
    // FillFrom() below
    // - if FieldBits is defined, it will store the identified field index
    procedure FillFrom(const JSONRecord: RawUTF8; FieldBits: PSQLFieldBits=nil); overload;
    /// fill all published properties of this object from a JSON result
    // - the data inside P^ is modified (unescaped and transformed): don't call
    // FillFrom(pointer(JSONRecordUTF8)) but FillFrom(JSONRecordUTF8) which makes
    // a temporary copy of the JSONRecordUTF8 text
    // - use JSON data, as exported by GetJSONValues()
    // - JSON data may be expanded or not
    // - if FieldBits is defined, it will store the identified field index
    procedure FillFrom(P: PUTF8Char; FieldBits: PSQLFieldBits=nil); overload;
    /// fill all published properties of this object from another object
    // - source object must be a parent or of the same class as the current record
    // - 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)
    procedure FillFrom(aRecord: TSQLRecord); overload;
    /// fill the specified properties of this object from another object
    // - source object must be a parent or of the same class as the current record
    // - copy the fields, as specified by their bit index in the source record;
    // you may use aRecord.GetNonVoidFields if you want to update some fields
    procedure FillFrom(aRecord: TSQLRecord; const aRecordFieldBits: TSQLFieldBits); overload;
    {$ifndef NOVARIANTS}
    /// fill all published properties of this object from a supplied TDocVariant
    // object document
    // - is a wrapper around VariantSaveJSON() + FillFrom() methods
    procedure FillFrom(const aDocVariant: variant); overload;
    {$endif}
    /// fill a published property value of this object from a UTF-8 encoded value
    // - see TPropInfo about proper Delphi / UTF-8 type mapping/conversion
    // - use this method to fill a BLOB property, i.e. a property defined with
    // type TSQLRawBlob, since by default all BLOB properties are not
    // set by the standard Retrieve() method (to save bandwidth)
    // - if FieldBits is defined, it will store the identified field index
    procedure FillValue(PropName, Value: PUTF8Char; wasString: boolean;
      FieldBits: PSQLFieldBits=nil);

    /// return true if all published properties values in Other are identical to
    // the published properties of this object
    // - instances must be of the same class type
    // - only simple fields (i.e. not TSQLRawBlob/TSQLRecordMany) are compared
    // - comparison is much faster than SameValues() below
    function SameRecord(Reference: TSQLRecord): boolean;
    /// return true if all published properties values in Other are identical to
    // the published properties of this object
    // - work with different classes: Reference properties name must just be
    // present in the calling object
    // - only simple fields (i.e. not TSQLRawBlob/TSQLRecordMany) are compared
    // - compare the text representation of the values: fields may be of different
    // type, encoding or precision, but still have same values
    function SameValues(Reference: TSQLRecord): boolean;
    /// clear the values of all published properties, and also the ID property
    procedure ClearProperties; overload;
    /// clear the values of specified published properties
    // - '' will leave the content untouched, '*' will clear all simple fields
    procedure ClearProperties(const aFieldsCSV: RawUTF8); overload;
    /// set the simple fields with the supplied values
    // - 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") - 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
    // - return true on success, but be aware that the field list must match
    // the field layout, otherwise if may return true but will corrupt data
    function SimplePropertiesFill(const aSimpleFields: array of const): boolean;
    /// initialize a TDynArray wrapper to map dynamic array property values
    // - if the field name is not existing or not a dynamic array, result.IsVoid
    // will be TRUE
    function DynArray(const DynArrayFieldName: RawUTF8): TDynArray; overload;
    /// initialize a TDynArray wrapper to map dynamic array property values
    // - this overloaded version expect the dynamic array to have been defined
    // with a not null index attribute, e.g.
    // ! published
    // !   property Ints: TIntegerDynArray index 1 read fInts write fInts;
    // !   property Currency: TCurrencyDynArray index 2 read fCurrency write fCurrency;
    // - if the field index is not existing or not a dynamic array, result.IsVoid
    // will be TRUE
    function DynArray(DynArrayFieldIndex: integer): TDynArray; overload;

    /// this property stores the record's integer ID
    // - if this TSQLRecord is not a instance, but a field value in a published
    //  property of type sftID (i.e. TSQLRecord(aID)), this method will try
    //  to retrieve it; but prefered method is to typecast it via PtrInt(aProperty),
    //  because GetID() relies on some low-level Windows memory mapping trick, and
    //  will recognize an ID value up to 1,048,576 (i.e. $100000)
    // - notice: the Setter should not be used usualy; you should not have to write
    //  aRecord.ID := someID in your code, since the ID is set during Retrieve or
    //  Add of the record
    // - use IDValue property for direct read/write access to the record's ID
    // field, if you know that this TSQLRecord is a true allocated class instance
    property ID: TID read GetID;
    /// this property gives direct access to the record's integer ID
    // - using IDValue expects this TSQLRecord to be a true instance, not a
    // transtyped sftID (i.e. TSQLRecord(aID))
    property IDValue: TID read fID write fID;
    /// this read-only property can be used to retrieve the ID as a TSQLRecord object
    // - published properties of type TSQLRecord (one-to-many relationship) do not
    // store real class instances (only exception is if they inherit from
    // TSQLRecordMany) - you can use this value to assign a TSQLRecord instance
    // to a published property, as such:
    // ! Main := TSQLRecordMain.Create;
    // ! Client.Add(Main);
    // ! Detail := TSQLRecordDetail.Create;
    // ! Detail.Main := Main.AsTSQLRecord; // will store Main.ID in MAIN column
    // ! Client.Add(Detail);
    // - is especially useful on 64-bit plaform, since on 32-bit:
    // ! Detail.Main := pointer(Main.ID)
    // compiles (whereas it won't on 64-bit) and is the same than platform-independent
    // ! Detail.Main := Main.AsTSQLRecord;
    // - using Main.AsTSQLRecord will ensure that the ID is retrieved, even
    // if Main itself is not a true instance
    // - if the stored ID is bigger than 32-bit, then it will raise an
    // EORMException: in this case, you should use a TID / T*ID kind of
    // published property, and not a TSQLRecord, which is limited to the
    // pointer size
    // - on FPC, if you get an Error: Incompatible types: got "Pointer" expected
    // "T...", then you are missing a {$mode Delphi} conditional in your unit:
    // the easiest is to include {$I Synopse.inc} at the top of your unit
    property AsTSQLRecord: pointer read GetIDAsPointer;
    /// this property is set to true, if any published property is a BLOB (TSQLRawBlob)
    property HasBlob: boolean read GetHasBlob;
    /// this property returns the published property count with any valid
    // database field except TSQLRawBlob/TSQLRecordMany
    // - by default, the TSQLRawBlob (BLOB) fields are not included into this set:
    // they must be read specificaly (in order to spare bandwidth)
    // - TSQLRecordMany fields are not accessible directly, but as instances
    // created by TSQLRecord.Create
    property SimpleFieldCount: integer read GetSimpleFieldCount;
    /// this property contains the TSQLTable after a call to FillPrepare()
    property FillTable: TSQLTable read GetTable;
    /// this property contains the current row number (beginning with 1),
    // initialized to 1 by FillPrepare(), which will be read by FillOne
    property FillCurrentRow: integer read GetFillCurrentRow;
    /// this property is set to true, if all rows have been browsed after
    // FillPrepare / while FillOne do ...
    property FillReachedEnd: boolean read GetFillReachedEnd;
    /// used internally by FillPrepare() and corresponding Fill*() methods
    property FillContext: TSQLRecordFill read fFill;
    /// 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;
  published
    { published properties in inherited classes will be interpreted as SQL fields }
  end;

  PSQLRecord = ^TSQLRecord;
  TSQLRecordArray = array[0..MaxInt div SizeOf(TSQLRecord)-1] of TSQLRecord;
  PSQLRecordArray = ^TSQLRecordArray;

  /// root class for defining and mapping database records with case-insensitive
  // NOCASE collation
  // - abstract ancestor, from which you may inherit your own ORM classes
  // - by default, any sftUTF8Text field (RawUTF8, UnicodeString, WideString
  // properties) will use our Unicode SYSTEMNOCASE SQLite3 collation, which calls
  // UTF8ILComp() to handle most western languages, but is not standard
  // - you may inherit from this class to ensure any text field will use the
  // faster and SQLite3 built-in NOCASE collation, handling only 7-bit A-Z chars
  // - inherit from TSQLRecordNoCase or TSQLRecordCaseSensitive if you expect
  // your text fields to contain only basic (un)accentued ASCCI characters, and
  // to be opened by any standard/ SQlite3 library or tool (outside of
  // SynSQLite3.pas/SynDBExplorer)
  TSQLRecordNoCase = class(TSQLRecord)
  protected
    /// will call Props.SetCustomCollationForAll(sftUTF8Text,'NOCASE')
    class procedure InternalDefineModel(Props: TSQLRecordProperties); override;
  end;

  /// root class for defining and mapping database records with case-sensitive
  // BINARY collation
  // - abstract ancestor, from which you may inherit your own ORM classes
  // - by default, any sftUTF8Text field (RawUTF8, UnicodeString, WideString
  // properties) will use our Unicode SYSTEMNOCASE SQLite3 collation, which calls
  // UTF8ILComp() to handle most western languages, but is not standard
  // - you may inherit from this class to ensure any text field will use the
  // faster and SQLite3 built-in BINARY collation, which is case-sensitive
  // - inherit from TSQLRecordNoCase or TSQLRecordCaseSensitive if you expect
  // your text fields to contain only basic (un)accentued ASCCI characters, and
  // to be opened by any standard/ SQlite3 library or tool (outside of
  // SynSQLite3.pas/SynDBExplorer)
  TSQLRecordCaseSensitive = class(TSQLRecord)
  protected
    /// will call Props.SetCustomCollationForAll(sftUTF8Text,'BINARY')
    class procedure InternalDefineModel(Props: TSQLRecordProperties); override;
  end;

  /// database records with NOCASE collation and JSON_OPTIONS_FAST_EXTENDED variants
  // - abstract ancestor, from which you may inherit your own ORM classes
  TSQLRecordNoCaseExtended = class(TSQLRecordNoCase)
  protected
    /// will call Props.SetVariantFieldsDocVariantOptions(JSON_OPTIONS_FAST_EXTENDED);
    class procedure InternalDefineModel(Props: TSQLRecordProperties); override;
  end;

  /// database records with BINARY collation and JSON_OPTIONS_FAST_EXTENDED variants
  // - abstract ancestor, from which you may inherit your own ORM classes
  TSQLRecordCaseSensitiveExtended = class(TSQLRecordCaseSensitive)
  protected
    /// will call Props.SetVariantFieldsDocVariantOptions(JSON_OPTIONS_FAST_EXTENDED);
    class procedure InternalDefineModel(Props: TSQLRecordProperties); override;
  end;

  /// database records with NOCASE collation and JSON_OPTIONS_FAST_EXTENDED
  // variants, and itoNoIndex4TID option to avoid indexes on TID/T*ID properties
  // - abstract ancestor, from which you may inherit your own ORM classes
  TSQLRecordNoCaseExtendedNoID = class(TSQLRecordNoCaseExtended)
  public
    /// overriden method forcing no index creation on TID/T*ID properties
    class procedure InitializeTable(Server: TSQLRestServer; const FieldName: RawUTF8;
      Options: TSQLInitializeTableOptions); override;
  end;

  /// allow on-the-fly translation of a TSQLTable grid value
  // - should return valid JSON value of the given cell (i.e. quoted strings,
  // or valid JSON object/array) unless HumanFriendly is defined
  // - e.g. TSQLTable.OnExportValue property will customize TSQLTable's
  // GetJSONValues, GetHtmlTable, and GetCSVValues methods returned content
  TOnSQLTableGetValue = function(Sender: TSQLTable; Row, Field: integer;
    HumanFriendly: boolean): RawJSON of object;

  /// store TSQLFieldType and RTTI for a given TSQLTable field
  TSQLTableFieldType = record
    /// the field kind, as in JSON (match TSQLPropInfo.SQLFieldTypeStored)
    ContentType: TSQLFieldType;
    /// how this field could be stored in a database
    // - equals ftUnknown if InitFields guessed the field type, or for sftVariant
    ContentDB: TSQLDBFieldType;
    /// the field size in bytes; -1 means not computed yet
    ContentSize: integer;
    /// used for sftEnumerate, sftSet and sftBlobDynArray fields
    ContentTypeInfo: pointer;
    /// the corresponding index in fQueryTables[]
    TableIndex: integer;
  end;
  PSQLTableFieldType = ^TSQLTableFieldType;

  /// wrapper to an ORM result table, staticaly stored as UTF-8 text
  // - contain all result in memory, until destroyed
  // - first row contains the field names
  // - following rows contains the data itself
  // - GetString() can be used in a TDrawString
  // - will be implemented as TSQLTableJSON for remote access through optimized
  // JSON content
  TSQLTable = class
  protected
    fRowCount: integer;
    fFieldCount: integer;
    /// contains the data, e.g. as returned by sqlite3_get_table()
    fResults: PPUTF8CharArray;
    fFieldType: array of TSQLTableFieldType;
    fFieldTypeAllRows: boolean;
    /// the field names
    fFieldNames: TRawUTF8DynArray;
    /// used by FieldIndex() for fast O(log(n)) binary search
    fFieldNameOrder: TCardinalDynArray;
    /// contain the fResults[] pointers, after a IDColumnHide() call
    fIDColumn, fNotIDColumn: TPUTF8CharDynArray;
    /// index of a 'ID' field, -1 if none (e.g. after IDColumnHide method call)
    fFieldIndexID: integer;
    /// the internal state counter of the database when the data was retrieved
    fInternalState: cardinal;
    /// contains the parameters used for sorting
    fSortParams: TSQLTableSortParams;
    /// contains the TSQLRecord instances created by NewRecord method
    fOwnedRecords: TSynObjectList;
    /// if the TSQLRecord is the owner of this table, i.e. if it must free it
    fOwnerMustFree: Boolean;
    /// current cursor row (1..RowCount), as set by the Step() method
    fStepRow: integer;
    /// information about the Query sourcing this result set
    fQueryTables: TSQLRecordClassDynArray;
    fQueryColumnTypes: array of TSQLFieldType;
    fQuerySQL: RawUTF8;
    fQueryTableNameFromSQL: RawUTF8;
    fQueryTableIndexFromSQL: integer; // -2=nosearch -1=notfound fQueryTables[0..n]
    /// field length information
    fFieldLengthMean: TIntegerDynArray;
    fFieldLengthMeanSum: integer;
    /// column bit set at parsing to mark a string value (e.g. "..." in JSON)
    fFieldParsedAsString: set of 0..255;
    fOnExportValue: TOnSQLTableGetValue;
    /// avoid GPF when TSQLTable is nil
    function GetRowCount: integer; {$ifdef HASINLINE}inline;{$endif}
    /// fill the fFieldType[] array (from fQueryTables[] or fResults[] content)
    procedure InitFieldTypes;
    /// fill the internal fFieldNames[] array
    procedure InitFieldNames;
    /// guess the property type information from ORM
    function FieldPropFromTables(const PropName: RawUTF8;
      out PropInfo: TSQLPropInfo; out TableIndex: integer): TSQLFieldType;
    function GetQueryTableNameFromSQL: RawUTF8;
  public
    /// initialize the result table
    // - you can optionaly associate the corresponding TSQLRecordClass types,
    // by which the results were computed (it will use RTTI for column typing)
    constructor Create(const aSQL: RawUTF8);
    /// initialize the result table
    // - you can associate the corresponding TSQLRecordClass types,
    // by which the results were computed (it will use RTTI for column typing)
    constructor CreateFromTables(const Tables: array of TSQLRecordClass; const aSQL: RawUTF8);
    /// initialize the result table
    // - you can set the expected column types matching the results column layout
    constructor CreateWithColumnTypes(const ColumnTypes: array of TSQLFieldType; const aSQL: RawUTF8);
    /// free associated memory and owned records
    destructor Destroy; override;
    /// read-only access to a particular field value, as UTF-8 encoded buffer
    // - if Row and Fields are correct, returns a pointer to the UTF-8 buffer,
    // or nil if the corresponding JSON was null or ""
    // - if Row and Fields are not correct, returns nil
    function Get(Row,Field: integer): PUTF8Char; overload;
      {$ifdef HASINLINE}inline;{$endif}
    /// read-only access to a particular field value, as RawUTF8 text
    function GetU(Row,Field: integer): RawUTF8; overload;
    /// read-only access to a particular field value, as UTF-8 encoded buffer
    // - points to memory buffer allocated by Init()
    function Get(Row: integer; const FieldName: RawUTF8): PUTF8Char; overload;
    /// read-only access to a particular field value, as RawUTF8 text
    function GetU(Row: integer; const FieldName: RawUTF8): RawUTF8; overload;
    /// read-only access to a particular field value, as Win Ansi text
    function GetA(Row,Field: integer): WinAnsiString;
    /// read-only access to a particular field value, as Win Ansi text shortstring
    function GetS(Row,Field: integer): shortstring;
    {$ifndef NOVARIANTS}
    /// read-only access to a particular field value, as a Variant
    // - text will be stored as RawUTF8 (as varString type)
    // - will try to use the most approriate Variant type for conversion (will
    // use e.g. TDateTime for sftDateTime, or a TDocVariant for JSON objects
    // in a sftVariant column) - so you should better set the exact field types
    // (e.g. from ORM) before calling this method
    function GetVariant(Row,Field: integer): variant; overload;
    /// read-only access to a particular field value, as a Variant
    // - text will be stored as RawUTF8 (as varString type)
    // - will try to use the most approriate Variant type for conversion (will
    // use e.g. TDateTime for sftDateTime, or a TDocVariant for JSON objects
    // in a sftVariant column) - so you should better set the exact field types
    // (e.g. from ORM) before calling this method
    procedure GetVariant(Row,Field: integer; var result: variant); overload;
    /// read-only access to a particular field, via a lookup field name
    // - will call GetVariant() on the corresponding field
    // - returns null if the lookup did not have any match
    function GetValue(const aLookupFieldName,aLookupValue,aValueFieldName: RawUTF8): variant;
    {$endif}
    /// read-only access to a particular field value, as VCL string text
    // - the global UTF8ToString() function will be used for the conversion:
    // for proper i18n handling before Delphi 2009, you should use the
    // overloaded method with aUTF8ToString=Language.UTF8ToString
    function GetString(Row,Field: integer): string;
    /// read-only access to a particular field value, as fast Unicode string text
    // - SynUnicode is either WideString, either UnicodeString, depending on the
    // Delphi compiler revision, to ensure fastest native Unicode process available
    function GetSynUnicode(Row,Field: integer): SynUnicode;
    /// fill a unicode buffer with a particular field value
    // - return number of wide characters written in Dest^
    function GetWP(Row,Field: integer; Dest: PWideChar; MaxDestChars: cardinal): integer;
    /// read-only access to a particular field value, as UTF-16 Unicode text
    // - Raw Unicode is WideChar(zero) terminated
    // - its content is allocated to contain all WideChars (not trimed to 255,
    // like GetWP() above
    function GetW(Row,Field: integer): RawUnicode;
    /// read-only access to a particular field value, as integer value
    function GetAsInteger(Row,Field: integer): integer; overload;
      {$ifdef HASINLINE}inline;{$endif}
    /// read-only access to a particular field value, as integer value
    function GetAsInteger(Row: integer; const FieldName: RawUTF8): integer; overload;
      {$ifdef HASINLINE}inline;{$endif}
    /// read-only access to a particular field value, as Int64 value
    function GetAsInt64(Row,Field: integer): Int64; overload;
      {$ifdef HASINLINE}inline;{$endif}
    /// read-only access to a particular field value, as Int64 value
    function GetAsInt64(Row: integer; const FieldName: RawUTF8): Int64; overload;
      {$ifdef HASINLINE}inline;{$endif}
    /// read-only access to a particular field value, as extended value
    function GetAsFloat(Row,Field: integer): TSynExtended; overload;
      {$ifdef HASINLINE}inline;{$endif}
    /// read-only access to a particular field value, as extended value
    function GetAsFloat(Row: integer; const FieldName: RawUTF8): TSynExtended; overload;
      {$ifdef HASINLINE}inline;{$endif}
    /// read-only access to a particular field value, as TDateTime value
    // - sftDateTime/sftDateTimeMS will be converted from ISO-8601 text
    // - sftTimeLog, sftModTime, sftCreateTime will expect the content to be
    // encoded as a TTimeLog Int64 value - as sftInteger may have been
    // identified by TSQLTable.InitFieldTypes
    // - sftUnixTime/sftUnixMSTime field will call UnixTimeToDateTime/UnixMSTimeToDateTime
    // - for sftTimeLog, sftModTime, sftCreateTime or sftUnixTime fields, you
    // may have to force the column type, since it may be identified as sftInteger
    // or sftCurrency by default from its JSON number content, e.g. via:
    // ! aTable.SetFieldType('FieldName',sftModTime);
    // - sftCurrency,sftFloat will return the corresponding double value
    // - any other types will try to convert ISO-8601 text }
    function GetAsDateTime(Row,Field: integer): TDateTime; overload;
    /// read-only access to a particular field value, as TDateTime value
    function GetAsDateTime(Row: integer; const FieldName: RawUTF8): TDateTime; overload;
    /// read-only access to a particular field value, as currency value
    function GetAsCurrency(Row,Field: integer): currency; overload;
      {$ifdef HASINLINE}inline;{$endif}
    /// read-only access to a particular field value, as currency value
    function GetAsCurrency(Row: integer; const FieldName: RawUTF8): currency; overload;
      {$ifdef HASINLINE}inline;{$endif}
    /// read-only access to a particular field value, ready to be displayed
    // - mostly used with Row=0, i.e. to get a display value from a field name
    // - use "string" type, i.e. UnicodeString for Delphi 2009+
    // - value is first un-camel-cased: 'OnLine' value will return 'On line' e.g.
    // - then System.LoadResStringTranslate() is called if available
    function GetCaption(Row,Field: integer): string;
    /// read-only access to a particular Blob value
    // - a new TSQLRawBlob is created
    // - Blob data is converted from SQLite3 BLOB literals (X'53514C697465' e.g.)
    // or Base-64 encoded content ('\uFFF0base64encodedbinary')
    // - prefered manner is to directly use REST protocol to retrieve a blob field
    function GetBlob(Row,Field: integer): TSQLRawBlob;
    /// read-only access to a particular Blob value
    // - a new TBytes is created
    // - Blob data is converted from SQLite3 BLOB literals (X'53514C697465' e.g.)
    //   or Base-64 encoded content ('\uFFF0base64encodedbinary')
    // - prefered manner is to directly use REST protocol to retrieve a blob field
    function GetBytes(Row,Field: integer): TBytes;
    /// read-only access to a particular Blob value
    // - a new TCustomMemoryStream is created - caller shall free its instance
    // - Blob data is converted from SQLite3 BLOB literals (X'53514C697465' e.g.)
    //   or Base-64 encoded content ('\uFFF0base64encodedbinary')
    // - prefered manner is to directly use REST protocol to retrieve a blob field
    function GetStream(Row,Field: integer): TStream;
    /// read-only access to a particular field value, as VCL text
    // - Client is one TSQLClient instance (used to display TRecordReference via
    // the associated TSQLModel)
    // - returns the Field Type
    // - return generic string Text, i.e. UnicodeString for Delphi 2009+, ready
    // to be displayed to the VCL, for sftEnumerate, sftTimeLog,
    // sftUnixTime/sftUnixMSTime and sftRecord/sftRecordVersion/sftID/sftTID
    // - returns '' as string Text, if text can by displayed directly
    // with Get*() methods above
    // - returns '' for other properties kind, if UTF8ToString is nil,
    // or the ready to be displayed value if UTF8ToString event is set
    // (to be used mostly with Language.UTF8ToString)
    // - CustomFormat can optionaly set a custom format string, e.g. '%f' or '%n'
    // or complex FormatFloat()/FormatCurr() syntax (as '#,##0.00') for sftFloat
    // and sftCurrency columns (instead of plain JSON float value), or
    // date/time format as expected by FormatDateTime() for all date time kind
    // of fields (as sftDateTime, sftDateTimeMS, sftTimeLog, sftModTime,
    // sftCreateTime, sftUnixTime, sftUnixMSTime)
    function ExpandAsString(Row,Field: integer; Client: TObject; out Text: string;
      const CustomFormat: string=''): TSQLFieldType;
    /// read-only access to a particular field value, as VCL text
    // - this method is just a wrapper around ExpandAsString method, returning
    // the content as a SynUnicode string type (i.e. UnicodeString since Delphi
    // 2009, and WideString for non Unicode versions of Delphi)
    // - it is used by the reporting layers of the framework (e.g. TSQLRibbon.AddToReport)
    function ExpandAsSynUnicode(Row,Field: integer; Client: TObject; out Text: SynUnicode): TSQLFieldType;
    /// read-only access to a particular DateTime field value
    // - expect SQLite3 TEXT field in ISO 8601 'YYYYMMDD hhmmss' or
    // 'YYYY-MM-DD hh:mm:ss' format
    function GetDateTime(Row,Field: integer): TDateTime;
    /// read-only access to a particular TTimeLog field value
    // - return the result as TTimeLogBits.Text() Iso-8601 encoded text
    function GetTimeLog(Row,Field: integer; Expanded: boolean; FirstTimeChar: AnsiChar = 'T'): RawUTF8;
    /// widechar length (UTF-8 decoded as UTF-16) of a particular field value
    // - could be used with VCL's UnicodeString, or for Windows API
    function LengthW(Row,Field: integer): integer;
    /// get all values for a specified field into a dynamic RawUTF8 array
    // - don't perform any conversion, but just create an array of raw PUTF8Char data
    // - returns the number of rows in Values[]
    function GetRowValues(Field: integer; out Values: TRawUTF8DynArray): integer; overload;
    /// get all values for a specified field into a dynamic Integer array
    // - returns the number of rows in Values[]
    function GetRowValues(Field: integer; out Values: TInt64DynArray): integer; overload;
    /// get all values for a specified field as CSV
    // - don't perform any conversion, but create a CSV from raw PUTF8Char data
    function GetRowValues(Field: integer; const Sep: RawUTF8=',';
      const Head: RawUTF8=''; const Trail: RawUTF8=''): RawUTF8; overload;
    /// get all values lengths for a specified field into a PIntegerArray
    // - returns the total length as result, and fill LenStore with all rows
    // individual lengths using StrLen() - caller should eventually call
    // LenStore.Done to release any temp memory
    // - returns 0 if Field is invalid or no data is stored in this TSQLTable -
    // don't call LenStore.Done in this case
    function GetRowLengths(Field: integer; var LenStore: TSynTempBuffer): integer;
    {$ifndef NOVARIANTS}
    /// retrieve a field value in a variant
    // - returns null if the row/field is incorrect
    // - expand* methods will allow to return human-friendly representations
    procedure GetAsVariant(row,field: integer; out value: variant;
      expandTimeLogAsText,expandEnumsAsText,expandHugeIDAsUniqueIdentifier: boolean;
      options: TDocVariantOptions=JSON_OPTIONS_FAST);
    /// retrieve a row value as a variant, ready to be accessed via late-binding
    // - Row parameter numbering starts from 1 to RowCount
    // - this method will return a TDocVariant containing a copy of all
    // field values of this row, uncoupled to the TSQLTable instance life time
    // - expand* methods will allow to return human-friendly representations
    procedure ToDocVariant(Row: integer; out doc: variant;
      options: TDocVariantOptions=JSON_OPTIONS_FAST;
      expandTimeLogAsText: boolean=false; expandEnumsAsText: boolean=false;
      expandHugeIDAsUniqueIdentifier: boolean=false); overload;
    /// retrieve all row values as a dynamic array of variants, ready to be
    // accessed via late-binding
    // - if readonly is TRUE, will contain an array of TSQLTableRowVariant, which
    // will point directly to the TSQLTable, which should remain allocated
    // - if readonly is FALSE, will contain an array of TDocVariant, containing
    // a copy of all field values of this row, uncoupled to the TSQLTable instance
    // - readonly=TRUE is faster to allocate (around 4 times for 10,000 rows), but
    // may be slightly slower to access than readonly=FALSE, if all values are
    // likely be accessed later in the process
    procedure ToDocVariant(out docs: TVariantDynArray; readonly: boolean); overload;
    /// retrieve all row values as a TDocVariant of kind dvArray, ready to be
    // accessed via late-binding
    // - if readonly is TRUE, will contain an array of TSQLTableRowVariant, which
    // will point directly to the TSQLTable, which should remain allocated
    // - if readonly is FALSE, will contain an array of TDocVariant, containing
    // a copy of all field values of this row, uncoupled to the TSQLTable instance
    // - readonly=TRUE is faster to allocate (around 4 times for 10,000 rows), but
    // may be slightly slower to access than readonly=FALSE, if all values are
    // likely be accessed later in the process
    procedure ToDocVariant(out docarray: variant; readonly: boolean); overload;
      // {$ifdef HASINLINE}inline;{$endif} won't reset docarray as required
    {$endif NOVARIANTS}

    /// save the table values in JSON format
    // - JSON data is added to TJSONWriter, with UTF-8 encoding, and not flushed
    // - if Expand is true, JSON data is an array of objects, for direct use
    // with any Ajax or .NET client:
    // & [ {"col1":val11,"col2":"val12"},{"col1":val21,... ]
    // - if W.Expand is false, JSON data is serialized (used in TSQLTableJSON)
    // & { "fieldCount":1,"values":["col1","col2",val11,"val12",val21,..] }
    // - RowFirst and RowLast can be used to ask for a specified row extent
    // of the returned data (by default, all rows are retrieved)
    // - IDBinarySize will force the ID field to be stored as hexadecimal text
    procedure GetJSONValues(W: TJSONWriter; RowFirst: integer=0;
      RowLast: integer=0; IDBinarySize: integer=0); overload;
    /// same as the overloaded method, but appending an array to a TStream
    procedure GetJSONValues(JSON: TStream; Expand: boolean;
      RowFirst: integer=0; RowLast: integer=0; IDBinarySize: integer=0); overload;
    /// same as the overloaded method, but returning result into a RawUTF8
    function GetJSONValues(Expand: boolean; IDBinarySize: integer=0;
      BufferSize: integer=0): RawUTF8; overload;
    /// save the table as CSV format, into a stream
    // - if Tab=TRUE, will use TAB instead of ',' between columns
    // - you can customize the ',' separator - use e.g. the global ListSeparator
    // variable (from SysUtils) to reflect the current system definition (some
    // country use ',' as decimal separator, for instance our "douce France")
    // - AddBOM will add a UTF-8 Byte Order Mark at the beginning of the content
    procedure GetCSVValues(Dest: TStream; Tab: boolean; CommaSep: AnsiChar=',';
      AddBOM: boolean=false; RowFirst: integer=0; RowLast: integer=0); overload;
    /// save the table as CSV format, into a string variable
    // - if Tab=TRUE, will use TAB instead of ',' between columns
    // - you can customize the ',' separator - use e.g. the global ListSeparator
    // variable (from SysUtils) to reflect the current system definition (some
    // country use ',' as decimal separator, for instance our "douce France")
    // - AddBOM will add a UTF-8 Byte Order Mark at the beginning of the content
    function GetCSVValues(Tab: boolean; CommaSep: AnsiChar=',';
      AddBOM: boolean=false; RowFirst: integer=0; RowLast: integer=0): RawUTF8; overload;
    /// save the table in 'schemas-microsoft-com:rowset' XML format
    // - this format is used by ADODB.recordset, easily consumed by MS apps
    // - see @https://synopse.info/forum/viewtopic.php?pid=11691#p11691
    procedure GetMSRowSetValues(Dest: TStream; RowFirst,RowLast: integer); overload;
    /// save the table in 'schemas-microsoft-com:rowset' XML format
    // - this format is used by ADODB.recordset, easily consumed by MS apps
    // - see @https://synopse.info/forum/viewtopic.php?pid=11691#p11691
    function GetMSRowSetValues: RawUTF8; overload;
    /// save the table in Open Document Spreadsheet compressed format
    // - this is a set of XML files compressed in a zip container
    // - this method will return the raw binary buffer of the file
    // - see @https://synopse.info/forum/viewtopic.php?id=2133
    function GetODSDocument(withColumnTypes: boolean=false): RawByteString;
    /// append the table content as a HTML <table> ... </table>
    procedure GetHtmlTable(Dest: TTextWriter); overload;
    /// save the table as a <html><body><table> </table></body></html> content
    function GetHtmlTable(const Header: RawUTF8='<head><style>table,th,td'+
      '{border: 1px solid black;border-collapse: collapse;}th,td{padding: 5px;'+
      'font-family: sans-serif;}</style></head>'#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<TSQLRecord> 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<T: TSQLRecord>: TObjectList<T>; overload;
    {$endif ISDELPHI2010}
    /// 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 fPrivateCopy content changed (then fPrivateCopyHash
    // will be updated using crc32c hash if aUpdateHash is set)
    function PrivateCopyChanged(aJSON: PUTF8Char; aLen: integer;
      aUpdateHash: boolean): 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=unicode61 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=unicode61 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=unicode61 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 - note that by design, those source/dest tables are stored as
  // pointers, so are limited to 32-bit ID values on 32-bit systems
  // - 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,2000]));
    function FillMany(aClient: TSQLRest; aSourceID: TID=0;
      const aAndWhereSQL: RawUTF8=''): integer;
    /// retrieve all records associated to a particular Dest record, which
    // has a TSQLRecordMany property
    // - returns the Count of records corresponding to this aSource record
    // - use a "for .." loop or a "while FillOne do ..." loop to iterate
    // through all Dest items, getting also any additional 'through' columns
    // - 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.FillManyFromDest(Client,DestID,FormatUTF8('Salary>? AND Salary<?',[],[1000,2000]));
    function FillManyFromDest(aClient: TSQLRest; aDestID: TID;
      const aAndWhereSQL: RawUTF8=''): integer;
    /// retrieve all Dest items IDs associated to the specified Source
    function DestGet(aClient: TSQLRest; aSourceID: TID; out DestIDs: TIDDynArray): boolean; overload;
    /// retrieve all Dest items IDs associated to the current Source ID
    // - source 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 call the other overloaded method
    function DestGet(aClient: TSQLRest; out DestIDs: TIDDynArray): boolean; overload;
    /// retrieve all Source items IDs associated to the specified Dest ID
    function SourceGet(aClient: TSQLRest; aDestID: TID; out SourceIDs: TIDDynArray): boolean;
    /// retrieve all Dest items IDs associated to the current or
    // specified Source ID, adding a WHERE condition against the Dest rows
    // - if aSourceID is 0, the value is taken from current fSourceID field
    // (set by TSQLRecord.Create)
    // - aDestWhereSQL can specify the Dest table name in the statement, e.g.
    //  '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,2000])
    // - this is faster than a manual FillMany() then loading each Dest,
    // because the condition is executed in the SQL statement by the server
    function DestGetJoined(aClient: TSQLRest; const aDestWhereSQL: RawUTF8;
      aSourceID: TID; out DestIDs: TIDDynArray): boolean; overload;
    /// create a Dest record, then FillPrepare() it to retrieve all Dest items
    // associated to the current or specified Source ID, adding a WHERE condition
    // against the Dest rows
    // - if aSourceID is 0, the value is taken from current fSourceID field
    // (set by TSQLRecord.Create)
    // - aDestWhereSQL can specify the Dest table name in the statement, e.g.
    // 'Salary>:(1000): AND Salary<:(2000):') according to TSQLRecordMany
    // properties - note that you should better use such inlined parameters as
    // ! FormatUTF8('Salary>? AND Salary<?',[],[1000,2000])
    function DestGetJoined(aClient: TSQLRest; const aDestWhereSQL: RawUTF8;
      aSourceID: TID): TSQLRecord; overload;
    /// create a TSQLTable, containing all specified Fields, after a JOIN
    // associated to the current or specified Source ID
    // - the Table will have the fields specified by the JoinKind parameter
    // - aCustomFieldsCSV can be used to specify which fields must be retrieved
    // (for jkDestFields, jkPivotFields, jkPivotAndDestFields) - default is all
    // - if aSourceID is 0, the value is taken from current fSourceID field
    // (set by TSQLRecord.Create)
    // - aDestWhereSQL can specify the Dest table name in the statement, e.g.
    // 'Salary>:(1000): AND Salary<:(2000):') according to TSQLRecordMany
    // properties - note that you should better use such inlined parameters as
    // ! FormatUTF8('Salary>? AND Salary<?',[],[1000,2000])
    function DestGetJoinedTable(aClient: TSQLRest; const aDestWhereSQL: RawUTF8;
      aSourceID: TID; JoinKind: TSQLRecordManyJoinKind;
      const aCustomFieldsCSV: RawUTF8=''): TSQLTable;
    /// add a Dest record to the Source record list
    // - returns TRUE on success, FALSE on error
    // - if NoDuplicates is TRUE, the existence of this Source/Dest ID pair
    // is first checked
    // - current Source and Dest properties are filled with the corresponding
    // TRecordReference values corresponding to the supplied IDs
    // - any current value of the additional fields are used to populate the
    // newly created content (i.e. all published properties of this record)
    // - if aUseBatch is set, it will use this TSQLRestBach.Add() instead
    // of the slower aClient.Add() method
    function ManyAdd(aClient: TSQLRest; aSourceID, aDestID: TID;
      NoDuplicates: boolean=false; aUseBatch: TSQLRestBatch=nil): boolean; overload;
    /// add a Dest record to the current Source record list
    // - source 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 call the other overloaded method
    function ManyAdd(aClient: TSQLRest; aDestID: TID;
      NoDuplicates: boolean=false): boolean; overload;
    /// will delete the record associated with a particular Source/Dest pair
    // - will return TRUE if the pair was found and successfully deleted
    // - if aUseBatch is set, it will use this TSQLRestBach.Delete() instead
    // of the slower aClient.Delete() method
    function ManyDelete(aClient: TSQLRest; aSourceID, aDestID: TID;
      aUseBatch: TSQLRestBatch=nil): boolean; overload;
    /// will delete the record associated with the current source and a specified Dest
    // - source 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 call the other overloaded method
    function ManyDelete(aClient: TSQLRest; aDestID: TID): boolean; overload;
    /// will retrieve the record associated with a particular Source/Dest pair
    // - will return TRUE if the pair was found
    // - in this case, all "through" columns are available in the TSQLRecordMany
    // field instance
    function ManySelect(aClient: TSQLRest; aSourceID, aDestID: TID): boolean; overload;
    /// will retrieve the record associated with the current source and a specified Dest
    // - source 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 call the other overloaded method
    function ManySelect(aClient: TSQLRest; aDestID: TID): boolean; overload;

    // get the SQL WHERE statement to be used to retrieve the associated
    // records according to a specified ID
    // - search for aID as Source ID if isDest is FALSE
    // - search for aID as Dest ID if isDest is TRUE
    // - 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
    // such inlined parameters e.g. calling
    // ! FormatUTF8('Salary>? AND Salary<?',[],[1000,2000])
    function IDWhereSQL(aClient: TSQLRest; aID: TID; isDest: boolean;
      const aAndWhereSQL: RawUTF8=''): RawUTF8;
  end;

  /// a base record, with a JSON-logging capability
  // - used to store a log of events into a JSON text, easy to be displayed
  // with a TSQLTableToGrid
  // - this log can then be stored as a RawUTF8 field property into a result
  // record, for instance
  TSQLRecordLog = class(TSQLRecord)
  protected
    /// store the Log Table JSON content
    fLogTableStorage: TMemoryStream;
    /// used by Log() to add the value of OneLog to fLogTableStorage
    fLogTableWriter: TJSONSerializer;
    /// current internal row count
    fLogTableRowCount: integer;
    /// maximum rows count
    fMaxLogTableRowCount: integer;
  public
    /// initialize the internal storage with a supplied JSON content
    // - this JSON content must follow the format retrieved by
    // LogTableJSON and LogTableJSONFrom methods
    constructor CreateFrom(OneLog: TSQLRecord; const aJSON: RawUTF8);
    /// release the private fLogTableWriter and fLogTableStorage objects
    destructor Destroy; override;
    /// add the value of OneLog to the Log Table JSON content
    // - the ID property of the supplied OneLog record is incremented before adding
    procedure Log(OneLog: TSQLRecord);
    /// returns the JSON data as added by previous call to Log()
    // - JSON data is in not-expanded format
    // - this function can be called multiple times
    function LogTableJSON: RawUTF8;
    /// returns the internal position of the Log content
    // - use this value to later retrieve a log range with LogTableJSONFrom()
    function LogCurrentPosition: integer;
    /// returns the log JSON data from a given start position
    // - StartPosition was retrieved previously with LogCurrentPosition
    // - if StartPosition=0, the whole Log content is returned
    // - multiple instances of LogCurrentPosition/LogTableJSONFrom() can be
    // used at once
    function LogTableJSONFrom(StartPosition: integer): RawUTF8;
    /// the current associated Log Table rows count value
    // - is incremented every time Log() method is called
    // - will be never higher than MaxLogTableRowCount below (if set)
    property LogTableRowCount: integer read fLogTableRowCount;
    /// if the associated Log Table rows count reachs this value, the
    // first data row will be trimed
    // - do nothing is value is left to 0 (which is the default)
    // - total rows count won't never be higher than this value
    // - used to spare memory usage
    property MaxLogTableRowCount: integer read fMaxLogTableRowCount;
  end;

  /// common ancestor for tables with digitally signed RawUTF8 content
  // - content is signed according to a specific User Name and the digital
  // signature date and time
  // - internaly uses the very secure SHA-256 hashing algorithm for performing
  // the digital signature
  TSQLRecordSigned = class(TSQLRecord)
  protected
    /// time and date of signature
    fSignatureTime: TTimeLog;
    /// hashed signature
    fSignature: RawUTF8;
    function ComputeSignature(const UserName,Content: RawByteString): RawUTF8;
  public
    /// time and date of signature
    // - if the signature is invalid, this field will contain numerical 1 value
    // - this property is defined here to allow inherited to just declared the name
    // in its published section:
    // ! property SignatureTime;
    property SignatureTime: TTimeLog read fSignatureTime write fSignatureTime;
    /// as the Content of this record is added to the database,
    // its value is hashed and stored as 'UserName/03A35C92....' into this property
    // - secured SHA-256 hashing is used internaly
    // - digital signature is allowed only once: this property is written only once
    // - this property is defined here to allow inherited to just declared the name
    // in its published section:
    // ! property Signature;
    property Signature: RawUTF8 read fSignature write fSignature;
  public
    /// use this procedure to sign the supplied Content of this record for a
    // specified UserName, with the current Date and Time
    // - SHA-256 hashing is used internaly
    // - returns true if signed successfully (not already signed)
    function SetAndSignContent(const UserName: RawUTF8;
      const Content: RawByteString; ForcedSignatureTime: Int64=0): boolean;
    /// returns true if this record content is correct according to the
    // stored digital Signature
    function CheckSignature(const Content: RawByteString): boolean;
    /// retrieve the UserName who digitally signed this record
    // - returns '' if was not digitally signed
    function SignedBy: RawUTF8;
    /// reset the stored digital signature
    // - SetAndSignContent() can be called after this method
    procedure UnSign;
  end;

  /// a base record, which will have creation and modification timestamp fields
  TSQLRecordTimed = class(TSQLRecord)
  protected
    fCreated: TCreateTime;
    fModified: TModTime;
  published
    /// will be filled by the ORM when this item will be created in the database
    property Created: TCreateTime read fCreated write fCreated;
    /// will be filled by the ORM each time this item will be written in the database
    property Modified: TModTime read fModified write fModified;
  end;

  /// common ancestor for tables which should implement any interface
  // - by default, TSQLRecord does not implement any interface: this does make
  // sense for performance and resource use reasons
  // - inherit from this class if you want your class to implement the needed
  // IInterface methods (QueryInterface/AddRef/Release)
  TSQLRecordInterfaced = class(TSQLRecord, IInterface)
  protected
    fRefCount: Integer;
    {$ifdef FPC}
    function QueryInterface(
      {$IFDEF FPC_HAS_CONSTREF}constref{$ELSE}const{$ENDIF} IID: TGUID;
      out Obj): longint; {$IFNDEF WINDOWS}cdecl{$ELSE}stdcall{$ENDIF};
    function _AddRef: longint; {$IFNDEF WINDOWS}cdecl{$ELSE}stdcall{$ENDIF};
    function _Release: longint; {$IFNDEF WINDOWS}cdecl{$ELSE}stdcall{$ENDIF};
    {$else}
    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
    {$endif}
  public
    class function NewInstance: TObject; override;
    procedure AfterConstruction; override;
    procedure BeforeDestruction; override;
    property RefCount: Integer read fRefCount;
  end;

  /// the possible Server-side instance implementation patterns for
  // interface-based services
  // - each interface-based service will be implemented by a corresponding
  // class instance on the server: this parameter is used to define how
  // class instances are created and managed
  // - on the Client-side, each instance will be handled depending on the
  // server side implementation (i.e. with sicClientDriven behavior if necessary)
  // - sicSingle: one object instance is created per call - this is the
  // most expensive way of implementing the service, but is safe for simple
  // workflows (like a one-type call); this is the default setting for
  // TSQLRestServer.ServiceRegister method
  // - sicShared: one object instance is used for all incoming calls and is
  // not recycled subsequent to the calls - the implementation should be
  // thread-safe on the server side
  // - sicClientDriven: one object instance will be created in synchronization
  // with the client-side lifetime of the corresponding interface: when the
  // interface will be released on client, it will be released on the server
  // side - a numerical identifier will be transmitted for all JSON requests
  // - sicPerSession, sicPerUser and sicPerGroup modes will maintain one
  // object instance per running session / user / group (only working if
  // RESTful authentication is enabled) - since it may be shared among users or
  // groups, the sicPerUser and sicPerGroup implementation should be thread-safe
  // - sicPerThread will maintain one object instance per calling thread - it
  // may be useful instead of sicShared mode if the service process expects
  // some per-heavy thread initialization, for instance
  TServiceInstanceImplementation = (
    sicSingle, sicShared, sicClientDriven, sicPerSession, sicPerUser, sicPerGroup,
    sicPerThread);
  /// set of Server-side instance implementation patterns for
  // interface-based services
  TServiceInstanceImplementations = set of TServiceInstanceImplementation;

  /// handled kind of parameters for an interface-based service provider method
  // - we do not handle all kind of Delphi variables, but provide some
  // enhanced types handled by JSONToObject/ObjectToJSON functions (smvObject)
  // or TDynArray.LoadFromJSON / TTextWriter.AddDynArrayJSON methods (smvDynArray)
  // - records will be serialized as Base64 string, with our RecordSave/RecordLoad
  // low-level format by default, or as true JSON objects since Delphi 2010 or
  // after registration via a TTextWriter.RegisterCustomJSONSerializer call
  // - smvRawJSON will transmit the raw JSON content, without serialization
  TServiceMethodValueType = (
    smvNone,
    smvSelf,
    smvBoolean,
    smvEnum,
    smvSet,
    smvInteger,
    smvCardinal,
    smvInt64,
    smvDouble,
    smvDateTime,
    smvCurrency,
    smvRawUTF8,
    smvString,
    smvRawByteString,
    smvWideString,
    smvBinary,
    smvRecord,
    {$ifndef NOVARIANTS}
    smvVariant,
    {$endif}
    smvObject,
    smvRawJSON,
    smvDynArray,
    smvInterface);

  /// handled kind of parameters internal variables for an interface-based method
  // - reference-counted variables will have their own storage
  // - all non referenced-counted variables are stored within some 64-bit content
  // - smvVariant kind of parameter will be handled as a special smvvRecord
  TServiceMethodValueVar = (
    smvvNone, smvvSelf, smvv64, smvvRawUTF8, smvvString, smvvWideString,
    smvvRecord, smvvObject, smvvDynArray, smvvInterface);

  /// set of parameters for an interface-based service provider method
  TServiceMethodValueTypes = set of TServiceMethodValueType;

  /// handled kind of parameters direction for an interface-based service method
  // - IN, IN/OUT, OUT directions can be applied to arguments, and will
  // be available through our JSON-serialized remote access: smdVar and smdOut
  // kind of parameters will be returned within the "result": JSON array
  // - smdResult is used for a function method, to handle the returned value
  TServiceMethodValueDirection = (
    smdConst,
    smdVar,
    smdOut,
    smdResult);
  /// set of parameters direction for an interface-based service method
  TServiceMethodValueDirections = set of TServiceMethodValueDirection;

  /// set of low-level processing options at assembly level
  // - vIsString is included for smvRawUTF8, smvString, smvRawByteString and
  // smvWideString kind of parameter (smvRecord has it to false, even if they
  // are Base-64 encoded within the JSON content, and also smvVariant/smvRawJSON)
  // - vPassedByReference is included if the parameter is passed as reference
  // (i.e. defined as var/out, or is a record or a reference-counted type result)
  // - vIsObjArray is set if the dynamic array is a T*ObjArray, so should be
  // cleared with ObjArrClear() and not TDynArray.Clear
  // - vIsSPI indicates that the value contains some Sensitive Personal
  // Information (e.g. a bank card number or a plain password), which type has
  // been previously registered via TInterfaceFactory.RegisterUnsafeSPIType
  // so that low-level logging won't include such values
  // - vIsQword is set for ValueType=smvInt64 over a QWord unsigned 64-bit value
  // - vIsDynArrayString is set for ValueType=smvDynArray of string values
  // - vIsDateTimeMS is set for ValueType=smvDateTime and TDateTimeMS value
  TServiceMethodValueAsm = set of (vIsString, vPassedByReference,
    vIsObjArray, vIsSPI, vIsQword, vIsDynArrayString, vIsDateTimeMS);

  /// describe a service provider method argument
  {$ifdef USERECORDWITHMETHODS}TServiceMethodArgument = record
    {$else}TServiceMethodArgument = object{$endif}
  public
    /// the argument name, as declared in Delphi
    ParamName: PShortString;
    /// the type name, as declared in Delphi
    ArgTypeName: PShortString;
    /// the low-level RTTI information of this argument
    ArgTypeInfo: PTypeInfo;
    /// we do not handle all kind of Delphi variables
    ValueType: TServiceMethodValueType;
    /// the variable direction as defined at code level
    ValueDirection: TServiceMethodValueDirection;
    /// how the variable may be stored
    ValueVar: TServiceMethodValueVar;
    /// how the variable is to be passed at asm level
    ValueKindAsm: TServiceMethodValueAsm;
    /// byte offset in the CPU stack of this argument
    // - may be -1 if pure register parameter with no backup on stack (x86)
    InStackOffset: integer;
    /// used to specify if the argument is passed as register
    // - contains 0 if parameter is not a register
    // - contains 1 for EAX, 2 for EDX and 3 for ECX registers for x86
    // - contains 1 for RCX, 2 for RDX, 3 for R8, and
    // 4 for R9, with a backing store on the stack for x64
    // - contains 1 for R0, 2 R1 ... 4 for R3, with a backing store on the stack for arm
    // - contains 1 for X0, 2 X1 ... 8 for X7, with a backing store on the stack for aarch64
    RegisterIdent: integer;
    /// used to specify if a floating-point argument is passed as register
    // - contains always 0 for x86/x87
    // - contains 1 for XMM0, 2 for XMM1, ..., 4 for XMM3 for x64
    // - contains 1 for D0, 2 for D1, ..., 8 for D7 for armhf
    // - contains 1 for V0, 2 for V1, ..., 8 for V7 for aarch64
    FPRegisterIdent: integer;
    /// size (in bytes) of this argument on the stack
    SizeInStack: integer;
    /// size (in bytes) of this smvv64 ordinal value
    // - e.g. depending of the associated kind of enumeration
    SizeInStorage: integer;
    /// hexadecimal binary size (in bytes) of this smvv64 ordinal value
    // - set only if ValueType=smvBinary
    SizeInBinary: integer;
    /// index of the associated variable in the local array[ArgsUsedCount[]]
    // - for smdConst argument, contains -1 (no need to a local var: the value
    // will be on the stack only)
    IndexVar: integer;
    /// a TDynArray wrapper initialized properly for this smvDynArray
    DynArrayWrapper: TDynArray;
    {$ifndef FPC}
    /// set ArgTypeName and ArgTypeInfo values from RTTI
    procedure SetFromRTTI(var P: PByte);
    {$endif}
    /// serialize the argument into the TServiceContainer.Contract JSON format
    // - non standard types (e.g. clas, enumerate, dynamic array or record)
    // are identified by their type identifier - so contract does not extend
    // up to the content of such high-level structures
    procedure SerializeToContract(WR: TTextWriter);
    /// check if the supplied argument value is the default (e.g. 0, '' or null)
    function IsDefault(V: pointer): boolean;
    /// unserialize a JSON value into this argument
    function FromJSON(const MethodName: RawUTF8; var R: PUTF8Char; V: pointer;
      Error: PShortString{$ifndef NOVARIANTS}; DVO: TDocVariantOptions{$endif}): boolean;
    /// append the JSON value corresponding to this argument
    // - includes a pending ','
    procedure AddJSON(WR: TTextWriter; V: pointer;
      ObjectOptions: TTextWriterWriteObjectOptions=[woDontStoreDefault]);
    /// append the value corresponding to this argument as within a JSON string
    // - will escape any JSON string character, and include a pending ','
    procedure AddJSONEscaped(WR: TTextWriter; V: pointer);
    /// append the JSON value corresponding to this argument, from its text value
    // - includes a pending ','
    procedure AddValueJSON(WR: TTextWriter; const Value: RawUTF8);
    /// append the default JSON value corresponding to this argument
    // - includes a pending ','
    procedure AddDefaultJSON(WR: TTextWriter);
    /// convert a value into its JSON representation
    procedure AsJson(var DestValue: RawUTF8; V: pointer);
    {$ifndef NOVARIANTS}
    /// convert a value into its variant representation
    // - complex objects will be converted into a TDocVariant, after JSON
    // serialization: variant conversion options may e.g. be retrieve from
    // TInterfaceFactory.DocVariantOptions
    procedure AsVariant(var DestValue: variant; V: pointer;
      Options: TDocVariantOptions);
    /// add a value into a TDocVariant object or array
    // - Dest should already have set its Kind to either dvObject or dvArray
    procedure AddAsVariant(var Dest: TDocVariantData; V: pointer);
    /// normalize a value containing one input or output argument
    // - sets and enumerates will be translated to strings (also in embedded
    // objects and T*ObjArray)
    procedure FixValue(var Value: variant);
    /// normalize a value containing one input or output argument, and add
    // it to a destination variant Document
    // - sets and enumerates will be translated to strings (also in embedded
    // objects and T*ObjArray)
    procedure FixValueAndAddToObject(const Value: variant; var DestDoc: TDocVariantData);
    {$endif}
  end;

  /// pointer to a service provider method argument
  PServiceMethodArgument = ^TServiceMethodArgument;

  /// describe a service provider method arguments
  TServiceMethodArgumentDynArray = array of TServiceMethodArgument;

  /// callback called by TServiceMethodExecute to process an interface
  // callback parameter
  // - implementation should set the Obj local variable to an instance of
  // a fake class implementing the aParamInfo interface
  TServiceMethodExecuteCallback =
    procedure(var Par: PUTF8Char; ParamInterfaceInfo: PTypeInfo; out Obj) of object;

  /// how TServiceMethod.TServiceMethod method will return the generated document
  // - will return either a dvObject or dvArray TDocVariantData, depending on
  // the expected returned document layout
  // - returned content could be "normalized" (for any set or enumerate) if
  // Kind is pdvObjectFixed
  TServiceMethodParamsDocVariantKind = (pdvArray, pdvObject, pdvObjectFixed);

  /// describe an interface-based service provider method
  {$ifdef USERECORDWITHMETHODS}TServiceMethod = record
    {$else}TServiceMethod = object{$endif}
  public
    /// the method URI, i.e. the method name
    // - as declared in Delphi code, e.g. 'Add' for ICalculator.Add
    // - this property value is hashed internaly for faster access
    URI: RawUTF8;
    /// the method default result, formatted as a JSON array
    // - example of content may be '[]' for a procedure or '[0]' for a function
    // - any var/out and potential function result will be set as a JSON array
    // of values, with 0 for numerical values, "" for textual values,
    // false for booleans, [] for dynamic arrays, a void record serialized
    // as expected (including customized serialization) and null for objects
    DefaultResult: RawUTF8;
    /// the fully qualified dotted method name, including the interface name
    // - as used by TServiceContainerInterfaceMethod.InterfaceDotMethodName
    // - match the URI fullpath name, e.g. 'Calculator.Add'
    InterfaceDotMethodName: RawUTF8;
    /// method index in the original (non emulated) interface
    // - our custom methods start at index 3 (RESERVED_VTABLE_SLOTS), since
    // QueryInterface, _AddRef, and _Release are always defined by default
    // - so it maps TServiceFactory.Interface.Methods[ExecutionMethodIndex-3]
    ExecutionMethodIndex: byte;
    /// TRUE if the method is inherited from another parent interface
    IsInherited: boolean;
    /// the directions of arguments with vIsSPI defined in Args[].ValueKindAsm
    HasSPIParams: TServiceMethodValueDirections;
    /// is 0 for the root interface, 1..n for all inherited interfaces
    HierarchyLevel: byte;
    /// describe expected method arguments
    // - Args[0] always is smvSelf
    // - if method is a function, an additional smdResult argument is appended
    Args: TServiceMethodArgumentDynArray;
    /// the index of the result pseudo-argument in Args[]
    // - is -1 if the method is defined as a (not a function)
    ArgsResultIndex: shortint;
    /// the index of the first const / var argument in Args[]
    ArgsInFirst: shortint;
    /// the index of the last const / var argument in Args[]
    ArgsInLast: shortint;
    /// the index of the first var / out / result argument in Args[]
    ArgsOutFirst: shortint;
    /// the index of the last var / out / result argument in Args[]
    ArgsOutLast: shortint;
    /// the index of the last argument in Args[], excepting result
    ArgsNotResultLast: shortint;
    /// the index of the last var / out argument in Args[]
    ArgsOutNotResultLast: shortint;
    /// the number of const / var parameters in Args[]
    // - i.e. the number of elements in the input JSON array
    ArgsInputValuesCount: byte;
    /// the number of var / out parameters +  in Args[]
    // - i.e. the number of elements in the output JSON array or object
    ArgsOutputValuesCount: byte;
    /// true if the result is a TServiceCustomAnswer record
    // - that is, a custom Header+Content BLOB transfert, not a JSON object
    ArgsResultIsServiceCustomAnswer: boolean;
    /// true if there is a single input parameter as RawByteString/TSQLRawBlob
    // - TSQLRestRoutingREST.ExecuteSOAByInterface will identify binary input
    // with mime-type 'application/octet-stream' as expected
    ArgsInputIsOctetStream: boolean;
    /// the index of the first argument expecting manual stack initialization
    // - set if there is any smvObject,smvDynArray,smvRecord,smvInterface or
    // smvVariant
    ArgsManagedFirst: shortint;
    /// the index of the last argument expecting manual stack initialization
    // - set if there is any smvObject, smvDynArray, smvRecord, smvInterface or
    // smvVariant
    ArgsManagedLast: shortint;
    /// contains all used kind of arguments
    ArgsUsed: TServiceMethodValueTypes;
    /// contains the count of variables for all used kind of arguments
    ArgsUsedCount: array[TServiceMethodValueVar] of byte;
    /// needed CPU stack size (in bytes) for all arguments
    // - under x64, does not include the backup space for the four registers
    ArgsSizeInStack: cardinal;
    /// retrieve an argument index in Args[] from its name
    // - search is case insensitive
    // - if Input is TRUE, will search within const / var arguments
    // - if Input is FALSE, will search within var / out / result arguments
    // - returns -1 if not found
    function ArgIndex(ArgName: PUTF8Char; ArgNameLen: integer; Input: boolean): integer;
    /// find the next argument index in Args[]
    // - if Input is TRUE, will search within const / var arguments
    // - if Input is FALSE, will search within var / out / result arguments
    // - returns true if arg is the new value, false otherwise
    function ArgNext(var arg: integer; Input: boolean): boolean;
    /// convert parameters encoded as a JSON array into a JSON object
    // - if Input is TRUE, will handle const / var arguments
    // - if Input is FALSE, will handle var / out / result arguments
    function ArgsArrayToObject(P: PUTF8Char; Input: boolean): RawUTF8;
    /// convert parameters encoded as name=value or name='"value"' or name='{somejson}'
    // into a JSON object
    // - on Windows, use double-quotes ("") anywhere you expect single-quotes (")
    // - as expected e.g. from a command line tool
    // - if Input is TRUE, will handle const / var arguments
    // - if Input is FALSE, will handle var / out / result arguments
    function ArgsCommandLineToObject(P: PUTF8Char; Input: boolean;
      RaiseExceptionOnUnknownParam: boolean=false): RawUTF8;
    /// returns a dynamic array list of all parameter names
    // - if Input is TRUE, will handle const / var arguments
    // - if Input is FALSE, will handle var / out / result arguments
    function ArgsNames(Input: Boolean): TRawUTF8DynArray;
    {$ifndef NOVARIANTS}
    /// computes a TDocVariant containing the input or output arguments values
    // - Values[] should contain the input/output raw values as variant
    // - Kind will specify the expected returned document layout
    procedure ArgsValuesAsDocVariant(Kind: TServiceMethodParamsDocVariantKind;
      out Dest: TDocVariantData; const Values: TVariantDynArray; Input: boolean;
      Options: TDocVariantOptions=[dvoReturnNullForUnknownProperty,dvoValueCopiedByReference]);
    /// normalize a TDocVariant containing the input or output arguments values
    // - "normalization" will ensure sets and enums are seralized as text
    // - if Input is TRUE, will handle const / var arguments
    // - if Input is FALSE, will handle var / out / result arguments
    procedure ArgsAsDocVariantFix(var ArgsObject: TDocVariantData; Input: boolean);
    /// convert a TDocVariant array containing the input or output arguments
    // values in order, into an object with named parameters
    // - here sets and enums will keep their current values, mainly numerical
    // - if Input is TRUE, will handle const / var arguments
    // - if Input is FALSE, will handle var / out / result arguments
    procedure ArgsAsDocVariantObject(const ArgsParams: TDocVariantData;
      var ArgsObject: TDocVariantData; Input: boolean);
    /// computes a TDocVariant containing the input or output arguments values
    // - Values[] should point to the input/output raw binary values, as stored
    // in TServiceMethodExecute.Values during execution
    procedure ArgsStackAsDocVariant(const Values: TPPointerDynArray;
      out Dest: TDocVariantData; Input: Boolean);
    {$endif}
  end;

  /// describe all mtehods of an interface-based service provider
  TServiceMethodDynArray = array of TServiceMethod;

  /// a pointer to an interface-based service provider method description
  // - since TInterfaceFactory instances are shared in a global list, we
  // can safely use such pointers in our code to refer to a particular method
  PServiceMethod = ^TServiceMethod;

  /// common ancestor for storing interface-based service execution statistics
  // - each call could be logged and monitored in the database
  // - TServiceMethodExecute could store all its calls in such a table
  // - enabled on server side via either TServiceFactoryServer.SetServiceLog or
  // TServiceContainerServer.SetServiceLog method
  TSQLRecordServiceLog = class(TSQLRecordNoCaseExtended)
  protected
    fMethod: RawUTF8;
    fInput: variant;
    fOutput: variant;
    fUser: integer;
    fSession: integer;
    fTime: TModTime;
    fMicroSec: integer;
    fIP: RawUTF8;
  public
    /// overriden method creating an index on the Method/MicroSec columns
    class procedure InitializeTable(Server: TSQLRestServer; const FieldName: RawUTF8;
      Options: TSQLInitializeTableOptions); override;
  published
    /// the 'interface.method' identifier of this call
    // - this column will be indexed, for fast SQL queries, with the MicroSec
    // column (for performance tuning)
    property Method: RawUTF8 read fMethod write fMethod;
    /// the input parameters, as a JSON document
    // - will be stored in JSON_OPTIONS_FAST_EXTENDED format, i.e. with
    // shortened field names, for smaller TEXT storage
    // - content may be searched using JsonGet/JsonHas SQL functions on a
    // SQlite3 storage, or with direct document query under MongoDB/PostgreSQL
    property Input: variant read fInput write fInput;
    /// the output parameters, as a JSON document, including result: for a function
    // - will be stored in JSON_OPTIONS_FAST_EXTENDED format, i.e. with
    // shortened field names, for smaller TEXT storage
    // - content may be searched using JsonGet/JsonHas SQL functions on a
    // SQlite3 storage, or with direct document query under MongoDB/PostgreSQL
    property Output: variant read fOutput write fOutput;
    /// the Session ID, if there is any
    property Session: integer read fSession write fSession;
    /// the User ID, if there is an identified Session
    property User: integer read fUser write fUser;
    /// will be filled by the ORM when this record is written in the database
    property Time: TModTime read fTime write fTime;
    /// execution time of this method, in micro seconds
    property MicroSec: integer read fMicroSec write fMicroSec;
    /// if not localhost/127.0.0.1, the remote IP address
    property IP: RawUTF8 read fIP write fIP;
  end;

  /// execution statistics used for DB-based asynchronous notifications
  // - as used by TServiceFactoryClient.SendNotifications
  // - here, the Output column may contain the information about an error
  // occurred during process
  TSQLRecordServiceNotifications = class(TSQLRecordServiceLog)
  protected
    fSent: TTimeLog;
  public
    /// this overriden method will create an index on the 'Sent' column
    class procedure InitializeTable(Server: TSQLRestServer; const FieldName: RawUTF8;
      Options: TSQLInitializeTableOptions); override;
    /// search for pending events since a supplied ID
    // - returns FALSE if no notification was found
    // - returns TRUE ad fill a TDocVariant array of JSON Objects, including
    // "ID": field, and Method as "MethodName": field
    class function LastEventsAsObjects(Rest: TSQLRest; LastKnownID: TID; Limit: integer;
      Service: TInterfaceFactory; out Dest: TDocVariantData;
      const MethodName: RawUTF8 = 'Method'; IDAsHexa: boolean = false): boolean;
    /// allows to convert the Input array into a proper single JSON Object
    // - "ID": field will be included, and Method as "MethodName": field
    function SaveInputAsObject(Service: TInterfaceFactory;
      const MethodName: RawUTF8 = 'Method'; IDAsHexa: boolean = false): variant; virtual;
    /// run FillOne and SaveInputAsObject into a TDocVariant array of JSON Objects
    // - "ID": field will be included, and Method as "MethodName": field
    procedure SaveFillInputsAsObjects(Service: TInterfaceFactory; out Dest: TDocVariantData;
      const MethodName: RawUTF8 = 'Method'; IDAsHexa: boolean = false);
  published
    /// when this notification has been sent
    // - equals 0 until it was actually notified
    property Sent: TTimeLog read fSent write fSent;
  end;

  TServiceMethodExecute = class;

  /// the current step of a TServiceMethodExecute.OnExecute call
  TServiceMethodExecuteEventStep = (smsUndefined, smsBefore, smsAfter, smsError);

  /// the TServiceMethodExecute.OnExecute signature
  TServiceMethodExecuteEvent = procedure(Sender: TServiceMethodExecute;
    Step: TServiceMethodExecuteEventStep) of object;

  /// execute a method of a TInterfacedObject instance, from/to JSON
  TServiceMethodExecute = class
  protected
    fMethod: PServiceMethod;
    fRawUTF8s: TRawUTF8DynArray;
    fStrings: TStringDynArray;
    fWideStrings: TWideStringDynArray;
    fRecords: array of TBytes;
    fInt64s: TInt64DynArray;
    fObjects: TObjectDynArray;
    fInterfaces: TPointerDynArray;
    fDynArrays: array of record
      Value: Pointer;
      Wrapper: TDynArray;
    end;
    fValues: TPPointerDynArray;
    fAlreadyExecuted: boolean;
    fTempTextWriter: TJSONSerializer;
    fOnExecute: array of TServiceMethodExecuteEvent;
    fBackgroundExecutionThread: TSynBackgroundThreadMethod;
    fOnCallback: TServiceMethodExecuteCallback;
    fOptions: TServiceMethodOptions;
    fServiceCustomAnswerHead: RawUTF8;
    fServiceCustomAnswerStatus: cardinal;
    fLastException: Exception;
    fInput: TDocVariantData;
    fOutput: TDocVariantData;
    fCurrentStep: TServiceMethodExecuteEventStep;
    fExecutedInstancesFailed: TRawUTF8DynArray;
    procedure BeforeExecute;
    procedure RawExecute(const Instances: PPointerArray; InstancesLast: integer); virtual;
    procedure AfterExecute;
  public
    /// initialize the execution instance
    constructor Create(aMethod: PServiceMethod);
    /// finalize the execution instance
    destructor Destroy; override;
    /// allow to hook method execution
    // - if optInterceptInputOutput is defined in Options, then Sender.Input/Output
    // fields will contain the execution data context when Hook is called
    procedure AddInterceptor(const Hook: TServiceMethodExecuteEvent);
    /// execute the corresponding method of weak IInvokable references
    // - will retrieve a JSON array of parameters from Par (as [1,"par2",3])
    // - will append a JSON array of results in Res, or set an Error message, or
    // a JSON object (with parameter names) in Res if ResultAsJSONObject is set
    // - if one Instances[] is supplied, any exception will be propagated (unless
    // optIgnoreException is set); if more than one Instances[] is supplied,
    // corresponding ExecutedInstancesFailed[] property will be filled with
    // the JSON serialized exception
    function ExecuteJson(const Instances: array of pointer; Par: PUTF8Char;
      Res: TTextWriter; Error: PShortString=nil; ResAsJSONObject: boolean=false): boolean;
    /// execute the corresponding method of one weak IInvokable reference
    // - exepect no output argument, i.e. no returned data, unless output is set
    // - this version will identify TInterfacedObjectFake implementations,
    // and will call directly fInvoke() if possible, to avoid JSON marshalling
    // - expect params value to be without [ ], just like TOnFakeInstanceInvoke
    function ExecuteJsonCallback(Instance: pointer; const params: RawUTF8;
      output: PRawUTF8): boolean;
    /// execute directly TInterfacedObjectFake.fInvoke()
    // - expect params value to be with [ ], just like ExecuteJson
    function ExecuteJsonFake(Instance: pointer; params: PUTF8Char): boolean;
    /// low-level direct access to the associated method information
    property Method: PServiceMethod read fMethod;
    /// low-level direct access to the current input/output parameter values
    // - you should not need to access this, but rather set
    // optInterceptInputOutput in Options, and read Input/Output content
    property Values: TPPointerDynArray read fValues;
    /// associated settings, as copied from TServiceFactoryServer.Options
    property Options: TServiceMethodOptions read fOptions write fOptions;
    /// the current state of the execution
    property CurrentStep: TServiceMethodExecuteEventStep
      read fCurrentStep write fCurrentStep;
    /// set from output TServiceCustomAnswer.Header result parameter
    property ServiceCustomAnswerHead: RawUTF8
      read fServiceCustomAnswerHead write fServiceCustomAnswerHead;
    /// set from output TServiceCustomAnswer.Status result parameter
    property ServiceCustomAnswerStatus: cardinal
      read fServiceCustomAnswerStatus write fServiceCustomAnswerStatus;
    /// set if optInterceptInputOutput is defined in TServiceFactoryServer.Options
    // - contains a dvObject with input parameters as "argname":value pairs
    // - this is a read-only property: you cannot change the input content
    property Input: TDocVariantData read fInput;
    /// set if optInterceptInputOutput is defined in TServiceFactoryServer.Options
    // - contains a dvObject with output parameters as "argname":value pairs
    // - this is a read-only property: you cannot change the output content
    property Output: TDocVariantData read fOutput;
    /// only set during AddInterceptor() callback execution, if Step is smsError
    property LastException: Exception read fLastException;
    /// reference to the background execution thread, if any
    property BackgroundExecutionThread: TSynBackgroundThreadMethod
      read fBackgroundExecutionThread;
    /// points e.g. to TSQLRestServerURIContext.ExecuteCallback
    property OnCallback: TServiceMethodExecuteCallback read fOnCallback;
    /// contains exception serialization after ExecuteJson of multiple instances
    // - follows the Instances[] order as supplied to RawExecute/ExecuteJson
    // - if only a single Instances[] is supplied, the exception will be
    // propagated to the caller, unless optIgnoreException option is defined
    // - if more than one Instances[] is supplied, any raised Exception will
    // be serialized using ObjectToJSONDebug(), or this property will be left
    // to its default nil content if no exception occurred
    property ExecutedInstancesFailed: TRawUTF8DynArray read fExecutedInstancesFailed;
    /// allow to use an instance-specific temporary TJSONSerializer
    function TempTextWriter: TJSONSerializer;
  end;

  /// a record type to be used as result for a function method for custom content
  // for interface-based services
  // - all answers are pure JSON object by default: using this kind of record
  // as result will allow a response of any type (e.g. binary, HTML or text)
  // - this kind of answer will be understood by our TServiceContainerClient
  // implementation, and it may be used with plain AJAX or HTML requests
  // (via POST), to retrieve some custom content
  TServiceCustomAnswer = record
    /// mandatory response type, as encoded in the HTTP header
    // - useful to set the response mime-type - see e.g. JSON_CONTENT_TYPE_HEADER_VAR
    // TEXT_CONTENT_TYPE_HEADER or BINARY_CONTENT_TYPE_HEADER constants or
    // GetMimeContentType() function
    // - in order to be handled as expected, this field SHALL be set to NOT ''
    // (otherwise TServiceCustomAnswer will be transmitted as raw JSON)
    Header: RawUTF8;
    /// the response body
    // - corresponding to the response type, as defined in Header
    Content: RawByteString;
    /// the HTTP response code
    // - if not overriden, will default to HTTP_SUCCESS = 200 on server side
    // - on client side, will always contain HTTP_SUCCESS = 200 on success,
    // or any error should be handled as expected by the caller (e.g. using
    // TServiceFactoryClient.GetErrorMessage for decoding REST/SOA errors)
    Status: cardinal;
  end;

  PServiceCustomAnswer = ^TServiceCustomAnswer;

  {$M+}
  /// abstract factory class allowing to call interface resolution in cascade
  // - you can inherit from this class to chain the TryResolve() calls so
  // that several kind of implementations may be asked by a TInjectableObject,
  // e.g. TInterfaceStub, TServiceContainer or TDDDRepositoryRestObjectMapping
  // - this will implement factory pattern, as a safe and thread-safe DI/IoC
  TInterfaceResolver = class
  protected
    /// override this method to resolve an interface from this instance
    function TryResolve(aInterface: PTypeInfo; out Obj): boolean; virtual; abstract;
    /// override this method check if this instance implements aInterface
    function Implements(aInterface: PTypeInfo): boolean; virtual; abstract;
  end;
  {$M-}

  /// abstract factory class targetting a single kind of interface
  TInterfaceResolverForSingleInterface = class(TInterfaceResolver)
  protected
    fInterfaceTypeInfo: PTypeInfo;
    fInterfaceAncestors: PTypeInfoDynArray;
    fInterfaceAncestorsImplementationEntry: TPointerDynArray;
    fImplementationEntry: PInterfaceEntry;
    fImplementation: TClassInstance;
    function TryResolve(aInterface: PTypeInfo; out Obj): boolean; override;
    function Implements(aInterface: PTypeInfo): boolean; override;
    function GetImplementationName: string;
    // main IoC/DI virtual method - call fImplementation.CreateNew by default
    function CreateInstance: TInterfacedObject; virtual;
  public
    /// this overriden constructor will check and store the supplied class
    // to implement an interface
    constructor Create(aInterface: PTypeInfo; aImplementation: TInterfacedObjectClass); overload;
    /// this overriden constructor will check and store the supplied class
    // to implement an interface by TGUID
    constructor Create(const aInterface: TGUID; aImplementation: TInterfacedObjectClass); overload;
    /// you can use this method to resolve the interface as a new instance
    function GetOneInstance(out Obj): boolean;
  published
    /// the class name which will implement each repository instance
    property ImplementationClass: string read GetImplementationName;
  end;

  TInterfaceStub = class;

  /// used to store a list of TInterfacedObject instances
  TInterfacedObjectObjArray = array of TInterfacedObject;

  /// used to store a list of TInterfaceResolver instances
  TInterfaceResolverObjArray = array of TInterfaceResolver;

  /// used to store a list of TInterfaceStub instances
  TInterfaceStubObjArray = array of TInterfaceStub;

  /// abstract factory class targetting any kind of interface
  // - you can inherit from this class to customize dependency injection (DI/IoC),
  // defining the resolution via InjectStub/InjectResolver/InjectInstance methods,
  // and doing the instance resolution using the overloaded Resolve*() methods
  // - TServiceContainer will inherit from this class, as the main entry point
  // for interface-based services of the framework (via TSQLRest.Services)
  // - you can use RegisterGlobal() class method to define some process-wide DI
  TInterfaceResolverInjected = class(TInterfaceResolver)
  protected
    fResolvers: TInterfaceResolverObjArray;
    fResolversToBeReleased: TInterfaceResolverObjArray;
    fDependencies: TInterfacedObjectObjArray;
    function TryResolve(aInterface: PTypeInfo; out Obj): boolean; override;
    function TryResolveInternal(aInterface: PTypeInfo; out Obj): boolean;
    function Implements(aInterface: PTypeInfo): boolean; override;
    class function RegisterGlobalCheck(aInterface: PTypeInfo;
      aImplementationClass: TClass): PInterfaceEntry;
  public
    /// define a global class type for interface resolution
    // - most of the time, you will need a local DI/IoC resolution list; but
    // you may use this method to register a set of shared and global resolution
    // patterns, common to the whole injection process
    // - by default, TAutoLocker and TLockedDocVariant will be registered by
    // this unit to implement IAutoLocker and ILockedDocVariant interfaces
    class procedure RegisterGlobal(aInterface: PTypeInfo;
      aImplementationClass: TInterfacedObjectClass); overload;
    /// define a global instance for interface resolution
    // - most of the time, you will need a local DI/IoC resolution list; but
    // you may use this method to register a set of shared and global resolution
    // patterns, common to the whole injection process
    // - the supplied instance will be owned by the global list (incrementing
    // its internal reference count), until it will be released via
    // ! RegisterGlobalDelete()
    // - the supplied instance will be freed in the finalization of this unit,
    // if not previously released via RegisterGlobalDelete()
    class procedure RegisterGlobal(aInterface: PTypeInfo;
      aImplementation: TInterfacedObject); overload;
    /// undefine a global instance for interface resolution
    // - you can unregister a given instance previously defined via
    // ! RegisterGlobal(aInterface,aImplementation)
    // - if you do not call RegisterGlobalDelete(), the remaning instances will
    // be freed in the finalization of this unit
    class procedure RegisterGlobalDelete(aInterface: PTypeInfo);
    /// prepare and setup interface DI/IoC resolution with some blank
    // TInterfaceStub specified by their TGUID
    procedure InjectStub(const aStubsByGUID: array of TGUID); overload; virtual;
    /// prepare and setup interface DI/IoC resolution with TInterfaceResolver
    // kind of factory
    // - e.g. a customized TInterfaceStub/TInterfaceMock, a TServiceContainer,
    // a TDDDRepositoryRestObjectMapping or any factory class
    // - by default, only TInterfaceStub/TInterfaceMock will be owned by this
    // instance, and released by Destroy - unless you set OwnOtherResolvers
    procedure InjectResolver(const aOtherResolvers: array of TInterfaceResolver;
      OwnOtherResolvers: boolean=false); overload; virtual;
    /// prepare and setup interface DI/IoC resolution from a TInterfacedObject instance
    // - any TInterfacedObject declared as dependency will have its reference
    // count increased, and decreased in Destroy
    procedure InjectInstance(const aDependencies: array of TInterfacedObject); overload; virtual;
    /// can be used to perform an DI/IoC for a given interface
    // - will search for the supplied interface to its internal list of resolvers
    // - returns TRUE and set the Obj variable with a matching instance
    // - can be used as such to resolve an ICalculator interface:
    // ! var calc: ICalculator;
    // ! begin
    // !   if Catalog.Resolve(TypeInfo(ICalculator),calc) then
    // !   ... use calc methods
    function Resolve(aInterface: PTypeInfo; out Obj): boolean; overload;
    /// can be used to perform an DI/IoC for a given interface
    // - you shall have registered the interface TGUID by a previous call to
    // ! TInterfaceFactory.RegisterInterfaces([TypeInfo(ICalculator),...])
    // - returns TRUE and set the Obj variable with a matching instance
    // - can be used as such to resolve an ICalculator interface:
    // ! var calc: ICalculator;
    // ! begin
    // !   if ServiceContainer.Resolve(ICalculator,cal) then
    // !   ... use calc methods
    function Resolve(const aGUID: TGUID; out Obj): boolean; overload;
    /// can be used to perform several DI/IoC for a given set of interfaces
    // - here interfaces and instances are provided as TypeInfo,@Instance pairs
    // - raise an EServiceException if any interface can't be resolved, unless
    // aRaiseExceptionIfNotFound is set to FALSE
    procedure ResolveByPair(const aInterfaceObjPairs: array of pointer;
      aRaiseExceptionIfNotFound: boolean=true);
    /// can be used to perform several DI/IoC for a given set of interfaces
    // - here interfaces and instances are provided as TGUID and @Instance
    // - you shall have registered the interface TGUID by a previous call to
    // ! TInterfaceFactory.RegisterInterfaces([TypeInfo(ICalculator),...])
    // - raise an EServiceException if any interface can't be resolved, unless
    // aRaiseExceptionIfNotFound is set to FALSE
    procedure Resolve(const aInterfaces: array of TGUID; const aObjs: array of pointer;
      aRaiseExceptionIfNotFound: boolean=true); overload;
    /// release all used instances
    // - including all TInterfaceStub instances as specified to Inject(aStubsByGUID)
    // - will call _Release on all TInterfacedObject dependencies
    destructor Destroy; override;
  end;

  /// any service implementation class could inherit from this class to
  // allow dependency injection aka SOLID DI/IoC by the framework
  // - once created, the framework will call AddResolver() member, so that its
  // Resolve*() methods could be used to inject any needed dependency for lazy
  // dependency resolution (e.g. within a public property getter)
  // - any interface published property will also be automatically injected
  // - if you implement a SOA service with this class, TSQLRestServer.Services
  // will be auto-injected via TServiceFactoryServer.CreateInstance()
  TInjectableObject = class(TInterfacedObjectWithCustomCreate)
  protected
    fResolver: TInterfaceResolver;
    fResolverOwned: Boolean;
    // DI/IoC resolution protected methods
    function TryResolve(aInterface: PTypeInfo; out Obj): boolean;
    /// this method will resolve all interface published properties
    procedure AutoResolve(aRaiseEServiceExceptionIfNotFound: boolean);
  public
    /// initialize an instance, defining one or several mean of dependency resolution
    // - simple TInterfaceStub could be created directly from their TGUID,
    // then any kind of DI/IoC resolver instances could be specified, i.e.
    // either customized TInterfaceStub/TInterfaceMock, a TServiceContainer or
    // a TDDDRepositoryRestObjectMapping, and then any TInterfacedObject
    // instance will be used during dependency resolution:
    // ! procedure TMyTestCase.OneTestCaseMethod;
    // ! var Test: IServiceToBeTested;
    // ! begin
    // !   Test := TServiceToBeTested.CreateInjected(
    // !     [ICalculator],
    // !     [TInterfaceMock.Create(IPersistence,self).
    // !       ExpectsCount('SaveItem',qoEqualTo,1),
    // !      RestInstance.Services],
    // !     [AnyInterfacedObject]);
    // !   ...
    // - note that all the injected stubs/mocks instances will be owned by the
    // TInjectableObject, and therefore released with it
    // - any TInterfacedObject declared as dependency will have its reference
    // count increased, and decreased in Destroy
    // - once DI/IoC is defined, will call the AutoResolve() protected method
    constructor CreateInjected(const aStubsByGUID: array of TGUID;
      const aOtherResolvers: array of TInterfaceResolver;
      const aDependencies: array of TInterfacedObject;
      aRaiseEServiceExceptionIfNotFound: boolean=true); virtual;
    /// initialize an instance, defining one dependency resolver
    // - the resolver may be e.g. a TServiceContainer
    // - once the DI/IoC is defined, will call the AutoResolve() protected method
    // - as called by  TServiceFactoryServer.CreateInstance
    constructor CreateWithResolver(aResolver: TInterfaceResolver;
      aRaiseEServiceExceptionIfNotFound: boolean=true); virtual;
    /// can be used to perform an DI/IoC for a given interface type information
    procedure Resolve(aInterface: PTypeInfo; out Obj); overload;
    /// can be used to perform an DI/IoC for a given interface TGUID
    procedure Resolve(const aGUID: TGUID; out Obj); overload;
    /// can be used to perform several DI/IoC for a given set of interfaces
    // - here interfaces and instances are provided as TypeInfo,@Instance pairs
    procedure ResolveByPair(const aInterfaceObjPairs: array of pointer);
    /// can be used to perform several DI/IoC for a given set of interfaces
    // - here interfaces and instances are provided as TGUID and pointers
    procedure Resolve(const aInterfaces: array of TGUID; const aObjs: array of pointer); overload;
    /// release all used instances
    // - including all TInterfaceStub instances as specified to CreateInjected()
    destructor Destroy; override;
    /// access to the associated dependency resolver, if any
    property Resolver: TInterfaceResolver read fResolver;
  end;

  /// class-reference type (metaclass) of a TInjectableObject type
  TInjectableObjectClass = class of TInjectableObject;

  /// service implementation class, with direct access on the associated
  // TServiceFactoryServer/TSQLRestServer instances
  // - allow dependency injection aka SOLID DI/IoC by the framework using
  // inherited TInjectableObject.Resolve() methods
  // - allows direct access to the underlying ORM using its Server method
  // - this class will allow Server instance access outside the scope of
  // remote SOA execution, e.g. when a DI is performed on server side: it
  // is therefore a better alternative to ServiceContext.Factory,
  // ServiceContext.Factory.RestServer or ServiceContext.Request.Server
  TInjectableObjectRest = class(TInjectableObject)
  protected
    fFactory: TServiceFactoryServer;
    fServer: TSQLRestServer;
  public
    /// initialize an instance, defining associated dependencies
    // - the resolver may be e.g. a TServiceContainer
    // - once the DI/IoC is defined, will call the AutoResolve() protected method
    // - as called by  TServiceFactoryServer.CreateInstance
    constructor CreateWithResolverAndRest(aResolver: TInterfaceResolver;
      aFactory: TServiceFactoryServer; aServer: TSQLRestServer;
      aRaiseEServiceExceptionIfNotFound: boolean=true); virtual;
    /// access to the associated interface factory
    // - this property will be injected by TServiceFactoryServer.CreateInstance,
    // so may be nil if the instance was created outside the SOA context
    property Factory: TServiceFactoryServer read fFactory;
    /// access ot the associated REST Server, e.g. to its ORM methods
    // - slightly faster than Factory.RestServer
    // - this value will be injected by TServiceFactoryServer.CreateInstance,
    // so may be nil if the instance was created outside the SOA context
    property Server: TSQLRestServer read fServer;
  end;

  /// class-reference type (metaclass) of a TInjectableObjectRest type
  TInjectableObjectRestClass = class of TInjectableObjectRest;

  /// used to set the published properties of a TInjectableAutoCreateFields
  // - TInjectableAutoCreateFields.Create will check any resolver able to
  // implement this interface, then run its SetProperties() method on it
  IAutoCreateFieldsResolve = interface
    ['{396362E9-B60D-43D4-A0D4-802E4479F24E}']
    /// this method will be called once on any TInjectableAutoCreateFields just
    // created instance
    procedure SetProperties(Instance: TObject);
  end;

  /// abstract class which will auto-inject its dependencies (DI/IoC), and also
  // manage the instances of its TPersistent/TSynPersistent published properties
  // - abstract class able with a virtual constructor, dependency injection
  // (i.e. SOLID DI/IoC), and automatic memory management of all nested class
  // published properties
  // - will also release any T*ObjArray dynamic array storage of persistents,
  // previously registered via TJSONSerializer.RegisterObjArrayForJSON()
  // - this class is a perfect parent for any class storing data by value, and
  // dependency injection, e.g. DDD services or daemons
  // - note that non published (e.g. public) properties won't be instantiated
  // - 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 matching its parent type
  // - since the destructor will release all nested properties, you should
  // never store a reference of any of those nested instances outside
  // - if any associated resolver implements IAutoCreateFieldsResolve, its
  // SetProperties() method will be called on all created T*Persistent
  // published properties, so that it may initialize its values
  TInjectableAutoCreateFields = class(TInjectableObject)
  public
    /// this overriden constructor will instantiate all its nested
    // TPersistent/TSynPersistent/TSynAutoCreateFields class published properties
    // - then resolve and call IAutoCreateFieldsResolve.SetProperties(self)
    constructor Create; override;
    /// finalize the instance, and release its published properties
    destructor Destroy; override;
  end;

  /// event used by TInterfaceFactory to run a method from a fake instance
  // - aMethod will specify which method is to be executed
  // - aParams will contain the input parameters, encoded as a JSON array,
  // without the [ ] characters (e.g. '1,"arg2",3')
  // - shall return TRUE on success, or FALSE in case of failure, with
  // a corresponding explanation in aErrorMsg
  // - method results shall be serialized as JSON in aResult;  if
  // aServiceCustomAnswer is not nil, the result shall use this record
  // to set HTTP custom content and headers, and ignore aResult content
  // - aClientDrivenID can be set optionally to specify e.g. an URI-level session
  TOnFakeInstanceInvoke = function(const aMethod: TServiceMethod;
    const aParams: RawUTF8; aResult, aErrorMsg: PRawUTF8;
    aClientDrivenID: PCardinal; aServiceCustomAnswer: PServiceCustomAnswer): boolean of object;

  /// event called when destroying a TInterfaceFactory's fake instance
  /// - this method will be run when the fake class instance is destroyed
  // (e.g. if aInstanceCreation is sicClientDriven, to notify the server
  // than the client life time just finished)
  TOnFakeInstanceDestroy = procedure(aClientDrivenID: cardinal) of object;

  /// may be used to store the Methods[] indexes of a TInterfaceFactory
  // - current implementation handles up to 128 methods, a limit above
  // which "Interface Segregation" principles is obviously broken
  TInterfaceFactoryMethodBits = set of 0..MAX_METHOD_COUNT-1;

  /// a dynamic array of TInterfaceFactory instances
  TInterfaceFactoryObjArray = array of TInterfaceFactory;

  /// class handling interface RTTI and fake implementation class
  // - a thread-safe global list of such class instances is implemented to cache
  // information for better speed: use class function TInterfaceFactory.Get()
  // and not manual TInterfaceFactory.Create / Free
  // - if you want to search the interfaces by name or TGUID, call once
  // Get(TypeInfo(IMyInterface)) or RegisterInterfaces() for proper registration
  // - will use TInterfaceFactoryRTTI classes generated from Delphi RTTI
  TInterfaceFactory = class
  protected
    fInterfaceTypeInfo: PTypeInfo;
    fInterfaceIID: TGUID;
    fMethodsCount: cardinal;
    fAddMethodsLevel: integer;
    fMethods: TServiceMethodDynArray;
    fMethod: TDynArrayHashed;
    // contains e.g. [{"method":"Add","arguments":[...]},{"method":"...}]
    fContract: RawUTF8;
    fInterfaceName: RawUTF8;
    fInterfaceURI: RawUTF8;
    {$ifndef NOVARIANTS}
    fDocVariantOptions: TDocVariantOptions;
    {$endif}
    fFakeVTable: array of pointer;
    fFakeStub: PByteArray;
    fMethodIndexCallbackReleased: Integer;
    fMethodIndexCurrentFrameCallback: Integer;
    procedure AddMethodsFromTypeInfo(aInterface: PTypeInfo); virtual; abstract;
    function GetMethodsVirtualTable: pointer;
  public
    /// this is the main entry point to the global interface factory cache
    // - access to this method is thread-safe
    // - this method will also register the class to further retrieval
    class function Get(aInterface: PTypeInfo): TInterfaceFactory; overload;
    /// retrieve an interface factory from cache, from its TGUID
    // - access to this method is thread-safe
    // - you shall have registered the interface by a previous call to the
    // overloaded Get(TypeInfo(IMyInterface)) method or RegisterInterfaces()
    // - if the supplied TGUID has not been previously registered, returns nil
    class function Get(const aGUID: TGUID): TInterfaceFactory; overload;
    /// retrieve an interface factory from cache, from its name (e.g. 'IMyInterface')
    // - access to this method is thread-safe
    // - you shall have registered the interface by a previous call to the
    // overloaded Get(TypeInfo(IMyInterface)) method or RegisterInterfaces()
    // - if the supplied TGUID has not been previously registered, returns nil
    class function Get(const aInterfaceName: RawUTF8): TInterfaceFactory; overload;
    /// register one or several interfaces to the global interface factory cache
    // - so that you can use TInterfaceFactory.Get(aGUID) or Get(aName)
    class procedure RegisterInterfaces(const aInterfaces: array of PTypeInfo);
    /// could be used to retrieve an array of TypeInfo() from their GUID
    class function GUID2TypeInfo(const aGUIDs: array of TGUID): PTypeInfoDynArray; overload;
    /// could be used to retrieve an array of TypeInfo() from their GUID
    class function GUID2TypeInfo(const aGUID: TGUID): PTypeInfo; overload;
    /// returns the list of all declared TInterfaceFactory
    // - as used by SOA and mocking/stubing features of this unit
    class function GetUsedInterfaces: TSynObjectListLocked;
    /// add some TInterfaceFactory instances from their GUID
    class procedure AddToObjArray(var Obj: TInterfaceFactoryObjArray;
       const aGUIDs: array of TGUID);
    /// register some TypeInfo() containing unsafe parameter values
    // - i.e. any RTTI type containing Sensitive Personal Information, e.g.
    // a bank card number or a plain password
    // - such values will force associated values to be ignored during loging,
    // as a more tuned alternative to optNoLogInput or optNoLogOutput
    class procedure RegisterUnsafeSPIType(const Types: array of pointer);

    /// initialize the internal properties from the supplied interface RTTI
    // - it will check and retrieve all methods of the supplied interface,
    // and prepare all internal structures for later use
    // - do not call this constructor directly, but TInterfaceFactory.Get()
    constructor Create(aInterface: PTypeInfo);
    /// find the index of a particular method in internal Methods[] list
    // - will search for a match against Methods[].URI property
    // - won't find the default AddRef/Release/QueryInterface methods
    // - will return -1 if the method is not known
    // - if aMethodName does not have an exact method match, it will try with a
    // trailing underscore, so that e.g. /service/start will match IService._Start()
    function FindMethodIndex(const aMethodName: RawUTF8): integer;
    /// find a particular method in internal Methods[] list
    // - just a wrapper around FindMethodIndex() returing a PServiceMethod
    // - will return nil if the method is not known
    function FindMethod(const aMethodName: RawUTF8): PServiceMethod;
    /// find the index of a particular interface.method in internal Methods[] list
    // - will search for a match against Methods[].InterfaceDotMethodName property
    // - won't find the default AddRef/Release/QueryInterface methods
    // - will return -1 if the method is not known
    function FindFullMethodIndex(const aFullMethodName: RawUTF8;
      alsoSearchExactMethodName: boolean=false): integer;
    /// find the index of a particular method in internal Methods[] list
    // - won't find the default AddRef/Release/QueryInterface methods
    // - will raise an EInterfaceFactoryException if the method is not known
    function CheckMethodIndex(const aMethodName: RawUTF8): integer; overload;
    /// find the index of a particular method in internal Methods[] list
    // - won't find the default AddRef/Release/QueryInterface methods
    // - will raise an EInterfaceFactoryException if the method is not known
    function CheckMethodIndex(aMethodName: PUTF8Char): integer; overload;
    /// returns the method name from its method index
    // - the method index should start at 0 for _free_/_contract_/_signature_
    // pseudo-methods, and start at index 3 for real Methods[]
    function GetMethodName(MethodIndex: integer): RawUTF8;
    /// set the Methods[] indexes bit from some methods names
    // - won't find the default AddRef/Release/QueryInterface methods
    // - will raise an EInterfaceFactoryException if the method is not known
    procedure CheckMethodIndexes(const aMethodName: array of RawUTF8; aSetAllIfNone: boolean;
      out aBits: TInterfaceFactoryMethodBits);
    /// returns the full 'Interface.MethodName' text, from a method index
    // - the method index should start at 0 for _free_/_contract_/_signature_
    // pseudo-methods, and start at index 3 for real Methods[]
    // - will return plain 'Interface' text, if aMethodIndex is incorrect
    function GetFullMethodName(aMethodIndex: integer): RawUTF8;
    /// the declared internal methods
    // - list does not contain default AddRef/Release/QueryInterface methods
    // - nor the _free_/_contract_/_signature_ pseudo-methods
    property Methods: TServiceMethodDynArray read fMethods;
    /// the number of internal methods
    // - does not include the default AddRef/Release/QueryInterface methods
    // - nor the _free_/_contract_/_signature_ pseudo-methods
    property MethodsCount: cardinal read fMethodsCount;
    /// identifies a CallbackReleased() method in this interface
    // - i.e. the index in Methods[] of the following signature:
    // ! procedure CallbackReleased(const callback: IInvokable; const interfaceName: RawUTF8);
    // - this method will be called e.g. by TInterfacedCallback.Destroy, when
    // a callback is released on the client side so that you may be able e.g. to
    // unsubscribe the callback from an interface list (via InterfaceArrayDelete)
    // - contains -1 if no such method do exist in the interface definition
    property MethodIndexCallbackReleased: Integer read fMethodIndexCallbackReleased;
    /// identifies a CurrentFrame() method in this interface
    // - i.e. the index in Methods[] of the following signature:
    // ! procedure CurrentFrame(isLast: boolean);
    // - this method will be called e.g. by TSQLHttpClientWebsockets.CallbackRequest
    // for interface callbacks in case of WebSockets jumbo frames, to allow e.g.
    // faster database access via a batch
    // - contains -1 if no such method do exist in the interface definition
    property MethodIndexCurrentFrameCallback: Integer read fMethodIndexCurrentFrameCallback;
    /// the registered Interface low-level Delphi RTTI type
    property InterfaceTypeInfo: PTypeInfo read fInterfaceTypeInfo;
    /// the registered Interface GUID
    property InterfaceIID: TGUID read fInterfaceIID;
    /// the interface name, without its initial 'I'
    // - e.g. ICalculator -> '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 oldest FPC, which did 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 <content ...> ...
    // </content> 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, owning 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;
    fServicesRouting: TSQLRestServerURIContextClass;
    fBackgroundTimer: TSQLRestBackgroundTimer;
    fOnDecryptBody, fOnEncryptBody: TNotifyRestBody;
    fCustomEncryptAES: TAESAbstract;
    fCustomEncryptSign: TSynSigner;
    fCustomEncryptCompress: TAlgoCompress;
    fCustomEncryptContentPrefix, fCustomEncryptContentPrefixUpper, fCustomEncryptUrlIgnore: RawUTF8;
    fAcquireExecution: array[TSQLRestServerURIContextCommand] of TSQLRestAcquireExecution;
    fPrivateGarbageCollector: TSynObjectList;
    {$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: IInterface>: 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<T> 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<TSQLRecordTest>;
    // !    R: TSQLRecordTest;
    // ! ...
    // !    List := Client.RetrieveList<TSQLRecordTest>('ID,Test');
    // !    if List<>nil then
    // !    try
    // !      for R in List do
    // !        writeln(R.ID,'=',R.Test);
    // !    finally
    // !      List.Free;
    // !    end;
    function RetrieveList<T: TSQLRecord>(const aCustomFieldsCSV: RawUTF8=''): TObjectList<T>; 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<T> on success (possibly with Count=0) - caller is
    // responsible of freeing the instance
    // - return nil on error
    function RetrieveList<T: TSQLRecord>(const FormatSQLWhere: RawUTF8;
      const BoundsSQLWhere: array of const;
      const aCustomFieldsCSV: RawUTF8=''): TObjectList<T>; overload;
    {$endif ISDELPHI2010}

    /// 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 fServicesRouting 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
  // - rsoNoTableURI will disable any /root/tablename URI for safety
  // - rsoMethodUnderscoreAsSlashURI will try to decode /root/method/name
  // as 'method_name' method
  TSQLRestServerOption = (
    rsoNoAJAXJSON,
    rsoGetAsJsonNotAsString,
    rsoGetID_str,
    rsoRedirectForbiddenToAuth,
    rsoHttp200WithNoBodyReturns204,
    rsoAddUpdateReturnsContent,
    rsoComputeFieldsBeforeWriteOnServerSide,
    rsoSecureConnectionRequired,
    rsoCookieIncludeRootPath,
    rsoCookieHttpOnlyFlagDisable,
    rsoAuthenticationURIDisable,
    rsoTimestampInfoURIDisable,
    rsoHttpHeaderCheckDisable,
    rsoGetUserRetrieveNoBlobData,
    rsoNoInternalState,
    rsoNoTableURI,
    rsoMethodUnderscoreAsSlashURI);
  /// 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);
    /// SessionAccess will detect and delete outdated sessions, but you can call
    // this method to force checking for deprecated session now
    // - may be used e.g. from OnSessionCreate to limit the number of active sessions
    // - this method is not thread-safe: caller should use Sessions.Lock/Unlock
    // - you can call it often: it will seek for outdated sessions once per second
    // - returns the current system Ticks number (at second resolution)
    function SessionDeleteDeprecated: cardinal;
    /// 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. corresponding to aInterfaces[0] - not to the others),
    // 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 for aInterfaces[0] - warning: only the
    // the first interface options are returned
    // - 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)
    // - will return the first of the registered TServiceFactoryServer created
    // on success (i.e. corresponding to aInterfaces[0] - not to the others),
    // 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 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),...]);
    // - will return the first of the registered TServiceFactoryServer created
    // on success (i.e. corresponding to aInterfaces[0] - not to the others),
    // or nil if registration failed (e.g. if any of the supplied interfaces
    // is not implemented by the given class)
    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
    // - will return the first of the registered TServiceFactoryServer created
    // on success (i.e. corresponding to aInterfaces[0] - not to the others),
    // or nil if registration failed (e.g. if any of the supplied interfaces
    // is not implemented by the given class)
    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 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; const FormatMsg: RawUTF8;
      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=CONST_AUTHENTICATION_NOT_USED): 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 after "408 REQUEST TIMEOUT" server error
    // - this is about an HTTP error 408 returned by the server, e.g. when the
    // ORM lock or transaction could not be acquired in a good enough time: this
    // value does not apply to the client side timeout, e.g. at HTTP level
    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 UNICODE}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
    raise 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 := (temp='0') or (temp='false') or (temp='null');
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
    raise 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 L<R then
  repeat
    I := L; J := R;
    P := (L+R) shr 1;
    repeat
      pivot := pointer(fList[fOrderedByName[P]].fName);
      while StrIComp(pointer(fList[fOrderedByName[I]].fName),pivot)<0 do inc(I);
      while StrIComp(pointer(fList[fOrderedByName[J]].fName),pivot)>0 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 (Code<HTTP_BADREQUEST); // 200..399
end;

function AuthURI(const URI, URIAuthenticationBearer: RawUTF8): RawUTF8;
begin
  if URIAuthenticationBearer='' then
    result := URI else
    if PosExChar('?',URI)=0 then
      result := URI+'?authenticationbearer='+URIAuthenticationBearer else
      result := URI+'&authenticationbearer='+URIAuthenticationBearer
end;

function ToMethod(const method: RawUTF8): TSQLURIMethod;
const NAME: array[mGET..high(TSQLURIMethod)] of string[10] = ( // sorted by occurence
  'GET','POST','PUT','DELETE','HEAD','BEGIN','END','ABORT','LOCK','UNLOCK','STATE',
  'OPTIONS','PROPFIND','PROPPATCH','TRACE','COPY','MKCOL','MOVE','PURGE','REPORT',
  'MKACTIVITY','MKCALENDAR','CHECKOUT','MERGE','NOTIFY','PATCH','SEARCH','CONNECT');
var L: PtrInt;
    N: PShortString;
begin
  L := Length(method);
  if L<11 then begin
    N := @NAME;
    for result := low(NAME) to high(NAME) do
      if (L=ord(N^[0])) and IdemPropNameUSameLen(@N^[1],pointer(method),L) then
        exit else
        inc(PByte(N),11);
  end;
  result := mNone;
end;

function IsInvalidHttpHeader(head: PUTF8Char; headlen: PtrInt): boolean;
var i: PtrInt;
begin
  result := true;
  for i := 0 to headlen-3 do
    if (PInteger(head+i)^=$0a0d0a0d) or
       (PWord(head+i)^=$0d0d) or (PWord(head+i)^=$0a0a) then
      exit;
  result := false;
end;


{ ******************* process monitoring / statistics }

{ TSynMonitorUsage }

function MonitorPropUsageValue(info: PPropInfo): TSynMonitorType;
var typ: pointer;
begin
  typ := info^.TypeInfo;
  if typ=TypeInfo(TSynMonitorTotalMicroSec) then
    result := smvMicroSec else
  if typ=TypeInfo(TSynMonitorOneMicroSec) then
    result := smvOneMicroSec else
  if typ=TypeInfo(TSynMonitorTotalBytes) then
    result := smvBytes else
  if typ=TypeInfo(TSynMonitorOneBytes) then
    result := smvOneBytes else
  if typ=TypeInfo(TSynMonitorBytesPerSec) then
    result := smvBytesPerSec else
  if typ=TypeInfo(TSynMonitorCount) then
    result := smvCount else
  if typ=TypeInfo(TSynMonitorCount64) then
    result := smvCount64 else
  if typ=TypeInfo(TSynMonitorOneCount) then
    result := smvOneCount else
    result := smvUndefined;
end;

function TSynMonitorUsage.Track(Instance: TObject; const Name: RawUTF8): integer;

  procedure ClassTrackProps(ClassType: TClass; var Props: TSynMonitorUsageTrackPropDynArray);
  var i,n: integer;
      nfo: PPropInfo;
      k: TSynMonitorType;
      g: TSynMonitorUsageGranularity;
      p: PSynMonitorUsageTrackProp;
      ctp: TClass;
  begin
    n := length(Props);
    while ClassType<>nil 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<low(fValues) then
    raise ESynException.CreateUTF8('%.Save(%) unexpected',[self,ToText(Gran)^]);
  TDocVariant.IsOfTypeOrNewFast(fValues[Gran]);
  for t := 0 to length(fTracked)-1 do begin
    track := @fTracked[t];
    n := length(track^.Props);
    data.InitFast(n,dvObject);
    for p := 0 to n-1 do
    with track^.Props[p] do
      if not IsZero(Values[Gran]) then begin
        // save non void values
        val.InitArrayFrom(Values[Gran],JSON_OPTIONS_FAST);
        data.AddValue(Name,Variant(val));
        val.Clear;
        // handle local cache
        if Kind in SYNMONITORVALUE_CUMULATIVE then begin
          if Gran<=Scope then // reset of cumulative values
            FillZero(Values[Gran]);
        end else begin
          if Gran<mugYear then // propagate instant values
            // e.g. Values[mugDay][hour] := Values[mugHour][minute] (=v)
            Values[succ(Gran)][ID.GetTime(Gran,true)] :=
              Values[Gran][ID.GetTime(pred(Gran),true)];
        end;
      end;
    _Safe(fValues[Gran]).AddOrUpdateValue(track^.Name,variant(data));
    data.Clear;
  end;
  _Safe(fValues[Gran]).SortByName;
  ID.Truncate(Gran);
  if not SaveDB(ID.Value,fValues[Gran],Gran) then
    fLog.SynLog.Log(sllWarning,'%.Save(ID=%=%,%) failed',
      [ClassType,ID.Value,ID.Text(true),ToText(Gran)^]);
end;

procedure TSynMonitorUsage.LoadTrack(var Track: TSynMonitorUsageTrack);
var p,v: Integer;
    g: TSynMonitorUsageGranularity;
    val,int: PDocVariantData;
begin // fValues[] variants -> 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 v<int^.Count then
              Values[g][v] := VariantToInt64Def(int^.Values[v],0);
        end;
    end;
end;

function TSynMonitorUsage.Load(const Time: TTimeLogBits): boolean;
var g: TSynMonitorUsageGranularity;
    id: TSynMonitorUsageID;
    t: integer;
begin
  // load fValues[] variants
  result := true;
  id.FromTimeLog(Time.Value);
  for g := low(fValues) to high(fValues) do begin
    id.Truncate(g);
    if not LoadDB(id.Value,g,fValues[g]) then
      result := false;
  end;
  // fill fTracked[].Props[].Values[]
  for t := 0 to length(fTracked)-1 do
    LoadTrack(fTracked[t]);
end;


{ TSynMonitorUsageID }

procedure TSynMonitorUsageID.From(Y, M, D, H: integer);
begin
  Value := H+(D-1) shl USAGE_ID_SHIFT[mugDay]+
    (M-1) shl USAGE_ID_SHIFT[mugMonth]+(Y-USAGE_ID_YEAROFFSET)shl USAGE_ID_SHIFT[mugYear];
end;

procedure TSynMonitorUsageID.From(Y, M, D: integer);
begin
  Value := USAGE_ID_HOURMARKER[mugDay]+(D-1) shl USAGE_ID_SHIFT[mugDay]+
    (M-1) shl USAGE_ID_SHIFT[mugMonth]+(Y-USAGE_ID_YEAROFFSET)shl USAGE_ID_SHIFT[mugYear];
end;

procedure TSynMonitorUsageID.From(Y, M: integer);
begin
  Value := USAGE_ID_HOURMARKER[mugMonth]+(M-1) shl USAGE_ID_SHIFT[mugMonth]+
    (Y-USAGE_ID_YEAROFFSET)shl USAGE_ID_SHIFT[mugYear];
end;

procedure TSynMonitorUsageID.From(Y: integer);
begin
  Value := USAGE_ID_HOURMARKER[mugYear]+(Y-USAGE_ID_YEAROFFSET)shl USAGE_ID_SHIFT[mugYear];
end;

procedure TSynMonitorUsageID.FromTimeLog(const TimeLog: TTimeLog);
var bits: TTimeLogBits absolute TimeLog;
begin
  Value := bits.Hour+(bits.Day-1) shl USAGE_ID_SHIFT[mugDay]+
    (bits.Month-1) shl USAGE_ID_SHIFT[mugMonth]+
    (bits.Year-USAGE_ID_YEAROFFSET)shl USAGE_ID_SHIFT[mugYear];
end;

procedure TSynMonitorUsageID.FromNowUTC;
var now: TTimeLogBits;
begin
  now.FromUTCTime;
  From(now.Value);
end;

function TSynMonitorUsageID.GetTime(gran: TSynMonitorUsageGranularity;
  monthdaystartat0: boolean): integer;
begin
  if not (gran in [low(USAGE_ID_SHIFT)..high(USAGE_ID_SHIFT)]) then
    result := 0 else begin
    result := (Value shr USAGE_ID_SHIFT[gran]) and USAGE_ID_MASK[gran];
    case gran of
    mugYear:
      inc(result,USAGE_ID_YEAROFFSET);
    mugDay, mugMonth:
      if not monthdaystartat0 then
        inc(result);
    mugHour:
      if cardinal(result)>USAGE_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 Row<fRowCount then
      MoveFast(fIDColumn[Row+1],fIDColumn[Row],(fRowCount-Row)*SizeOf(PUTF8Char));
  if Row<fRowCount then begin
    Row := Row*FieldCount; // convert row index into position in fResults[]
    MoveFast(fResults[Row+FieldCount],fResults[Row],(fRowCount*FieldCount-Row)*SizeOf(pointer));
  end;
  dec(fRowCount);
  result := true;
end;

procedure TSQLTable.InitFieldNames;
var f: integer;
    P: PUTF8Char;
begin
  SetLength(fFieldNames,fFieldCount); // share one TRawUTF8DynArray
  for f := 0 to fFieldCount-1 do begin
    P := Get(0,f);
    if IsRowID(P) then // normalize RowID field name to 'ID'
      fFieldNames[f] := 'ID' else
      FastSetString(fFieldNames[f],P,StrLen(P));
  end;
end;

{$ifndef NOVARIANTS}
var
  SQLTableRowVariantType: TCustomVariantType = nil;

procedure TSQLTable.GetAsVariant(row,field: integer; out value: variant;
  expandTimeLogAsText,expandEnumsAsText,expandHugeIDAsUniqueIdentifier: boolean;
  options: TDocVariantOptions);
const JAN2015_UNIX = 1420070400;
var t: TTimeLogBits;
    id: TSynUniqueIdentifierBits;
    V: PUtf8Char;
    enum,err: integer;
begin
  if (self=nil) or (row<1) or (row>fRowCount) 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)<cardinal(FieldCount)) then begin
    if fFieldType=nil then
      InitFieldTypes;
    result := fFieldType[Field].ContentType;
  end else
    result := sftUnknown;
end;

function TSQLTable.FieldType(Field: integer; out FieldTypeInfo: PSQLTableFieldType): TSQLFieldType;
begin
  if (self<>nil) and (cardinal(Field)<cardinal(FieldCount)) then begin
    if fFieldType=nil then
      InitFieldTypes;
    FieldTypeInfo := @fFieldType[Field];
    result := FieldTypeInfo^.ContentType;
  end else begin
    FieldTypeInfo := nil;
    result := sftUnknown;
  end;
end;

function TSQLTable.Get(Row, Field: integer): PUTF8Char;
begin
  if (self=nil) or (fResults=nil) or (cardinal(Row)>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 BufferSize<SizeOf(tmp) then
    W := TJSONWriter.CreateOwnedStream(tmp) else
    W := TJSONWriter.CreateOwnedStream(BufferSize);
  try
    W.Expand := Expand;
    GetJSONValues(W,0,0,IDBinarySize); // create JSON data in MS
    W.SetText(result);
  finally
    W.Free;
  end;
end;

procedure TSQLTable.GetCSVValues(Dest: TStream; Tab: boolean; CommaSep: AnsiChar;
  AddBOM: boolean; RowFirst,RowLast: integer);
var U: PPUTF8Char;
    F,R,FMax: integer;
    W: TTextWriter;
    temp: TTextWriterStackBuffer;
begin
  if (self=nil) or (FieldCount<=0) or (fRowCount<=0) then
    exit;
  if (RowLast=0) or (RowLast>fRowCount) 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('<xml xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882" '+
      'xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" '+
      'xmlns:rs="urn:schemas-microsoft-com:rowset" xmlns:z="#RowsetSchema">');
    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('<s:Schema id="RowsetSchema"><s:ElementType name="row" content="eltOnly">');
      for f := 0 to FieldCount-1 do begin
        W.AddShort('<s:AttributeType name="f');
        W.Add(f);
        W.AddShort('" rs:name="');
        W.AddString(fFieldNames[f]);
        W.Add('"');
        W.AddString(FIELDTYPE_TOXML[fFieldType[f].ContentDB]);
        W.Add('/','>');
      end;
      W.AddShort('</s:ElementType></s:Schema>');
      // write rows data
      U := @fResults[FieldCount*RowFirst];
      W.AddShort('<rs:data>');
      for r := RowFirst to RowLast do begin
        W.AddShort('<z:row ');
        for f := 0 to FieldCount-1 do begin
          if U^<>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('</rs:data>');
    end;
    W.AddShort('</xml>');
    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 = '<office:document-content office:version="1.2" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"'+
  ' xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"'+
  ' xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" ><office:body><office:spreadsheet><table:table table:name="Sheet1">'+
  '<table:table-column table:number-columns-repeated="';
  ODSContentFooter = '</table:table><table:named-expressions/></office:spreadsheet></office:body></office:document-content>';
  ODSstyles: RawUTF8 = XMLUTF8_HEADER+'<office:document-styles xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" office:version="1.2"></office:document-styles>';
  ODSmeta: RawUTF8 = XMLUTF8_HEADER+'<office:document-meta xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" office:version="1.2"></office:document-meta>';
  ODSsettings: RawUTF8 = XMLUTF8_HEADER+'<office:document-settings xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" office:version="1.2"></office:document-settings>';
  ODSmanifest: RawUTF8 = XMLUTF8_HEADER+'<manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"'+
  ' manifest:version="1.2"><manifest:file-entry manifest:full-path="/" manifest:version="1.2" manifest:media-type="application/vnd.oasis.opendocument.spreadsheet"/>'+
  '<manifest:file-entry manifest:full-path="meta.xml" manifest:media-type="text/xml"/><manifest:file-entry manifest:full-path="settings.xml" manifest:media-type="text/xml"/>'+
  '<manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/><manifest:file-entry manifest:full-path="styles.xml" manifest:media-type="text/xml"/></manifest:manifest>';
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('<table:table-row>');
            if withColumnTypes and (r>0) then begin
              for f := 0 to FieldCount-1 do begin
                W.AddShort('<table:table-cell office:value-type="');
                case fFieldType[f].ContentDB of
                  ftInt64,ftDouble,ftCurrency: begin
                    W.AddShort('float" office:value="');
                    W.AddXmlEscape(U^);
                    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"><text:p>');
                    W.AddXmlEscape(U^);
                    W.AddShort('</text:p></table:table-cell>');
                  end;
                end;
                inc(U); // points to next value
              end;
            end else
              for f := 0 to FieldCount-1 do begin
                W.AddShort('<table:table-cell office:value-type="string"><text:p>');
                W.AddXmlEscape(U^);
                W.AddShort('</text:p></table:table-cell>');
                inc(U);
              end;
            W.AddShort('</table:table-row>');
          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('<table>'#10);
  U := pointer(fResults);
  for R := 0 to fRowCount do begin
    Dest.AddShort('<tr>');
    for F := 0 to FieldCount-1 do begin
      if R=0 then
        Dest.AddShort('<th>') else
        Dest.AddShort('<td>');
      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('</th>') else
        Dest.AddShort('</td>');
      inc(U); // points to next value
    end;
    Dest.AddShort('</tr>'#10);
  end;
  Dest.AddShort('</table>');
end;

function TSQLTable.GetHtmlTable(const Header: RawUTF8): RawUTF8;
var W: TTextWriter;
    temp: TTextWriterStackBuffer;
begin
  W := TTextWriter.CreateOwnedStream(temp);
  try
    W.AddShort('<html>');
    W.AddString(Header);
    W.AddShort('<body>'#10);
    GetHtmlTable(W);
    W.AddShort(#10'</body></html>');
    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<V2 then
    result := -1 else
    if V1=V2 then
      result := 0 else
      result := +1;
end;

function UTF8CompareBoolean(P1,P2: PUTF8Char): PtrInt;
label Z,P,N;
begin // assume 0 is FALSE, anything else is true
  if P1=P2  then goto Z else
  if P1=nil then goto P else
  if P2=nil then goto N else
  if (P1^=#0) or (PWord(P1)^=ord('0')) then
    if (P2^=#0) or (PWord(P2)^=ord('0')) then begin
Z:    result := 0;  // P1=false P2=false
      exit;
    end else begin
N:    result := -1; // P1=false P2=true
      exit;
    end else
    if (P2^<>#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 V1<V2 then
    result := -1 else
    if V1=V2 then
      result := 0 else
      result := +1;
end;

function UTF8CompareUInt32(P1,P2: PUTF8Char): PtrInt;
var V1,V2: PtrUInt;
begin
  if P1=P2 then begin
    result := 0;
    exit;
  end;
  V1 := GetCardinal(P1);
  V2 := GetCardinal(P2);
  if V1<V2 then
    result := -1 else
    if V1=V2 then
      result := 0 else
      result := +1;
end;

function UTF8CompareRecord(P1,P2: PUTF8Char): PtrInt;
var V1,V2: Int64;
    T1,T2: cardinal;
begin
  if P1=P2 then begin
    result := 0;
    exit;
  end;
  SetInt64(P1,V1);
  SetInt64(P2,V2);
  if V1=V2 then
    result := 0 else begin
    // special RecordRef / TRecordReference INTEGER sort
    T1 := V1 and 63;  // first sort by Table order
    T2 := V2 and 63;
    if T1<T2 then
      result := -1 else
      if T1>T2 then
        result := +1 else
        // we have T1=T2 -> same Table -> sort by ID
        if V1<V2 then
          result := -1 else
          if V1=V2 then
            result := 0 else
            result := +1;
  end;
end;

function UTF8CompareInt64(P1,P2: PUTF8Char): PtrInt;
var V1,V2: Int64;
begin
  if P1=P2 then begin
    result := 0;
    exit;
  end;
  SetInt64(P1,V1);
  SetInt64(P2,V2);
  if V1<V2 then
    result := -1 else
    if V1=V2 then
      result := 0 else
      result := +1;
end;

function UTF8CompareDouble(P1,P2: PUTF8Char): PtrInt;
var V1,V2: TSynExtended;
    Err: integer;
label er;
begin
  if P1=P2 then begin
    result := 0;
    exit;
  end;
  v1 := GetExtended(P1,Err);
  if Err<>0 then begin
er: result := UTF8IComp(P1,P2);
    exit;
  end;
  V2 := GetExtended(P2,Err);
  if Err<>0 then goto er;
  if V1<V2 then // we don't care about exact = for a sort: Epsilon check is slow
    result := -1 else
    result := +1;
end;

function UTF8CompareISO8601(P1,P2: PUTF8Char): PtrInt;
var V1,V2: TDateTime;
begin
  if P1=P2 then begin
    result := 0;
    exit;
  end;
  Iso8601ToDateTimePUTF8CharVar(P1,0,V1);
  Iso8601ToDateTimePUTF8CharVar(P2,0,V2);
  if (V1=0) or (V2=0) then // any invalid date -> compare as strings
    result := StrComp(P1,P2) else
    if SameValue(V1,V2,1/MSecsPerDay) then
      result := 0 else
      if V1<V2 then
        result := -1 else
        result := +1;
end;

var
  /// simple wrapper to UTF-8 compare function for the SQLite3 field datatypes
  // - used internaly for field sorting (see TSQLTable.SortFields() method)
  // and for default User Interface Query (see TSQLRest.QueryIsTrue() method)
  SQLFieldTypeComp: array[TSQLFieldType] of TUTF8Compare  =
   (nil,                  // unknown
    nil,    // AnsiText will be set to AnsiIComp in initialization block below
    {$ifdef USENORMTOUPPER}
    UTF8IComp,           // UTF8Text, 8 bits case insensitive compared
    {$else}
    nil,    // UTF8Text will be set to AnsiIComp in initialization block below
    {$endif}
    UTF8CompareUInt32,   // Enumerate
    UTF8CompareUInt32,   // Set
    UTF8CompareInt64,    // Integer
    UTF8CompareInt64,    // ID
    UTF8CompareRecord,   // Record
    UTF8CompareBoolean,  // Boolean
    UTF8CompareDouble,   // Float
    UTF8CompareISO8601,  // TDateTime
    UTF8CompareInt64,    // TTimeLog
    UTF8CompareCurr64,   // Currency
    nil,                 // Object (TEXT serialization)
    {$ifndef NOVARIANTS}
    nil,                 // Variant (TEXT serialization)
    nil,                 // TNullable*
    {$endif NOVARIANTS}
    nil,                 // Blob
    nil,                 // BlobDynArray
    nil,                 // BlobCustom
    nil,                 // UTF8Custom
    nil,
    UTF8CompareInt64,    // TModTime
    UTF8CompareInt64,    // TCreateTime
    UTF8CompareInt64,    // TID
    UTF8CompareInt64,    // TRecordVersion
    UTF8CompareInt64,    // TSessionUserID
    UTF8CompareISO8601,  // TDateTimeMS
    UTF8CompareInt64,    // TUnixTime
    UTF8CompareInt64);   // TUnixMSTime

type
  /// a static object is used for smaller recursive stack size and faster code
  // - these special sort implementation do 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)
  // - code generated is very optimized: stack and memory usage, CPU registers
  // prefered, multiplication avoided to calculate memory position from index,
  // hand tuned assembler...
  {$ifdef USERECORDWITHMETHODS}TUTF8QuickSort = record
    {$else}TUTF8QuickSort = object{$endif}
  public
    // sort parameters
    Results: PPUtf8CharArray;
    IDColumn: PPUtf8CharArray;
    Params: TSQLTableSortParams;
    CurrentRow: PtrInt;
    // avoid multiplications to calculate data memory position from index
    FieldCountNextPtr, FieldFirstPtr, FieldIDPtr: PtrUInt;
    // temp vars (avoid stack usage):
    PID: Int64;
    PP, CI, CJ: PPUTF8Char;
    I, J: PtrInt;
    /// recursively perform the sort
    procedure Sort(L, R: Integer);
    /// compare value at index I with pivot value
    // - sort by ID if values are identical
    function CompI: integer; {$ifdef HASINLINE}inline;{$endif}
    /// compare value at index J with pivot value
    // - sort by ID if values are identical
    function CompJ: integer; {$ifdef HASINLINE}inline;{$endif}
    /// set the pivot value
    procedure SetPP(aPP: PPUTF8Char; aP: PtrInt);
  end;

procedure TUTF8QuickSort.SetPP(aPP: PPUTF8Char; aP: PtrInt);
begin
  PP := aPP;
  // PID must be updated every time PP is modified
  if Assigned(IDColumn) then
    SetInt64(IDColumn[aP],PID) else
    SetInt64(PPUTF8Char(PtrUInt(aPP)-FieldIDPtr)^,PID);
end;

function TUTF8QuickSort.CompI: integer;
var i64: Int64;
begin
  result := Params.Comp(CI^,PP^);
  if result=0 then begin
    // same value -> sort by ID
    if Assigned(IDColumn) then
      SetInt64(IDColumn[I],i64) else
      SetInt64(PPUTF8Char(PtrUInt(CI)-FieldIDPtr)^,i64);
    if i64<PID then
      result := -1 else
    if i64<>PID 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 i64<PID then
      result := -1 else
    if i64<>PID 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)<cardinal(fFieldCount)) then begin
    if not Assigned(CustomCompare) then
      CustomCompare := fSortParams.Comp;
    if Assigned(CustomCompare) then begin // fast binary search
      L := 1;
      R := fRowCount;
      repeat
        result := (L+R) shr 1;
        cmp := CustomCompare(fResults[result*fFieldCount+FieldIndex],Value);
        if cmp=0 then
          exit;
        if cmp<0 then
          L := result+1 else
          R := result-1;
      until L>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 L<R then
  repeat
    I := L;
    J := R;
    P := (L+R) shr 1;
    repeat
      while Compare(I,P)<0 do inc(I);
      while Compare(J,P)>0 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<T>: TObjectList<T>;
var R,Item: TSQLRecord;
    row: PPUtf8Char;
    i: integer;
    {$ifdef ISDELPHIXE3}rec: PSQLRecordArray;{$endif}
begin
  result := TObjectList<T>.Create; // TObjectList<T> 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)<cardinal(FieldCount)) then begin
    if fFieldType=nil then
      InitFieldTypes;
    with fFieldType[Field] do
    if ContentSize>=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 (Row<result)) then begin
      if FieldIndex<>nil 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)<cardinal(fFieldCount)) then begin
    U := @fResults[FieldCount*StartRow+FieldIndex];
    if CaseSensitive then
      for result := StartRow to fRowCount do
        if StrComp(U^,Value)=0 then
          exit else
          inc(U,FieldCount) else
      for result := StartRow to fRowCount do
        if UTF8IComp(U^,Value)=0 then
          exit else
          inc(U,FieldCount);
  end;
  result := 0;
end;

function TSQLTable.SearchFieldIdemPChar(const Value: RawUTF8; FieldIndex, StartRow: integer): integer;
var U: PPUTF8Char;
    up: RawUTF8;
begin
  if (self<>nil) and (Value<>'') and (cardinal(FieldIndex)<cardinal(fFieldCount)) then begin
    UpperCaseCopy(Value,up);
    U := @fResults[FieldCount*StartRow+FieldIndex];
    for result := StartRow to fRowCount do
      if IdemPChar(U^,pointer(up)) then
        exit else
        inc(U,FieldCount);
  end;
  result := 0;
end;

{$ifndef NOVARIANTS}

function TSQLTable.GetVariant(Row, Field: integer): Variant;
begin
  GetVariant(Row,Field,result);
end;

procedure TSQLTable.GetVariant(Row,Field: integer; var result: variant);
var aType: TSQLFieldType;
    info: PSQLTableFieldType;
    U: PUTF8Char;
begin
  if Row=0 then // Field Name
    RawUTF8ToVariant(GetU(0,Field),result) else begin
    aType := FieldType(Field,info);
    U := Get(Row,Field);
    ValueVarToVariant(U,StrLen(U),aType,TVarData(result),true,info.ContentTypeInfo);
  end;
end;

function TSQLTable.GetValue(const aLookupFieldName,aLookupValue,aValueFieldName: RawUTF8): variant;
var f,r,v: integer;
begin
  SetVariantNull(result);
  f := FieldIndex(aLookupFieldName);
  v := FieldIndex(aValueFieldName);
  if (f<0) or (v<0) then
    exit;
  r := SearchFieldEquals(aLookupValue,f);
  if r>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;
  aUpdateHash: boolean): boolean;
var Hash: cardinal;
begin
  if aUpdateHash then begin
    Hash := crc32c(0,pointer(aJSON),aLen);
    result := (fPrivateCopyHash=0) or (Hash=0) or (Hash<>fPrivateCopyHash);
    if not result then
      exit;
    fPrivateCopyHash := Hash;
  end else
    result := true; // from Create() for better performance on single use
  FastSetString(fPrivateCopy,nil,aLen+16); // +16 for SSE4.2 read-ahead
  MoveFast(pointer(aJSON)^,pointer(fPrivateCopy)^,aLen+1); // +1 for trailing #0
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<nfield-1) then begin
          inc(max,nfield-f);
          break; // allow some missing fields in the input object
        end;
        inc(max);
      end;
      if P=nil then
        break; // unexpected end
      if EndOfObject<>'}' 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,{updatehash=}true) 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);
  PrivateCopyChanged(pointer(aJSON),len,{updatehash=}false);
  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);
  PrivateCopyChanged(pointer(aJSON),len,{updatehash=}false);
  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);
  PrivateCopyChanged(pointer(aJSON),len,{updatehash=}false);
  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): 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(GotoNextNotSpace(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 simple = all non-blob fields
    result := SimpleFields else
    result := Select;
  result := 'SELECT '+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,soBeginning);
    GetJSONValues(UsingStream,Expand,withID,Occasion,SQLRecordOptions);
    FastSetString(result,UsingStream.Memory,UsingStream.Seek(0,soCurrent));
  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+',';
      if Props.Kind=rFTS5 then // FTS5 knows ascii/porter/unicode61
        tokenizer := 'ascii' else
        tokenizer := 'simple'; // FTS3-4 know simple/porter/unicode61
      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);
          break; // e.g. TSQLRecordFTS3Porter -> 'Porter'
        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)<PtrUInt(SystemInfo.lpMinimumApplicationAddress) then
    // was called from a TSQLRecord property (sftID type)
    // (will return 0 if current instance is nil)
    result := PtrUInt(self) else
    result := fID;
    // was called from a real TSQLRecord instance
  {$else}
  if PtrUInt(self)<$100000 then // rough estimation, but works in practice
    result := PtrUInt(self) else
  try
    result := fID;
  except
    result := PtrUInt(self);
  end;
  {$endif MSWINDOWS}
end;

function TSQLRecord.GetIDAsPointer: pointer;
begin
  {$ifdef MSWINDOWS}
  if PtrUInt(self)<PtrUInt(SystemInfo.lpMinimumApplicationAddress) then
    // was called from a TSQLRecord property (sftID type)
    // (will return 0 if current instance is nil)
    result := self else
    // was called from a real TSQLRecord instance
    {$ifndef CPU64}
    if fID>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: PtrInt;
    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)<SizeOf(TSQLRibbonTabParameters)) then
    raise EModelException.CreateUTF8('%.Create(TabParameters?)',[self]);
  SetLength(Tables,TabParametersCount+length(NonVisibleTables));
  for i := 0 to TabParametersCount-1 do begin
    Tables[i] := TabParameters^.Table;
    inc(PByte(TabParameters),TabParametersSize);
  end;
  for i := 0 to high(NonVisibleTables) do
    Tables[i+TabParametersCount] := NonVisibleTables[i];
  Create(Tables,aRoot);
  fRestOwner := Owner;
  SetActions(Actions);
  SetEvents(Events);
end;

constructor TSQLModel.Create;
begin
  raise EModelException.CreateUTF8('Plain %.Create is not allowed: use overloaded Create()',[self]);
end;

function TSQLModel.SafeRoot: RawUTF8;
begin
  if self=nil then
    result := '' else
    result := fRoot;
end;

procedure TSQLModel.SetRoot(const aRoot: RawUTF8);
var i: integer;
begin
  for i := 1 to length(aRoot) do // allow RFC URI + '/' for URI-fragment
    if not (aRoot[i] in ['0'..'9','a'..'z','A'..'Z','_','-','.','~',' ','/']) then
      raise EModelException.CreateUTF8('%.Root="%" contains URI unfriendly chars',[self,aRoot]);
  if (aRoot<>'') 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.fModelMax<fTablesMax) then
      // fastest O(1) search in all registered models (if worth it)
      for i := 0 to Props.fModelMax do
        with Props.fModel[i] do
          if Model=self then begin
            result := TableIndex; // almost always loop-free
            exit;
          end;
    // manual search e.g. if fModel[] is not yet set
    c := pointer(Tables);
    for result := 0 to fTablesMax do
      if c^=aTable then
        exit else
        inc(c);
  end;
  result := -1;
end;

function TSQLModel.GetTableIndexInheritsFrom(aTable: TSQLRecordClass): integer;
begin
  if (self<>nil) 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 i<cardinal(length(fIDGenerator)) then
    result := fIDGenerator[i] else
    result := nil;
end;

function TSQLModel.NewRecord(const SQLTableName: RawUTF8): TSQLRecord;
var aClass: TSQLRecordClass;
begin
  aClass := Table[SQLTableName];
  if aClass=nil then
    result := nil else
    result := aClass.Create;
end;

procedure TSQLModel.SetActions(aActions: PTypeInfo);
begin
  if (aActions=nil) or not (aActions^.Kind=tkEnumeration) then
    fActions := nil else
    fActions := aActions^.EnumBaseType;
end;

procedure TSQLModel.SetEvents(aEvents: PTypeInfo);
begin
  if (aEvents=nil) or not (aEvents^.Kind=tkEnumeration) then
    fEvents := nil else
    fEvents := aEvents^.EnumBaseType;
end;

function TSQLModel.GetSQLCreate(aTableIndex: integer): RawUTF8;
begin
  if (self=nil) or (cardinal(aTableIndex)>cardinal(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.fCache.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.fCache.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
  fServicesRouting := 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<>fServicesRouting then
      if (aServicesRouting=nil) or (aServicesRouting=TSQLRestServerURIContext) then
         raise EServiceException.CreateUTF8('Unexpected %.SetRoutingClass(%)',
           [self,aServicesRouting]) else
         fServicesRouting := 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): 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: PtrInt;
    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 := SepLen*(T.fRowCount-1);
    for i := 1 to T.fRowCount do begin
      L := StrLen(T.fResults[i]); // ignore fResults[0] i.e. field name
      inc(Len,L);
      Lens[i-1] := L;
    end;
    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;
    sql: RawUTF8;
begin
  result := false;
  if (self=nil) or (Value=nil) then
    exit;
  sql := Trim(SQLWhere);
  if not EndWith(sql,' LIMIT 1') then
    sql := sql+' LIMIT 1'; // we keep a single record below
  T := MultiFieldValues(PSQLRecordClass(Value)^,aCustomFieldsCSV,sql);
  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 fCache.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,soBeginning); // 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,soEnd);
  SetLength(Blob,L);
  BlobData.Seek(0,soBeginning);
  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): 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 WITHLOG}
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>: 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<T>(const aCustomFieldsCSV: RawUTF8): TObjectList<T>;
begin
  result := RetrieveList<T>('',[],aCustomFieldsCSV);
end;

function TSQLRest.RetrieveList<T>(const FormatSQLWhere: RawUTF8;
  const BoundsSQLWhere: array of const; const aCustomFieldsCSV: RawUTF8): TObjectList<T>;
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<T>;
  finally
    Table.Free;
  end;
end;

{$endif ISDELPHI2010}


{ 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)<cardinal(Count) then
    if CacheAll then
      Value.FastDeleteSorted(Index) else
      with Values[Index] do begin
        Timestamp512 := 0;
        JSON := '';
        Tag := 0;
      end;
end;

procedure TSQLRestCacheEntry.FlushCacheAllEntries;
var i: integer;
begin
  if not CacheEnable then
    exit;
  Mutex.Lock;
  try
    if CacheAll then
      Value.Clear else
      for i := 0 to Count-1 do
      with Values[i] do begin
        Timestamp512 := 0;
        JSON := '';
        Tag := 0;
      end;
  finally
    Mutex.UnLock;
  end;
end;

procedure TSQLRestCacheEntry.SetCache(aID: TID);
var Rec: TSQLRestCacheEntryValue;
    i: integer;
begin
  Mutex.Lock;
  try
    CacheEnable := true;
    if not CacheAll and not Value.FastLocateSorted(aID,i) and (i>=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
      with fCache[i] do begin
        Mutex.Lock;
        try
          TimeOutMS := aTimeOutMS;
        finally
          Mutex.UnLock;
        end;
        result := true;
      end;
end;

function TSQLRestCache.IsCached(aTable: TSQLRecordClass): boolean;
var i: cardinal;
begin
  result := false;
  if (self=nil) or (aTable=nil) then
    exit;
  i := Rest.Model.GetTableIndexExisting(aTable);
  if i<Cardinal(Length(fCache)) then
    if fCache[i].CacheEnable then
      result := true;
end;

function TSQLRestCache.SetCache(aTable: TSQLRecordClass): boolean;
var i: integer;
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
      with fCache[i] do begin
        // global cache of all records of this table
        Mutex.Lock;
        try
          CacheEnable := true;
          CacheAll := true;
          Value.Clear;
          result := true;
        finally
          Mutex.UnLock;
        end;
      end;
end;

function TSQLRestCache.SetCache(aTable: TSQLRecordClass; aID: TID): boolean;
var i: cardinal;
begin
  result := false;
  if (self=nil) or (aTable=nil) or (aID<=0) then
    exit;
  i := Rest.Model.GetTableIndex(aTable);
  if 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 aTableIndex<Cardinal(Length(fCache)) then
    with fCache[aTableIndex] do
      if CacheEnable then
        SetJSON(aRecord);
end;

procedure TSQLRestCache.Notify(aTableIndex: integer; aID: TID;
  const aJSON: RawUTF8; aAction: TSQLOccasion);
begin
  if (self<>nil) and (aID>0) and (aAction in [soSelect,soInsert,soUpdate]) and
     (aJSON<>'') and (Cardinal(aTableIndex)<Cardinal(Length(fCache))) then
    with fCache[aTableIndex] do
      if CacheEnable then
        SetJSON(aID,aJSON);
end;

procedure TSQLRestCache.NotifyDeletion(aTableIndex: integer; aID: TID);
begin
  if (self<>nil) and (aID>0) and
     (Cardinal(aTableIndex)<Cardinal(Length(fCache))) then
    with fCache[aTableIndex] do
    if CacheEnable then begin
      Mutex.Lock;
      try
        FlushCacheEntry(Value.Find(aID));
      finally
        Mutex.UnLock;
      end;
    end;
end;

procedure TSQLRestCache.NotifyDeletions(aTableIndex: integer; const aIDs: array of Int64);
var i: PtrInt;
begin
  if (self<>nil) and (high(aIDs)>=0) and
     (Cardinal(aTableIndex)<Cardinal(Length(fCache))) then
    with fCache[aTableIndex] 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.NotifyDeletion(aTable: TSQLRecordClass; aID: TID);
begin
  if (self<>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 TableIndex<cardinal(Length(fCache)) then
    with fCache[TableIndex] do
      if CacheEnable and RetrieveJSON(aID,aValue) then
        result := true;
end;

function TSQLRestCache.Retrieve(aTableIndex: integer; aID: TID): RawUTF8;
begin
  result := '';
  if (self<>nil) and (aID>0) and
     (Cardinal(aTableIndex)<Cardinal(Length(fCache))) then
    with fCache[aTableIndex] do
      if CacheEnable then
        RetrieveJSON(aID,result);
end;


{ TSQLRestThread }

constructor TSQLRestThread.Create(aRest: TSQLRest;
  aOwnRest, aCreateSuspended: boolean);
begin
  if aRest=nil then
    raise EORMException.CreateUTF8('%.Create(aRest=nil)',[self]);
  fSafe.Init;
  fRest := aRest;
  fOwnRest := aOwnRest;
  if fThreadName='' then
    FormatUTF8('% %',[self,fRest.Model.Root],fThreadName);
  fEvent := TEvent.Create(nil,false,false,'');
  inherited Create(aCreateSuspended);
end;

procedure TSQLRestThread.WaitForNotExecuting(maxMS: integer);
var endtix: Int64;
begin
  if fExecuting then begin
    endtix := SynCommons.GetTickCount64+maxMS;
    repeat
      SleepHiRes(1); // wait for InternalExecute to finish
    until not fExecuting or (SynCommons.GetTickCount64>=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
    MoveFast(List[index+1],List[index],(Count-index)*SizeOf(List[index]));
end;

function TSQLRestClientCallbacks.UnRegister(aInstance: pointer): boolean;
var i: integer;
begin
  result := false;
  if (self=nil) or (Count=0) then
    exit;
  Safe.Lock;
  try
    for i := Count-1 downto 0 do
      if List[i].Instance=aInstance then
        if UnRegisterByIndex(i) then
          result := true else
          break;
  finally
    Safe.UnLock;
  end;
end;

procedure TSQLRestClientCallbacks.DoRegister(aID: integer;
  aInstance: pointer; aFactory: TInterfaceFactory);
begin
  if aID<=0 then
    exit;
  Safe.Lock;
  try
    if length(List)>=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;
  const FormatMsg: RawUTF8; 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 USELOCKERDEBUG}
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 begin
    if (Call^.OutStatus=HTTP_NOTIMPLEMENTED) and (isOpened in fInternalState) then begin
      InternalClose; // force recreate connection
      Exclude(fInternalState,isOpened);
      if ((Sender=nil) or OnIdleBackgroundThreadActive) then
        InternalURI(Call^); // try request again
    end;
    if Call^.OutStatus<>HTTP_NOTIMPLEMENTED then
      Include(fInternalState,isOpened);
  end;
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 not(rsoNoTableURI in fOptions) and (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;
  Options: TSQLInitializeTableOptions);
begin
  fCreateMissingTablesOptions := Options;
end;

procedure TSQLRestServer.InitializeTables(Options: TSQLInitializeTableOptions);
var t: PtrInt;
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: PtrInt;
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); // do it ASAP
  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<cardinal(length(fStaticData)) then
      result := fStaticData[i] else
      result := nil;
  end else
    result := nil;
end;

function TSQLRestServer.GetStaticTable(aClass: TSQLRecordClass): TSQLRest;
begin
  if (aClass=nil) or ((fStaticData=nil) and (fStaticVirtualTable=nil)) then
    result := nil else
    result := GetStaticTableIndex(Model.GetTableIndexExisting(aClass));
end;

function TSQLRestServer.GetStaticTableIndex(aTableIndex: integer): TSQLRest;
begin
  result := nil;
  if aTableIndex>=0 then begin
    if cardinal(aTableIndex)<cardinal(length(fStaticData)) then
      result := fStaticData[aTableIndex];
    if result=nil then
      if fVirtualTableDirect and (fStaticVirtualTable<>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)<cardinal(length(fStaticData)) then begin
      result := fStaticData[aTableIndex];
      if result<>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 (i<n) and (fStaticData[i]<>nil) and (fStaticData[i]<>aStaticData) then
    exit; // TSQLRecord already registered
  t := length(Model.Tables);
  if n<t then
    SetLength(fStaticData,t);
  fStaticData[i] := aStaticData;
  result := true;
end;

function TSQLRestServer.StaticDataCreate(aClass: TSQLRecordClass;
  const aFileName: TFileName; aBinaryFile: boolean;
  aServerClass: TSQLRestStorageInMemoryClass): TSQLRestStorage;
begin
  result := TSQLRestStorage(GetStaticDataServer(aClass));
  if result<>nil 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)<cardinal(length(fStaticData)) then
      // no SQLite3 module available for fStaticData[] -> 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 ID<?',
             [],[mDeleted,mDeleted+SQLRECORDVERSION_DELETEID_RANGE],max) then begin
            max := max and pred(SQLRECORDVERSION_DELETEID_RANGE);
            if max>current 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 ID<? order by ID';
      if MaxRowLimit>0 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 (UpdatedVersion<DeletedVersion)) then begin
            if (RecordVersion=0) or
               (OneFieldValue(Table,'ID',Rec.IDValue)='') then
              result.Add(Rec,true,true,Rec.fFill.TableMapFields,true) else
              result.Update(Rec,[],true);
            RecordVersion := UpdatedVersion;
            UpdatedVersion := 0;
          end else
          if DeletedVersion>0 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)<cardinal(length(fStaticData)) then
      Rest := fStaticData[TableIndex];
    if (Rest=nil) and (fStaticVirtualTable<>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: PtrInt;
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: PtrInt;
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: PtrInt;
begin
  for i := 0 to high(aMethods) do
    AuthenticationRegister(aMethods[i]);
end;

procedure TSQLRestServer.AuthenticationUnregister(aMethod: TSQLRestServerAuthenticationClass);
var i: PtrInt;
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: PtrInt;
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: PtrInt;
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;
  if rsoNoTableURI in Server.Options then
    TableIndex := -1 else
    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 else if rsoMethodUnderscoreAsSlashURI in Server.Options then begin
      URIUnderscoreAsSlash := URI;
      i := slash; // set e.g. 'Method_Name' from 'ModelRoot/Method/Name' URI
      repeat
        URIUnderscoreAsSlash[i] := '_';
        i := PosEx('/',URI,i+1);
      until i=0;
    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
  begin
    // check URI as 'ModelRoot/MethodName'
    MethodIndex := Server.fPublishedMethods.FindHashed(URI);
    if (MethodIndex<0) and (URIUnderscoreAsSlash<>'') then
      MethodIndex := Server.fPublishedMethods.FindHashed(URIUnderscoreAsSlash);
  end 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
  if MethodIndex >= 0 then
  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>=0) and (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 (i<Length(Stats.fPerTable[rw])) and
             (Stats.fPerTable[rw,i]<>nil) 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
      fCache.Flush else
      if Ctxt.TableID=0 then
        fCache.Flush(Ctxt.Table) else
        fCache.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: PtrInt;
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 // caller made fSessions.Safe.Lock
  if (self<>nil) and (cardinal(aSessionIndex)<cardinal(fSessions.Count)) then begin
    sess := fSessions.List[aSessionIndex];
    if Services<>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.SessionDeleteDeprecated: cardinal;
var i: PtrInt;
begin // caller made fSessions.Safe.Lock
  result := GetTickCount64 shr 10;
  if (self<>nil) and (fSessions<>nil) then begin
    if result<>fSessionsDeprecatedTix then begin
      fSessionsDeprecatedTix := result; // check sessions every second
      for i := fSessions.Count-1 downto 0 do
        if result>TAuthSession(fSessions.List[i]).TimeOutTix then
          SessionDelete(i,nil);
    end;
  end;
end;

function TSQLRestServer.SessionAccess(Ctxt: TSQLRestServerURIContext): TAuthSession;
var i: integer;
    tix, session: cardinal;
    sessions: ^TAuthSession;
begin // caller made fSessions.Safe.Lock
  if (self<>nil) and (fSessions<>nil) then begin
    // check deprecated sessions every second
    tix := SessionDeleteDeprecated;
    // retrieve session from its ID
    sessions := pointer(fSessions.List);
    session := Ctxt.Session;
    if session>CONST_AUTHENTICATION_NOT_USED then
      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 (firstOldIndex<fHistoryUncompressedCount-1) and (size>maxSize) 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)<fTrackChangesHistoryTableIndexCount then begin
        TableHistoryIndex := fTrackChangesHistoryTableIndex[aTableIndex];
        if TableHistoryIndex>=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)<fTrackChangesHistoryTableIndexCount then begin
      fTrackChangesHistoryTableIndex[tableIndex] := TableHistoryIndex;
      if TableHistoryIndex>=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)<fTrackChangesHistoryTableIndexCount) and
     (fTrackChangesHistoryTableIndex[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,fSQLRecordVersionDeleteTable,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 (Gran<Low(fStoredCache)) or (Gran>high(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 (Gran<Low(fStoredCache)) or (Gran>high(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]<tix) then
          exit;
    result := -1;
  finally
    Safe.UnLock;
  end;
end;

function TServicesPublishedInterfacesList.FindService(
  const aServiceName: RawUTF8): TSQLRestServerURIDynArray;
var i,n: integer;
    tix: Int64;
begin
  tix := GetTickCount64;
  result := nil;
  Safe.Lock;
  try
    n := 0;
    for i := Count-1 downto 0 do // downwards to return the latest first
      if FindPropName(List[i].Names,aServiceName)>=0 then
        if (fTimeOut=0) or (fTimeoutTix[i]<tix) then begin
          SetLength(result,n+1);
          result[n] := List[i].PublicURI;
          inc(n);
        end;
  finally
    Safe.UnLock;
  end;
end;

function TServicesPublishedInterfacesList.FindServiceAll(
  const aServiceName: RawUTF8): TSQLRestServerURIStringDynArray;
var i,n: integer;
    tix: Int64;
begin
  tix := GetTickCount64;
  result := nil;
  n := 0;
  Safe.Lock;
  try
    for i := Count-1 downto 0 do // downwards to return the latest first
      if FindPropName(List[i].Names,aServiceName)>=0 then
        if (fTimeOut=0) or (fTimeoutTix[i]<tix) then
          AddRawUTF8(TRawUTF8DynArray(result),n,List[i].PublicURI.URI);
  finally
    Safe.UnLock;
  end;
  SetLength(result,n);
end;

procedure TServicesPublishedInterfacesList.FindServiceAll(
  const aServiceName: RawUTF8; aWriter: TTextWriter);
var i: integer;
    tix: Int64;
begin
  tix := GetTickCount64;
  Safe.Lock;
  try
    aWriter.Add('[');
    if aServiceName='*' then begin
      // for RegisterFromServer: return all TServicesPublishedInterfaces
      for i := 0 to Count-1 do
        with List[i] do
          if (fTimeOut=0) or (fTimeoutTix[i]<tix) then begin
            aWriter.AddRecordJSON(List[i],TypeInfo(TServicesPublishedInterfaces));
            aWriter.Add(',');
          end;
    end else // from SQLRestClientURI.ServiceRetrieveAssociated
      // search matching (and non deprecated) services as TSQLRestServerURI
      for i := Count-1 downto 0 do // downwards to return the latest first
        with List[i] do
          if FindPropName(Names,aServiceName)>=0 then
            if (fTimeOut=0) or (fTimeoutTix[i]<tix) then begin
              aWriter.AddRecordJSON(PublicURI,TypeInfo(TSQLRestServerURI));
              aWriter.Add(',');
            end;
    aWriter.CancelLastComma;
    aWriter.Add(']');
  finally
    Safe.UnLock;
  end;
end;

function TServicesPublishedInterfacesList.RegisterFromServer(Client: TSQLRestClientURI): boolean;
var json: RawUTF8;
begin
  result := Client.CallBackGet('stat',['findservice','*'],json)=HTTP_SUCCESS;
  if result and (json<>'') 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 on %',
            [self,aIndex,fUnique[f].PropInfo.Name]);
    if fValues.FindHashedAndDelete(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);
        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;
      result := true;
      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 aShardRange<MIN_SHARD then
    raise EORMException.CreateUTF8('%.Create(%,aShardRange=%<%) does not make sense',
      [self,aClass,aShardRange,MIN_SHARD]);
  inherited Create(aClass,aServer);
  fShardRange := aShardRange;
  fShardLast := -1;
  fOptions := aOptions;
  if aMaxShardCount<2 then
    fMaxShardCount := 2 else
    fMaxShardCount := aMaxShardCount;
  InitShards; // set fShards[], fShardLast, fShardOffset and fShardLastID
  n := length(fShards);
  fShardNextID := (n+fShardOffset)*fShardRange+1;
  SetLength(fShardTableIndex,n);
  for i := 0 to fShardLast do
    if fShards[i]=nil then
      fShardTableIndex[i] := -1 else
      fShardTableIndex[i] := fShards[i].Model.GetTableIndexExisting(aClass);
  InternalLog('Create(%,range=%,maxcount=%) [%..%]',[fStoredClass,fShardRange,
    fMaxShardCount,fShardOffset,fShardOffset+n-1],sllDB);
end;

destructor TSQLRestStorageShard.Destroy;
var i,j: integer;
    rest: TSQLRest;
begin
  try
    if not (ssoNoConsolidateAtDestroy in fOptions) then
      ConsolidateShards;
  finally
    inherited Destroy;
    for i := 0 to high(fShards) do begin
      rest := fShards[i];
      if rest=nil then
        continue;
      rest.Free;
      for j := i+1 to high(fShards) do
        if fShards[j]=rest then
          fShards[j] := nil; // same instance re-used in fShards[]
    end;
  end;
end;

procedure TSQLRestStorageShard.ConsolidateShards;
begin // do nothing by default
end;


procedure TSQLRestStorageShard.RemoveShard(aShardIndex: integer);
begin
  StorageLock(true,'RemoveShard');
  try
    if (fShards<>nil) 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<integer(fStaticDataCount-1) then
          S.WriteBuffer(CHARS[4],1);
      end;
      S.WriteBuffer(CHARS[6],1);
    end;
  finally
    S.Free;
  end;
  InternalLog('UpdateToFile done in %',[timer.Stop],sllDB);
end;

function TSQLRestServerFullMemory.EngineExecute(const aSQL: RawUTF8): boolean;
begin
  result := false; // not implemented in this basic REST server class
end;

procedure TSQLRestServerFullMemory.Flush(Ctxt: TSQLRestServerURIContext);
begin
  if Ctxt.Method=mPUT then begin
    UpdateToFile;
    Ctxt.Success;
  end;
end;

function TSQLRestServerFullMemory.GetStorage(aTable: TSQLRecordClass): TSQLRestStorageInMemory;
var i: cardinal;
begin
  i := fModel.GetTableIndex(aTable);
  if i>=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,soBeginning);
  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]<LastOK64 then // too old?
      IDs[i] := 0 else // 0 frees entry
      LastEntry := i; // refresh last existing entry
  Count := LastEntry+1; // update count (may decrease list length)
end;

function TSQLLocks.UnLock(aID: TID): boolean;
var P: PInt64;
begin
  if (@self=nil) or (Count=0) or (aID=0) then
    result := false else begin
    P := Int64Scan(pointer(IDs),Count,aID);
    if P=nil then
      result := false else begin
      P^ := 0; // 0 marks free entry
      if ((PtrUInt(P)-PtrUInt(IDs))shr 3>=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].max<B[i].min) or (A[i].min>B[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].max<B[i].min) or (A[i].min>B[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 i<high(Classes) then
      Select := Select+',';
  end;
end;
begin
  result := nil;
  if (self=nil) or (fSourceID=nil) or (fDestID=nil) or (aClient=nil) then
    exit;
  if aSourceID=0 then
    if fSourceID<>nil 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<length(fCustomCollation)) and (fCustomCollation[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)<cardinal(Fields.Count));
  if result then begin
    if Fields.Count>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.Count=0) 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:00:00Z') else // the same pattern for date and dateTime
                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 Obj<>nil then
          if PropIsIDTypeCastedField(P,IsObj,Value) then begin
            HR(P);
            Add(PtrInt(Obj)); // not true instances, but ID
          end else
          if not(woDontStore0 in Options) or not IsObjectDefaultOrVoid(Obj) 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 t<n then begin
          fStaticVirtualTable[t] := nil;
          if IsZero(pointer(fStaticVirtualTable),n*SizeOf(pointer)) then
            fStaticVirtualTable := nil;
        end;
      end;
    fStatic.Free;
  end;
  inherited;
end;

function TSQLVirtualTable.Prepare(var Prepared: TSQLVirtualTablePrepared): boolean;
begin
  result := self<>nil;
  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: PtrInt;
    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 begin
      OutData := Base64ToBin(result);
      result := SecDecrypt(SecCtx,{$ifdef MSWINDOWS}TSSPIBuffer{$endif}(OutData));
    end;
  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: PtrInt;
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 := fServicesFactoryClients.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 := fServicesFactoryClients.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].InterfaceMethodIndex<SERVICE_PSEUDO_METHOD_COUNT then
        include(bits,i);
  while MethodNamesCSV<>nil 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)<SizeInStack then begin
            // no space, put on stack
            InStackOffset := ArgsSizeInStack;
            inc(ArgsSizeInStack,SizeInStack);
            // all other parameters following the current one, must also be placed on stack
            reg := PARAMREG_LAST+1;
            continue;
          end;
          RegisterIdent := reg;
          if SizeInStack>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 MethodIndex<SERVICE_PSEUDO_METHOD_COUNT then
    result := SERVICE_PSEUDO_METHOD[TServiceInternalMethod(MethodIndex)] else begin
    dec(MethodIndex,SERVICE_PSEUDO_METHOD_COUNT);
    if cardinal(MethodIndex)<fMethodsCount then
      result := fMethods[MethodIndex].URI else
      result := '';
  end;
end;

function TInterfaceFactory.GetFullMethodName(aMethodIndex: integer): RawUTF8;
begin
  if self=nil then
    result := '' else begin
    result := GetMethodName(aMethodIndex);
    if result = '' then
      result := fInterfaceName else
      result := fInterfaceName+'.'+result;
  end;
end;

{ low-level ASM for TInterfaceFactory.GetMethodsVirtualTable
  - all ARM, AARCH64 and Linux64 code below was provided by ALF! Thanks! :)  }
{$ifdef FPC}
{$ifdef CPUARM}
{$ifdef ASMORIG}
procedure TInterfacedObjectFake.ArmFakeStub;
var // warning: exact local variables order should match TFakeCallStack
  smetndx: pointer;
  sd7, sd6, sd5, sd4, sd3, sd2, sd1, sd0: double;
  sr3,sr2,sr1,sr0: pointer;
asm
  // get method index
  str  v1,smetndx
  // store registers
  vstr d0,sd0
  vstr d1,sd1
  vstr d2,sd2
  vstr d3,sd3
  vstr d4,sd4
  vstr d5,sd5
  vstr d6,sd6
  vstr d7,sd7
  str r0,sr0
  str r1,sr1
  str r2,sr2
  str r3,sr3
  // TFakeCallStack address as 2nd parameter
  // there is no lea equivalent instruction for ARM (AFAIK), so this is calculated by hand (by looking at assembler)
  sub r1, fp, #128
  // branch to the FakeCall function
  bl FakeCall
  // FakeCall should set Int64 result in method result, and float in aCall.FPRegs["sd0"]
  vstr d0,sd0
end;
{$else}
procedure TInterfacedObjectFake.ArmFakeStub;nostackframe;assembler;
asm
    // get method index
    str   r12,[r13, #-52]
    // create stack space
    mov   r12,r13
    stmfd r13!,{r11,r12,r14,r15}
    sub   r11,r12,#4
    sub   r13,r13,#128
    // store registers
    vstr  d0,[r11, #-112]
    vstr  d1,[r11, #-104]
    vstr  d2,[r11, #-96]
    vstr  d3,[r11, #-88]
    vstr  d4,[r11, #-80]
    vstr  d5,[r11, #-72]
    vstr  d6,[r11, #-64]
    vstr  d7,[r11, #-56]
    str   r0,[r11, #-128]
    str   r1,[r11, #-124]
    str   r2,[r11, #-120]
    str   r3,[r11, #-116]
    // set stack address
    add   r1,r13, #12
    // branch to the FakeCall function
    bl    FakeCall
    // store result
    vstr  d0,[r11, #-112]
    ldmea r11,{r11,r13,r15}
end;
{$endif ASMORIG}

{$endif}
{$ifdef CPUAARCH64}
procedure TInterfacedObjectFake.AArch64FakeStub;
var // warning: exact local variables order should match TFakeCallStack
  sx0, sx1, sx2, sx3, sx4, sx5, sx6, sx7: pointer;
  sd0, sd1, sd2, sd3, sd4, sd5, sd6, sd7: double;
  smetndx:pointer;
asm
  // get method index from IP0 [x16/r16]
  str x16,smetndx
  // store registers
  str d0,sd0
  str d1,sd1
  str d2,sd2
  str d3,sd3
  str d4,sd4
  str d5,sd5
  str d6,sd6
  str d7,sd7
  str x0,sx0
  str x1,sx1
  str x2,sx2
  str x3,sx3
  str x4,sx4
  str x5,sx5
  str x6,sx6
  str x7,sx7
  // TFakeCallStack address as 2nd parameter
  // sx0 is at the stack pointer !
  // local variables are stored in reverse on the stack
  add x1, sp, #0
  // branch to the FakeCall function
  bl FakeCall
  // FakeCall should set Int64 result in method result, and float in aCall.FPRegs["sd0"]
  str d0,sd0
end;
{$endif}
{$endif}

{$ifdef CPUX64}
procedure x64FakeStub;
var // warning: exact local variables order should match TFakeCallStack
  smetndx,
  {$ifdef Linux}
  sxmm7, sxmm6, sxmm5, sxmm4,
  {$endif}
  sxmm3, sxmm2, sxmm1, sxmm0: double;
  {$ifdef Linux}
  sr9, sr8, srcx, srdx, srsi, srdi: pointer;
  {$endif}
asm // caller = mov ax,{MethodIndex}; jmp x64FakeStub
        {$ifndef FPC}
        // FakeCall(self: TInterfacedObjectFake; var aCall: TFakeCallStack): Int64
        // So, make space for two variables (+shadow space)
        // adds $50 to stack, so rcx .. at rpb+$10+$50 = rpb+$60
       .params 2
        {$endif}
        and     rax, $ffff
        mov     smetndx, rax
        movlpd  sxmm0, xmm0 // movlpd to ignore upper 64-bit of 128-bit xmm reg
        movlpd  sxmm1, xmm1
        movlpd  sxmm2, xmm2
        movlpd  sxmm3, xmm3
        {$ifdef LINUX}
        movlpd  sxmm4, xmm4
        movlpd  sxmm5, xmm5
        movlpd  sxmm6, xmm6
        movlpd  sxmm7, xmm7
        mov     sr9, r9
        mov     sr8, r8
        mov     srcx, rcx
        mov     srdx, rdx
        mov     srsi, rsi
        mov     srdi, rdi
        lea     rsi, srdi // TFakeCallStack address as 2nd parameter
        {$else}
        {$ifndef FPC}
        mov     [rbp + $60], rcx
        mov     [rbp + $68], rdx
        mov     [rbp + $70], r8
        mov     [rbp + $78], r9
        {$else}
        mov     [rbp + $10], rcx
        mov     [rbp + $18], rdx
        mov     [rbp + $20], r8
        mov     [rbp + $28], r9
        {$endif FPC}
        lea     rdx, sxmm0 // TFakeCallStack address as 2nd parameter
        {$endif LINUX}
        call    TInterfacedObjectFake.FakeCall
        // FakeCall should set Int64 result in method result,
        //and float in aCall.FPRegs["XMM0"]
        movsd   xmm0, qword ptr sxmm0 // movsd for zero extension
end;
{$endif CPUX64}

const
  STUB_SIZE = 65536; // 16*4 KB (4 KB = memory granularity)

{$ifdef FPC} // alf: multi platforms support
{$ifndef MSWINDOWS}
var
  StubCallAllocMemLastStart: PtrUInt; // avoid unneeded fpmmap() calls

function StubCallAllocMem(const Size, flProtect: DWORD): pointer;
{$ifdef CPUARM}
const
  STUB_RELJMP = {$ifdef CPUARM}$7fffff{$else}$7fffffff{$endif}; // relative jmp
  STUB_INTERV = STUB_RELJMP+1; // try to reserve in closed stub interval
  STUB_ALIGN = QWord($ffffffffffff0000); // align to STUB_SIZE
var start,stop,stub,dist: PtrUInt;
begin
  stub := PtrUInt(@TInterfacedObjectFake.ArmFakeStub);
  if StubCallAllocMemLastStart<>0 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 stop<stub then
    stop := high(PtrUInt);
  stop := stop and STUB_ALIGN;
  while start<stop do begin // try whole -STUB_INTERV..+STUB_INTERV range
    inc(start,STUB_SIZE);
    result := fpmmap(pointer(start),STUB_SIZE,flProtect,MAP_PRIVATE or MAP_ANONYMOUS,-1,0);
    if result<>MAP_FAILED then begin // close enough for a 24/32-bit relative jump?
      dist := abs(stub-PtrUInt(result));
      if dist<STUB_RELJMP then begin
        StubCallAllocMemLastStart := start;
        exit;
      end else
        fpmunmap(result,STUB_SIZE);
    end;
  end;
  result := MAP_FAILED; // error
end;
{$else} // other platforms (Intel+Arm64) use absolute call
begin
  result := fpmmap(nil,STUB_SIZE,flProtect,MAP_PRIVATE OR MAP_ANONYMOUS,-1,0);
end;
{$endif CPUARM}
{$endif MSWINDOWS}
{$endif FPC}

type
  // internal memory buffer created with PAGE_EXECUTE_READWRITE flags
  TFakeStubBuffer = class
  protected
    fStub: PByteArray;
    fStubUsed: cardinal;
  public
    constructor Create;
    destructor Destroy; override;
    // call shall be protected by InterfaceFactoryCache critical section
    class function Reserve(size: Cardinal): pointer;
  end;

var
  CurrentFakeStubBuffer: TFakeStubBuffer;
  {$ifdef UNIX}
  MemoryProtection: boolean = false;
  {$endif UNIX}

constructor TFakeStubBuffer.Create;
begin
  {$ifdef MSWINDOWS}
  fStub := VirtualAlloc(nil,STUB_SIZE,MEM_COMMIT,PAGE_EXECUTE_READWRITE);
  if fStub=nil then
  {$else MSWINDOWS}
  {$ifdef KYLIX3}
  fStub := mmap(nil,STUB_SIZE,PROT_READ or PROT_WRITE or PROT_EXEC,MAP_PRIVATE or MAP_ANONYMOUS,-1,0);
  {$else}
  if not MemoryProtection then
    fStub := StubCallAllocMem(STUB_SIZE,PROT_READ or PROT_WRITE or PROT_EXEC);
  if (fStub=MAP_FAILED) or MemoryProtection then begin
    // i.e. on OpenBSD, we can have w^x protection
    fStub := StubCallAllocMem(STUB_SIZE,PROT_READ OR PROT_WRITE);
    if fStub<>MAP_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 paramcounter<n do
    {$ifdef FPC} begin // FPC has its own RTTI layout
      VMP := PME^.Param[paramcounter];
      f := mORMot.TParamFlags(VMP^.Flags);
      with sm^.Args[argsindex] do begin
        if pfVar in f then
          ValueDirection := smdVar else
        if pfOut in f then
          ValueDirection := smdOut;
          sm^.ArgsNotResultLast := argsindex;
        if ValueDirection<>smdConst 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;
      data := '';
      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 b<length(fBackgroundBatch) then
          FreeAndNil(fBackgroundBatch[b]);
      end;
    end;
  end;
end;

function TSQLRestBackgroundTimer.AsynchBatchStart(Table: TSQLRecordClass;
  SendSeconds, PendingRowThreshold, AutomaticTransactionPerRow: integer;
  Options: TSQLRestBatchOptions): boolean;
var b: Integer;
begin
  result := false;
  if (self=nil) or (SendSeconds<=0) then
    exit;
  b := fRest.Model.GetTableIndexExisting(Table);
  if (fBackgroundBatch<>nil) 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 := aComputed<aCount;
  qoLessThanOrEqualTo:    ok := aComputed<=aCount;
  qoGreaterThan:          ok := aComputed>aCount;
  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 asmndx<RESERVED_VTABLE_SLOTS then
        for i := 1 to fLogCount do begin
          log^.AddAsText(WR,aScope,SepChar);
          inc(log);
        end else
        for i := 1 to fLogCount do begin
          if log^.Method^.ExecutionMethodIndex=asmndx then
            if (aParams='') or (log^.Params=aParams) then
              log^.AddAsText(WR,aScope,SepChar);
          inc(log);
        end;
      WR.CancelLastChar(SepChar);
      WR.SetText(result);
    finally
      WR.Free;
    end;
  end;
end;

function TInterfaceStub.GetLogHash: cardinal;
begin
  result := Hash32(LogAsText);
end;

function TInterfaceStub.TryResolve(aInterface: PTypeInfo; out Obj): boolean;
begin
  if aInterface<>fInterface.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<n do begin
    fact := fInterfaceMethod[i].InterfaceService;
    if somemethods then begin
      FillcharFast(methods,SizeOf(methods),0);
      somemethods := false;
    end;
    repeat
      if (aExcludedMethodNamesCSV<>'') 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]);
  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)<fInterface.fMethodsCount) then
        AddNew;
    end;
  finally
    LeaveCriticalSection(fInstanceLock);
  end;
end;

function TServiceFactoryServer.RestServer: TSQLRestServer;
begin
  if self<>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)));
    dec(TInterfacedObjectHooked(result).FRefCount); // RefCount=1 after TryResolveInternal()
  end;
  else
    result := fImplementationClass.Create;
  end;
  inc(TInterfacedObjectHooked(result).FRefCount); // >0 to call Support() in event
  if Assigned(TSQLRestServer(Rest).OnServiceCreateInstance) then
    TSQLRestServer(Rest).OnServiceCreateInstance(self,result);
  if not AndIncreaseRefCount then
    dec(TInterfacedObjectHooked(result).FRefCount);
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.CurrentStep<smsBefore then begin
      W.CancelAll;
      W.Add('"POST",{Method:"%",Input:{',[exec.Method^.InterfaceDotMethodName]);
    end;
    if exec.CurrentStep<smsAfter then
      W.AddShort('},Output:{Failed:"Probably due to wrong input"');
    W.Add('},Session:%,User:%,Time:%,MicroSec:%',
      [integer(Ctxt.Session),Ctxt.SessionUser,TimeLogNowUTC,timeEnd]);
    if Ctxt.RemoteIPIsLocalHost then
      W.Add('}',',') else
      W.Add(',IP:"%"},',[Ctxt.fRemoteIP]);
    with Ctxt.ServiceExecution^ do
      LogRest.AsynchBatchRawAppend(LogClass,W);
  end;

begin
  if mlInterfaces in TSQLRestServer(Rest).StatLevels then
    {$ifdef LINUX}QueryPerformanceMicroSeconds{$else}QueryPerformanceCounter{$endif}(timeStart);
  // 1. initialize Inst.Instance and Inst.InstanceID
  Inst.InstanceID := 0;
  Inst.Instance := nil;
  case InstanceCreation of
    sicSingle:
      Inst.Instance := CreateInstance(true);
    sicShared:
      Inst.Instance := fSharedInstance;
    sicClientDriven, sicPerSession, sicPerUser, sicPerGroup, sicPerThread: begin
      case InstanceCreation of
      sicClientDriven:
        Inst.InstanceID := Ctxt.ServiceInstanceID;
      sicPerThread:
        Inst.InstanceID := PtrUInt(GetCurrentThreadId);
      else
        if Ctxt.Session>CONST_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: ' +
             'e.g. did you initialize your input record(s)?)',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 (Revision<fSlave.fRecordVersionMax) or
     ((Revision=fSlave.fRecordVersionMax) and (Event<>soInsert)) 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.

