/// abstract database direct access classes
// - this unit is a part of the freeware Synopse framework,
// licensed under a MPL/GPL/LGPL tri-license; version 1.18
unit SynDB;

{
    This file is part of Synopse framework.

    Synopse framework. Copyright (C) 2022 Arnaud Bouchez
      Synopse Informatique - https://synopse.info

  *** BEGIN LICENSE BLOCK *****
  Version: MPL 1.1/GPL 2.0/LGPL 2.1

  The contents of this file are subject to the Mozilla Public License Version
  1.1 (the "License"); you may not use this file except in compliance with
  the License. You may obtain a copy of the License at
  http://www.mozilla.org/MPL

  Software distributed under the License is distributed on an "AS IS" basis,
  WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  for the specific language governing rights and limitations under the License.

  The Original Code is Synopse mORMot framework.

  The Initial Developer of the Original Code is Arnaud Bouchez.

  Portions created by the Initial Developer are Copyright (C) 2022
  the Initial Developer. All Rights Reserved.

  Contributor(s):
  - Adam Siwon (asiwon)
  - Alexander (volax)
  - Alfred Glaenzer (alf)
  - delphinium
  - dominikcz
  - Esteban Martin (EMartin)
  - Joe (at jokusoftware)
  - Maciej Izak (hnb)


  Alternatively, the contents of this file may be used under the terms of
  either the GNU General Public License Version 2 or later (the "GPL"), or
  the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  in which case the provisions of the GPL or the LGPL are applicable instead
  of those above. If you wish to allow use of your version of this file only
  under the terms of either the GPL or the LGPL, and not to allow others to
  use your version of this file under the terms of the MPL, indicate your
  decision by deleting the provisions above and replace them with the notice
  and other provisions required by the GPL or the LGPL. If you do not delete
  the provisions above, a recipient may use your version of this file under
  the terms of any one of the MPL, the GPL or the LGPL.

  ***** END LICENSE BLOCK *****

}

{$I Synopse.inc} // define HASINLINE CPU32 CPU64 OWNNORMTOUPPER

interface

/// if defined, a TQuery class will be defined to emulate the BDE TQuery class
{$define EMULATES_TQUERY}

/// if defined, a set of classes will be defined to implement remote access
{$define WITH_PROXY}

{$ifdef LVCL}
  {$undef EMULATES_TQUERY}
{$endif}

uses
  {$ifdef MSWINDOWS}
  Windows,
  {$else}
  {$ifdef KYLIX3}
  LibC,
  Types,
  SynKylix,
  {$endif}
  {$ifdef FPC}
  SynFPCLinux,
  {$endif}
  {$endif}
  {$ifdef FPC}
  dynlibs,
  {$endif}
  {$ifdef ISDELPHIXE2}System.SysUtils,{$else}SysUtils,{$endif}
  Classes,
  {$ifndef LVCL}
  Contnrs,
  {$endif}
  {$ifndef DELPHI5OROLDER}
  Variants,
  {$endif}
  SynCommons,
  SynTable, // for TSynTableStatement
  SynLog;


{ -------------- TSQLDB* generic classes and types }

type
  // NOTE: TSQLDBFieldType is defined in SynCommons.pas (used by TSQLVar)

  /// an array of RawUTF8, for each existing column type
  // - used e.g. by SQLCreate method
  // - ftUnknown maps int32 field (e.g. boolean), ftNull maps RawUTF8 index # field,
  // ftUTF8 maps RawUTF8 blob field, other types map their default kind
  // - for UTF-8 text, ftUTF8 will define the BLOB field, whereas ftNull will
  // expect to be formated with an expected field length in ColumnAttr
  // - the RowID definition will expect the ORM to create an unique identifier,
  // and will use the ftInt64 type definition for this
  // and send it with the INSERT statement (some databases, like Oracle, do not
  // support standard's IDENTITY attribute) - see http://troels.arvin.dk/db/rdbms
  TSQLDBFieldTypeDefinition = array[TSQLDBFieldType] of RawUTF8;

  /// the diverse type of bound parameters during a statement execution
  // - will be paramIn by default, which is the case 90% of time
  // - could be set to paramOut or paramInOut if must be refereshed after
  // execution (for calling a stored procedure expecting such parameters)
  TSQLDBParamInOutType =
    (paramIn, paramOut, paramInOut);

  /// used to define a field/column layout in a table schema
  // - for TSQLDBConnectionProperties.SQLCreate to describe the new table
  // - for TSQLDBConnectionProperties.GetFields to retrieve the table layout
  TSQLDBColumnDefine = packed record
    /// the Column name
    ColumnName: RawUTF8;
    /// the Column type, as retrieved from the database provider
    // - returned as plain text by GetFields method, to be used e.g. by
    // TSQLDBConnectionProperties.GetFieldDefinitions method
    // - SQLCreate will check for this value to override the default type
    ColumnTypeNative: RawUTF8;
    /// the Column default width (in chars or bytes) of ftUTF8 or ftBlob
    // - can be set to value <0 for CLOB or BLOB column type, i.e. for
    // a value without any maximal length
    ColumnLength: PtrInt;
    /// the Column data precision
    // - used e.g. for numerical values
    ColumnPrecision: PtrInt;
    /// the Column data scale
    // - used e.g. for numerical values
    // - may be -1 if the metadata SQL statement returned NULL
    ColumnScale: PtrInt;
    /// the Column type, as recognized by our SynDB classes
    // - should not be ftUnknown nor ftNull
    ColumnType: TSQLDBFieldType;
    /// specify if column is indexed
    ColumnIndexed: boolean;
  end;

  /// used to define the column layout of a table schema
  // - e.g. for TSQLDBConnectionProperties.GetFields
  TSQLDBColumnDefineDynArray = array of TSQLDBColumnDefine;

  /// used to describe extended Index definition of a table schema
  TSQLDBIndexDefine = packed record
    /// name of the index
    IndexName: RawUTF8;
    /// description of the index type
    // - for MS SQL possible values are:
    // $ HEAP | CLUSTERED | NONCLUSTERED | XML |SPATIAL
    //  - for Oracle:
    // $ NORMAL | BITMAP | FUNCTION-BASED NORMAL | FUNCTION-BASED BITMAP | DOMAIN
    // see @http://docs.oracle.com/cd/B19306_01/server.102/b14237/statviews_1069.htm
    TypeDesc: RawUTF8;
    /// Expression for the subset of rows included in the filtered index
    // - only set for MS SQL - not retrieved for other DB types yet
    Filter: RawUTF8;
    /// comma separated list of indexed column names, in order of their definition
    KeyColumns: RawUTF8;
    /// comma separaded list of a nonkey column added to the index by using the CREATE INDEX INCLUDE clause
    // - only set for MS SQL - not retrieved for other DB types yet
    IncludedColumns: RawUTF8;
    /// if Index is unique
    IsUnique: boolean;
    /// if Index is part of a PRIMARY KEY constraint
    // - only set for MS SQL - not retrieved for other DB types yet
    IsPrimaryKey: boolean;
    /// if Index is part of a UNIQUE constraint
    // - only set for MS SQL - not retrieved for other DB types yet
    IsUniqueConstraint: boolean;
  end;

  /// used to describe extended Index definition of a table schema
  // - e.g. for TSQLDBConnectionProperties.GetIndexes
  TSQLDBIndexDefineDynArray = array of TSQLDBIndexDefine;

  /// used to define a parameter/column layout in a stored procedure schema
  // - for TSQLDBConnectionProperties.GetProcedureParameters to retrieve the stored procedure parameters
  // - can be extended according to https://msdn.microsoft.com/en-us/library/ms711701(v=vs.85).aspx
  TSQLDBProcColumnDefine = packed record
    /// the Column name
    ColumnName: RawUTF8;
    /// the Column type, as retrieved from the database provider
    // - used e.g. by TSQLDBConnectionProperties.GetProcedureParameters method
    ColumnTypeNative: RawUTF8;
    /// the Column default width (in chars or bytes) of ftUTF8 or ftBlob
    // - can be set to value <0 for CLOB or BLOB column type, i.e. for
    // a value without any maximal length
    ColumnLength: PtrInt;
    /// the Column data precision
    // - used e.g. for numerical values
    ColumnPrecision: PtrInt;
    /// the Column data scale
    // - used e.g. for numerical values
    // - may be -1 if the metadata SQL statement returned NULL
    ColumnScale: PtrInt;
    /// the Column type, as recognized by our SynDB classes
    // - should not be ftUnknown nor ftNull
    ColumnType: TSQLDBFieldType;
    /// defines the procedure column as a parameter or a result set column
    ColumnParamType: TSQLDBParamInOutType;
  end;

  /// used to define the parameter/column layout of a stored procedure schema
  // - e.g. for TSQLDBConnectionProperties.GetProcedureParameters
  TSQLDBProcColumnDefineDynArray = array of TSQLDBProcColumnDefine;

  /// possible column retrieval patterns
  // - used by TSQLDBColumnProperty.ColumnValueState
  TSQLDBStatementGetCol = (colNone, colNull, colWrongType, colDataFilled, colDataTruncated);

  /// used to define a field/column layout
  // - for TSQLDBConnectionProperties.SQLCreate to describe the table
  // - for T*Statement.Execute/Column*() methods to map the IRowSet content
  TSQLDBColumnProperty = packed record
    /// the Column name
    ColumnName: RawUTF8;
    /// a general purpose integer value
    // - for SQLCreate: default width (in WideChars or Bytes) of ftUTF8 or ftBlob;
    // if set to 0, a CLOB or BLOB column type will be created - note that
    // UTF-8 encoding is expected when calculating the maximum column byte size
    // for the CREATE TABLE statement (e.g. for Oracle 1333=4000/3 is used)
    // - for TOleDBStatement: the offset of this column in the IRowSet data,
    // starting with a DBSTATUSENUM, the data, then its length (for inlined
    // sftUTF8 and sftBlob only)
    // - for TSQLDBOracleStatement: contains an offset to this column values
    // inside fRowBuffer[] internal buffer
    // - for TSQLDBDatasetStatement: maps TField pointer value
    // - for TSQLDBPostgresStatement: contains the column type OID
    ColumnAttr: PtrUInt;
    /// the Column type, used for storage
    // - for SQLCreate: should not be ftUnknown nor ftNull
    // - for TOleDBStatement: should not be ftUnknown
    // - for SynDBOracle: never ftUnknown, may be ftNull (for SQLT_RSET)
    ColumnType: TSQLDBFieldType;
    /// set if the Column must exists (i.e. should not be null)
    ColumnNonNullable: boolean;
    /// set if the Column shall have unique value (add the corresponding constraint)
    ColumnUnique: boolean;
    /// set if the Column data is inlined within the main rows buffer
    // - for TOleDBStatement: set if column was NOT defined as DBTYPE_BYREF
    // which is the most common case, when column data < 4 KB
    // - for TSQLDBOracleStatement: FALSE if column is an array of
    // POCILobLocator (SQLT_CLOB/SQLT_BLOB) or POCIStmt (SQLT_RSET)
    // - for TSQLDBODBCStatement: FALSE if bigger than 255 WideChar (ftUTF8) or
    // 255 bytes (ftBlob)
    ColumnValueInlined: boolean;
    /// expected column data size
    // - for TSQLDBOracleStatement/TOleDBStatement/TODBCStatement: used to store
    // one column size (in bytes)
    ColumnValueDBSize: cardinal;
    /// optional character set encoding for ftUTF8 columns
    // - for SQLT_STR/SQLT_CLOB (SynDBOracle): equals to the OCI char set
    ColumnValueDBCharSet: integer;
    /// internal DB column data type
    // - for TSQLDBOracleStatement: used to store the DefineByPos() TypeCode,
    // can be SQLT_STR/SQLT_CLOB, SQLT_FLT, SQLT_INT, SQLT_DAT, SQLT_BLOB,
    // SQLT_BIN and SQLT_RSET
    // - for TSQLDBODBCStatement: used to store the DataType as returned
    // by ODBC.DescribeColW() - use private ODBC_TYPE_TO[ColumnType] to
    // retrieve the marshalled type used during column retrieval
    // - for TSQLDBFirebirdStatement: used to store XSQLVAR.sqltype
    // - for TSQLDBDatasetStatement: indicates the TField class type, i.e.
    // 0=TField, 1=TLargeIntField, 2=TWideStringField
    ColumnValueDBType: smallint;
    /// driver-specific encoding information
    // - for SynDBOracle: used to store the ftUTF8 column encoding, i.e. for
    // SQLT_CLOB, equals either to SQLCS_NCHAR or SQLCS_IMPLICIT
    ColumnValueDBForm: byte;
    /// may contain the current status of the column value
    // - for SynDBODBC: state of the latest SQLGetData() call
    ColumnDataState: TSQLDBStatementGetCol;
    /// may contain the current column size for not FIXEDLENGTH_SQLDBFIELDTYPE
    // - for SynDBODBC: size (in bytes) in corresponding fColData[]
    // - TSQLDBProxyStatement: the actual maximum column size
    ColumnDataSize: integer;
  end;

  PSQLDBColumnProperty = ^TSQLDBColumnProperty;

  /// used to define a table/field column layout
  TSQLDBColumnPropertyDynArray = array of TSQLDBColumnProperty;

  /// used to define how a column to be created
  TSQLDBColumnCreate = record
    /// the data type
    // - here, ftUnknown is used for Int32 values, ftInt64 for Int64 values,
    // as expected by TSQLDBFieldTypeDefinition
    DBType: TSQLDBFieldType;
    /// the column name
    Name: RawUTF8;
    /// the width, e.g. for VARCHAR() types
    Width: cardinal;
    /// if the column should be unique
    Unique: boolean;
    /// if the column should be non null
    NonNullable: boolean;
    /// if the column is the ID primary key
    PrimaryKey: boolean;
  end;
  /// used to define how a table is to be created
  TSQLDBColumnCreateDynArray = array of TSQLDBColumnCreate;

  /// identify a CRUD mode of a statement
  // - in addition to CRUD states, cPostgreBulkArray would identify if the ORM
  // should generate unnested/any bound array statements - currently only
  // supported by SynDBPostgres for bulk insert/update/delete
  TSQLDBStatementCRUD = (
    cCreate, cRead, cUpdate, cDelete, cPostgreBulkArray);

  /// identify the CRUD modes of a statement
  // - used e.g. for batch send abilities of a DB engine
  TSQLDBStatementCRUDs = set of TSQLDBStatementCRUD;

  /// the known database definitions
  // - will be used e.g. for TSQLDBConnectionProperties.SQLFieldCreate(), or
  // for OleDB/ODBC/ZDBC tuning according to the connected database engine
  TSQLDBDefinition = (dUnknown, dDefault, dOracle, dMSSQL, dJet, dMySQL,
    dSQLite, dFirebird, dNexusDB, dPostgreSQL, dDB2, dInformix);

  /// set of the available database definitions
  TSQLDBDefinitions = set of TSQLDBDefinition;

  {$M+}
  TSQLDBStatement = class;
  {$M-}

{$ifndef LVCL}
{$ifndef DELPHI5OROLDER}
  /// a custom variant type used to have direct access to a result row content
  // - use ISQLDBRows.RowData method to retrieve such a Variant
  TSQLDBRowVariantType = class(TSynInvokeableVariantType)
  protected
    function IntGet(var Dest: TVarData; const Instance: TVarData; Name: PAnsiChar; NameLen: PtrInt): boolean; override;
  end;
{$endif}
{$endif}

  /// generic interface to access a SQL query result rows
  // - not all TSQLDBStatement methods are available, but only those to retrieve
  // data from a statement result: the purpose of this interface is to make
  // easy access to result rows, not provide all available features - therefore
  // you only have access to the Step() and Column*() methods
  ISQLDBRows = interface
    ['{11291095-9C15-4984-9118-974F1926DB9F}']
    /// after a prepared statement has been prepared returning a ISQLDBRows
    // interface, this method must be called one or more times to evaluate it
    // - you shall call this method before calling any Column*() methods
    // - return TRUE on success, with data ready to be retrieved by Column*()
    // - return FALSE if no more row is available (e.g. if the SQL statement
    // is not a SELECT but an UPDATE or INSERT command)
    // - access the first or next row of data from the SQL Statement result:
    // 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
    // - should raise an Exception on any error
    // - typical use may be:
    // ! var Customer: Variant;
    // ! begin
    // !   with Props.Execute( 'select * from Sales.Customer where AccountNumber like ?',
    // !       ['AW000001%'],@Customer) do begin
    // !     while Step do //  loop through all matching data rows
    // !       assert(Copy(Customer.AccountNumber,1,8)='AW000001');
    // !     ReleaseRows;
    // !   end;
    // ! end;
    function Step(SeekFirst: boolean=false): boolean;
    /// release cursor memory and resources once Step loop is finished
    // - this method call is optional, but is better be used if the ISQLDBRows
    // statement from taken from cache, and returned a lot of content which
    // may still be in client (and server) memory
    // - will also free all temporary memory used for optional logging
    procedure ReleaseRows;

    /// the column/field count of the current Row
    function ColumnCount: integer;
    /// the Column name of the current Row
    // - Columns numeration (i.e. Col value) starts with 0
    // - it's up to the implementation to ensure than all column names are unique
    function ColumnName(Col: integer): RawUTF8;
    /// returns the Column index of a given Column name
    // - Columns numeration (i.e. Col value) starts with 0
    // - returns -1 if the Column name is not found (via case insensitive search)
    function ColumnIndex(const aColumnName: RawUTF8): integer;
    /// the Column type of the current Row
    // - FieldSize can be set to store the size in chars of a ftUTF8 column
    // (0 means BLOB kind of TEXT column)
    function ColumnType(Col: integer; FieldSize: PInteger=nil): TSQLDBFieldType;
    /// returns TRUE if the column contains NULL
    function ColumnNull(Col: integer): boolean;
    /// return a Column integer value of the current Row, first Col is 0
    function ColumnInt(Col: integer): Int64; overload;
    /// return a Column floating point value of the current Row, first Col is 0
    function ColumnDouble(Col: integer): double; overload;
    /// return a Column floating point value of the current Row, first Col is 0
    function ColumnDateTime(Col: integer): TDateTime; overload;
    /// return a column date and time value of the current Row, first Col is 0
    function ColumnTimestamp(Col: integer): TTimeLog; overload;
    /// return a Column currency value of the current Row, first Col is 0
    function ColumnCurrency(Col: integer): currency; overload;
    /// return a Column UTF-8 encoded text value of the current Row, first Col is 0
    function ColumnUTF8(Col: integer): RawUTF8; overload;
    /// return a Column text value as generic VCL string of the current Row, first Col is 0
    function ColumnString(Col: integer): string; overload;
    /// return a Column as a blob value of the current Row, first Col is 0
    function ColumnBlob(Col: integer): RawByteString; overload;
    /// return a Column as a blob value of the current Row, first Col is 0
    function ColumnBlobBytes(Col: integer): TBytes; overload;
    /// read a blob Column into the Stream parameter
    procedure ColumnBlobToStream(Col: integer; Stream: TStream); overload;
    /// write a blob Column into the Stream parameter
    // - expected to be used with 'SELECT .. FOR UPDATE' locking statements
    procedure ColumnBlobFromStream(Col: integer; Stream: TStream); overload;
    /// return a Column as a TSQLVar value, first Col is 0
    // - the specified Temp variable will be used for temporary storage of
    // svtUTF8/svtBlob values
    procedure ColumnToSQLVar(Col: Integer; var Value: TSQLVar;
      var Temp: RawByteString);
    {$ifndef LVCL}
    /// return a Column as a variant
    // - a ftUTF8 TEXT content will be mapped into a generic WideString variant
    // for pre-Unicode version of Delphi, and a generic UnicodeString (=string)
    // since Delphi 2009: you may not loose any data during charset conversion
    // - a ftBlob BLOB content will be mapped into a TBlobData AnsiString variant
    function ColumnVariant(Col: integer): Variant; overload;
    /// return a Column as a variant, first Col is 0
    // - this default implementation will call Column*() method above
    // - a ftUTF8 TEXT content will be mapped into a generic WideString variant
    // for pre-Unicode version of Delphi, and a generic UnicodeString (=string)
    // since Delphi 2009: you may not loose any data during charset conversion
    // - a ftBlob BLOB content will be mapped into a TBlobData AnsiString variant
    function ColumnToVariant(Col: integer; var Value: Variant): TSQLDBFieldType; overload;
    {$endif}
    /// return a special CURSOR Column content as a SynDB result set
    // - Cursors are not handled internally by mORMot, but some databases (e.g.
    // Oracle) usually use such structures to get data from stored procedures
    // - such columns are mapped as ftNull internally - so this method is the only
    // one giving access to the data rows
    // - see also BoundCursor() if you want to access a CURSOR out parameter
    function ColumnCursor(Col: integer): ISQLDBRows; overload;

    /// return a Column integer value of the current Row, from a supplied column name
    function ColumnInt(const ColName: RawUTF8): Int64; overload;
    /// return a Column floating point value of the current Row, from a supplied column name
    function ColumnDouble(const ColName: RawUTF8): double; overload;
    /// return a Column floating point value of the current Row, from a supplied column name
    function ColumnDateTime(const ColName: RawUTF8): TDateTime; overload;
    /// return a column date and time value of the current Row, from a supplied column name
    function ColumnTimestamp(const ColName: RawUTF8): TTimeLog; overload;
    /// return a Column currency value of the current Row, from a supplied column name
    function ColumnCurrency(const ColName: RawUTF8): currency; overload;
    /// return a Column UTF-8 encoded text value of the current Row, from a supplied column name
    function ColumnUTF8(const ColName: RawUTF8): RawUTF8; overload;
    /// return a Column text value as generic VCL string of the current Row, from a supplied column name
    function ColumnString(const ColName: RawUTF8): string; overload;
    /// return a Column as a blob value of the current Row, from a supplied column name
    function ColumnBlob(const ColName: RawUTF8): RawByteString; overload;
    /// return a Column as a blob value of the current Row, from a supplied column name
    function ColumnBlobBytes(const ColName: RawUTF8): TBytes; overload;
    /// read a blob Column into the Stream parameter
    procedure ColumnBlobToStream(const ColName: RawUTF8; Stream: TStream); overload;
    /// write a blob Column into the Stream parameter
    procedure ColumnBlobFromStream(const ColName: RawUTF8; Stream: TStream); overload;
    {$ifndef LVCL}
    /// return a Column as a variant, from a supplied column name
    function ColumnVariant(const ColName: RawUTF8): Variant; overload;
    /// return a Column as a variant, from a supplied column name
    // - since a property getter can't be an overloaded method, we define one
    // for the Column[] property
    function GetColumnVariant(const ColName: RawUTF8): Variant;
    /// return a special CURSOR Column content as a SynDB result set
    // - Cursors are not handled internally by mORMot, but some databases (e.g.
    // Oracle) usually use such structures to get data from strored procedures
    // - such columns are mapped as ftNull internally - so this method is the only
    // one giving access to the data rows
    function ColumnCursor(const ColName: RawUTF8): ISQLDBRows; overload;
    /// return a Column as a variant
    // - this default property can be used to write simple code like this:
    // ! procedure WriteFamily(const aName: RawUTF8);
    // ! var I: ISQLDBRows;
    // ! begin
    // !   I := MyConnProps.Execute('select * from table where name=?',[aName]);
    // !   while I.Step do
    // !     writeln(I['FirstName'],' ',DateToStr(I['BirthDate']));
    // !   I.ReleaseRows;
    // ! end;
    // - of course, using a variant and a column name will be a bit slower than
    // direct access via the Column*() dedicated methods, but resulting code
    // is fast in practice
    property Column[const ColName: RawUTF8]: Variant read GetColumnVariant; default;
    {$ifndef DELPHI5OROLDER}
    /// create a TSQLDBRowVariantType able to access any field content via late binding
    // - i.e. you can use Data.Name to access the 'Name' column of the current row
    // - this Variant will point to the corresponding TSQLDBStatement instance,
    // so it's not necessary to retrieve its value for each row; but once the
    // associated ISQLDBRows instance is released, you won't be able to access
    // its data - use RowDocVariant instead
    // - typical use is:
    // ! var Row: Variant;
    // ! (...)
    // !  with MyConnProps.Execute('select * from table where name=?',[aName]) do begin
    // !    Row := RowData;
    // !    while Step do
    // !      writeln(Row.FirstName,Row.BirthDate);
    // !    ReleaseRows;
    // !  end;
    function RowData: Variant;
    /// create a TDocVariant custom variant containing all columns values
    // - will create a "fast" TDocVariant object instance with all fields
    procedure RowDocVariant(out aDocument: variant;
      aOptions: TDocVariantOptions=JSON_OPTIONS_FAST);
    {$endif DELPHI5OROLDER}
    {$endif LVCL}
    /// return the associated statement instance
    function Instance: TSQLDBStatement;
    // return all rows content as a JSON string
    // - JSON data is retrieved with UTF-8 encoding
    // - if Expanded 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 Expanded is false, JSON data is serialized (used in TSQLTableJSON)
    // & { "FieldCount":1,"Values":["col1","col2",val11,"val12",val21,..] }
    // - BLOB field value is saved as Base64, in the '"\uFFF0base64encodedbinary"'
    // format and contains true BLOB data
    // - if ReturnedRowCount points to an integer variable, it will be filled with
    // the number of row data returned (excluding field names)
    // - similar to corresponding TSQLRequest.Execute method in SynSQLite3 unit
    function FetchAllAsJSON(Expanded: boolean; ReturnedRowCount: PPtrInt=nil): RawUTF8;
    // append all rows content as a JSON stream
    // - JSON data is added to the supplied TStream, with UTF-8 encoding
    // - if Expanded 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 Expanded is false, JSON data is serialized (used in TSQLTableJSON)
    // & { "FieldCount":1,"Values":["col1","col2",val11,"val12",val21,..] }
    // - BLOB field value is saved as Base64, in the '"\uFFF0base64encodedbinary"'
    // format and contains true BLOB data
    // - similar to corresponding TSQLRequest.Execute method in SynSQLite3 unit
    // - returns the number of row data returned (excluding field names)
    function FetchAllToJSON(JSON: TStream; Expanded: boolean): PtrInt;
    /// append all rows content as binary stream
    // - will save the column types and name, then every data row in optimized
    // binary format (faster and smaller than JSON)
    // - you can specify a LIMIT for the data extent (default 0 meaning all data)
    // - generates the format expected by TSQLDBProxyStatement
    function FetchAllToBinary(Dest: TStream; MaxRowCount: cardinal=0;
      DataRowPosition: PCardinalDynArray=nil): cardinal;
  end;

  /// generic interface to bind to prepared SQL query
  // - inherits from ISQLDBRows, so gives access to the result columns data
  // - not all TSQLDBStatement methods are available, but only those to bind
  // parameters and retrieve data after execution
  // - reference counting mechanism of this interface will feature statement
  // cache (if available) for NewThreadSafeStatementPrepared() or PrepareInlined()
  ISQLDBStatement = interface(ISQLDBRows)
  ['{EC27B81C-BD57-47D4-9711-ACFA27B583D7}']
    /// bind a NULL value to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - some providers (e.g. OleDB during MULTI INSERT statements) expect the
    // proper column type to be set in BoundType, even for NULL values
    procedure BindNull(Param: Integer; IO: TSQLDBParamInOutType=paramIn;
      BoundType: TSQLDBFieldType=ftNull);
    /// bind an integer value to a parameter
    // - the leftmost SQL parameter has an index of 1
    procedure Bind(Param: Integer; Value: Int64;
      IO: TSQLDBParamInOutType=paramIn); overload;
    /// bind a double value to a parameter
    // - the leftmost SQL parameter has an index of 1
    procedure Bind(Param: Integer; Value: double;
      IO: TSQLDBParamInOutType=paramIn); overload;
    /// bind a TDateTime value to a parameter
    // - the leftmost SQL parameter has an index of 1
    procedure BindDateTime(Param: Integer; Value: TDateTime;
      IO: TSQLDBParamInOutType=paramIn); overload;
    /// bind a currency value to a parameter
    // - the leftmost SQL parameter has an index of 1
    procedure BindCurrency(Param: Integer; Value: currency;
      IO: TSQLDBParamInOutType=paramIn); overload;
    /// bind a UTF-8 encoded string to a parameter
    // - the leftmost SQL parameter has an index of 1
    procedure BindTextU(Param: Integer; const Value: RawUTF8;
      IO: TSQLDBParamInOutType=paramIn); overload;
    /// bind a UTF-8 encoded buffer text (#0 ended) to a parameter
    // - the leftmost SQL parameter has an index of 1
    procedure BindTextP(Param: Integer; Value: PUTF8Char;
      IO: TSQLDBParamInOutType=paramIn); overload;
    /// bind a UTF-8 encoded string to a parameter
    // - the leftmost SQL parameter has an index of 1
    procedure BindTextS(Param: Integer; const Value: string;
      IO: TSQLDBParamInOutType=paramIn); overload;
    /// bind a UTF-8 encoded string to a parameter
    // - the leftmost SQL parameter has an index of 1
    procedure BindTextW(Param: Integer; const Value: WideString;
      IO: TSQLDBParamInOutType=paramIn); overload;
    /// bind a Blob buffer to a parameter
    // - the leftmost SQL parameter has an index of 1
    procedure BindBlob(Param: Integer; Data: pointer; Size: integer;
      IO: TSQLDBParamInOutType=paramIn); overload;
    /// bind a Blob buffer to a parameter
    // - the leftmost SQL parameter has an index of 1
    procedure BindBlob(Param: Integer; const Data: RawByteString;
      IO: TSQLDBParamInOutType=paramIn); overload;
    /// bind a Variant value to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - will call all virtual Bind*() methods from the Data type
    // - if DataIsBlob is TRUE, will call BindBlob(RawByteString(Data)) instead
    // of BindTextW(WideString(Variant)) - used e.g. by TQuery.AsBlob/AsBytes
    procedure BindVariant(Param: Integer; const Data: Variant; DataIsBlob: boolean;
      IO: TSQLDBParamInOutType=paramIn);
    /// bind one TSQLVar value
    // - the leftmost SQL parameter has an index of 1
    procedure Bind(Param: Integer; const Data: TSQLVar;
      IO: TSQLDBParamInOutType=paramIn); overload;
    /// bind one RawUTF8 encoded value
    // - the leftmost SQL parameter has an index of 1
    // - the value should match the BindArray() format, i.e. be stored as in SQL
    // (i.e. number, 'quoted string', 'YYYY-MM-DD hh:mm:ss', null)
    procedure Bind(Param: Integer; ParamType: TSQLDBFieldType; const Value: RawUTF8;
      ValueAlreadyUnquoted: boolean; IO: TSQLDBParamInOutType=paramIn); overload;
    /// bind an array of const values
    // - parameters marked as ? should be specified as method parameter in Params[]
    // - BLOB parameters can be bound with this method, when set after encoding
    // via BinToBase64WithMagic() call
    // - TDateTime parameters can be bound with this method, when encoded via
    // a DateToSQL() or DateTimeToSQL() call
    procedure Bind(const Params: array of const;
      IO: TSQLDBParamInOutType=paramIn); overload;
    /// bind an array of fields from an existing SQL statement
    // - can be used e.g. after ColumnsToSQLInsert() method call for fast data
    // conversion between tables
    procedure BindFromRows(const Fields: TSQLDBFieldTypeDynArray;
      Rows: TSQLDBStatement);
    /// bind a special CURSOR parameter to be returned as a SynDB result set
    // - Cursors are not handled internally by mORMot, but some databases (e.g.
    // Oracle) usually use such structures to get data from strored procedures
    // - such parameters are mapped as ftUnknown
    // - use BoundCursor() method to retrieve the corresponding ISQLDBRows after
    // execution of the statement
    procedure BindCursor(Param: integer);
    /// return a special CURSOR parameter content as a SynDB result set
    // - this method is not about a column, but a parameter defined with
    // BindCursor() before method execution
    // - Cursors are not handled internally by mORMot, but some databases (e.g.
    // Oracle) usually use such structures to get data from strored procedures
    // - this method allow direct access to the data rows after execution
    function BoundCursor(Param: Integer): ISQLDBRows;

    /// bind an array of values to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - values are stored as in SQL (i.e. number, 'quoted string',
    // 'YYYY-MM-DD hh:mm:ss', null)
    // - this default implementation will raise an exception if the engine
    // does not support array binding
    procedure BindArray(Param: Integer; ParamType: TSQLDBFieldType;
      const Values: TRawUTF8DynArray; ValuesCount: integer); overload;
    /// bind an array of integer values to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - this default implementation will raise an exception if the engine
    // does not support array binding
    procedure BindArray(Param: Integer; const Values: array of Int64); overload;
    /// bind an array of double values to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - this default implementation will raise an exception if the engine
    // does not support array binding
    procedure BindArray(Param: Integer; const Values: array of double); overload;
    /// bind an array of TDateTime values to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - values are stored as in SQL (i.e. 'YYYY-MM-DD hh:mm:ss')
    // - this default implementation will raise an exception if the engine
    // does not support array binding
    procedure BindArrayDateTime(Param: Integer; const Values: array of TDateTime);
    /// bind an array of currency values to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - this default implementation will raise an exception if the engine
    // does not support array binding
    procedure BindArrayCurrency(Param: Integer; const Values: array of currency);
    /// bind an array of RawUTF8 values to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - values are stored as in SQL (i.e. 'quoted string')
    // - this default implementation will raise an exception if the engine
    // does not support array binding
    procedure BindArray(Param: Integer; const Values: array of RawUTF8); overload;

    {$ifndef LVCL}
    /// retrieve the parameter content, after SQL execution
    // - the leftmost SQL parameter has an index of 1
    // - to be used e.g. with stored procedures:
    // ! query :=  'BEGIN TEST_PKG.DUMMY(?, ?, ?, ?, ?); END;';
    // ! stmt := Props.NewThreadSafeStatementPrepared(query, false);
    // ! stmt.Bind(1, in1, paramIn);
    // ! stmt.BindTextU(2, in2, paramIn);
    // ! stmt.BindTextU(3, in3, paramIn);
    // ! stmt.BindTextS(4, '', paramOut); //  to be retrieved with out1: string
    // ! stmt.Bind(5, 0, paramOut);       //  to be retrieved with out2: integer
    // ! stmt.ExecutePrepared;
    // ! stmt.ParamToVariant(4, out1, true);
    // ! stmt.ParamToVariant(5, out2, true);
    // - the parameter should have been bound with IO=paramOut or IO=paramInOut
    // if CheckIsOutParameter is TRUE
    function ParamToVariant(Param: Integer; var Value: Variant;
      CheckIsOutParameter: boolean=true): TSQLDBFieldType;
    {$endif}

    /// execute a prepared SQL statement
    // - parameters marked as ? should have been already bound with Bind*() functions
    // - should raise an Exception on any error
    // - after execution, you can access any returned data via ISQLDBRows methods
    procedure ExecutePrepared;
    // execute a prepared SQL statement and return all rows content as a JSON string
    // - JSON data is retrieved with UTF-8 encoding
    // - if Expanded 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 Expanded is false, JSON data is serialized (used in TSQLTableJSON)
    // & { "FieldCount":1,"Values":["col1","col2",val11,"val12",val21,..] }
    // - BLOB field value is saved as Base64, in the '"\uFFF0base64encodedbinary"'
    // format and contains true BLOB data
    procedure ExecutePreparedAndFetchAllAsJSON(Expanded: boolean; out JSON: RawUTF8);

    function GetForceBlobAsNull: boolean;
    procedure SetForceBlobAsNull(value: boolean);
    /// if set, any BLOB field won't be retrieved, and forced to be null
    // - this may be used to speed up fetching the results for SQL requests
    // with * statements
    property ForceBlobAsNull: boolean read GetForceBlobAsNull write SetForceBlobAsNull;
    function GetForceDateWithMS: boolean;
    procedure SetForceDateWithMS(value: boolean);
    /// if set, any ftDate field will contain the milliseconds information
    // when serialized into ISO-8601 text
    // - this setting is private to each statement, since may vary depending
    // on data definition (e.g. ORM TDateTime/TDateTimeMS)
    property ForceDateWithMS: boolean read GetForceDateWithMS write SetForceDateWithMS;
    /// gets a number of updates made by latest executed statement
    function UpdateCount: Integer;
  end;

{$ifdef WITH_PROXY}
  /// proxy commands implemented by TSQLDBProxyConnectionProperties.Process()
  // - method signature expect "const Input" and "var Output" arguments
  // - Input is not used for cConnect, cDisconnect, cGetForeignKeys,
  // cTryStartTransaction, cCommit, cRollback and cServerTimestamp
  // - Input is the TSQLDBProxyConnectionProperties instance for cInitialize
  // - Input is the RawUTF8 table name for most cGet* metadata commands
  // - Input is the SQL statement and associated bound parameters for cExecute,
  // cExecuteToBinary, cExecuteToJSON, and cExecuteToExpandedJSON, encoded as
  // TSQLDBProxyConnectionCommandExecute record
  // - Output is not used for cConnect, cDisconnect, cCommit, cRollback and cExecute
  // - Output is TSQLDBDefinition (i.e. DBMS type) for cInitialize
  // - Output is TTimeLog for cServerTimestamp
  // - Output is boolean for cTryStartTransaction
  // - Output is TSQLDBColumnDefineDynArray for cGetFields
  // - Output is TSQLDBIndexDefineDynArray for cGetIndexes
  // - Output is TSynNameValue (fForeignKeys) for cGetForeignKeys
  // - Output is TRawUTF8DynArray for cGetTableNames
  // - Output is RawByteString result data for cExecuteToBinary
  // - Output is UpdateCount: integer text for cExecute
  // - Output is RawUTF8 result data for cExecuteToJSON and cExecuteToExpandedJSON
  // - calls could be declared as such:
  // ! Process(cGetToken,?,result: Int64);
  // ! Process(cGetDBMS,User#1Hash: RawUTF8,fDBMS: TSQLDBDefinition);
  // ! Process(cConnect,?,?);
  // ! Process(cDisconnect,?,?);
  // ! Process(cTryStartTransaction,?,started: boolean);
  // ! Process(cCommit,?,?);
  // ! Process(cRollback,?,?);
  // ! Process(cServerTimestamp,?,result: TTimeLog);
  // ! Process(cGetFields,aTableName: RawUTF8,Fields: TSQLDBColumnDefineDynArray);
  // ! Process(cGetIndexes,aTableName: RawUTF8,Indexes: TSQLDBIndexDefineDynArray);
  // ! Process(cGetTableNames,?,Tables: TRawUTF8DynArray);
  // ! Process(cGetForeignKeys,?,fForeignKeys: TSynNameValue);
  // ! Process(cExecute,Request: TSQLDBProxyConnectionCommandExecute,UpdateCount: integer);
  // ! Process(cExecuteToBinary,Request: TSQLDBProxyConnectionCommandExecute,Data: RawByteString);
  // ! Process(cExecuteToJSON,Request: TSQLDBProxyConnectionCommandExecute,JSON: RawUTF8);
  // ! Process(cExecuteToExpandedJSON,Request: TSQLDBProxyConnectionCommandExecute,JSON: RawUTF8);
  // - cExceptionRaised is a pseudo-command, used only for sending an exception
  // to the client in case of execution problem on the server side
  TSQLDBProxyConnectionCommand = (
    cGetToken,cGetDBMS,
    cConnect, cDisconnect, cTryStartTransaction, cCommit, cRollback,
    cServerTimestamp,
    cGetFields, cGetIndexes, cGetTableNames, cGetForeignKeys,
    cExecute, cExecuteToBinary, cExecuteToJSON, cExecuteToExpandedJSON,
    cQuit, cExceptionRaised);

{$endif WITH_PROXY}

  {$M+} { published properties to be logged as JSON }
  TSQLDBConnection = class;
  TSQLDBConnectionProperties = class;
  {$M-}

  /// where the LIMIT clause should be inserted for a given SQL syntax
  // - used by TSQLDBDefinitionLimitClause and SQLLimitClause() method
  TSQLDBDefinitionLimitPosition = (posNone, posWhere, posSelect, posAfter, posOuter);

  /// defines the LIMIT clause to be inserted for a given SQL syntax
  // - used by TSQLDBDefinitionLimitClause and SQLLimitClause() method
  TSQLDBDefinitionLimitClause = record
    Position: TSQLDBDefinitionLimitPosition;
    InsertFmt: PUTF8Char;
  end;

  /// possible events notified to TOnSQLDBProcess callback method
  // - event handler is specified by TSQLDBConnectionProperties.OnProcess or
  // TSQLDBConnection.OnProcess properties
  // - speConnected / speDisconnected will notify TSQLDBConnection.Connect
  // and TSQLDBConnection.Disconnect calls
  // - speNonActive / speActive will be used to notify external DB blocking
  // access, so can be used e.g. to change the mouse cursor shape (this trigger
  // is re-entrant, i.e. it will be executed only once in case of nested calls)
  // - speReconnected will be called if TSQLDBConnection did successfully
  // recover its database connection (on error, TQuery will call
  // speConnectionLost): this event will be called by TSQLDBConnection.Connect
  // after a regular speConnected notification
  // - speConnectionLost will be called by TQuery in case of broken connection,
  // and if Disconnect/Reconnect did not restore it as expected (i.e. speReconnected)
  // - speStartTransaction / speCommit / speRollback will notify the
  // corresponding TSQLDBConnection.StartTransaction, TSQLDBConnection.Commit
  // and TSQLDBConnection.Rollback methods
  TOnSQLDBProcessEvent = (
    speConnected, speDisconnected,
    speNonActive, speActive,
    speConnectionLost, speReconnected,
    speStartTransaction, speCommit, speRollback);

  /// event handler called during all external DB process
  // - event handler is specified by TSQLDBConnectionProperties.OnProcess or
  // TSQLDBConnection.OnProperties properties
  TOnSQLDBProcess = procedure(Sender: TSQLDBConnection; Event: TOnSQLDBProcessEvent) of object;

  /// event handler called when the low-level driver send some warning information
  // - errors will trigger Exceptions, but sometimes the database driver returns
  // some non critical information, which is logged and may be intercepted using
  // the TSQLDBConnectionProperties.OnStatementInfo property
  // - may be used e.g. to track ORA-28001 or ORA-28002 about account expire
  // - is currently implemented by SynDBOracle, SynDBODBC and SynOleDB units
  TOnSQLDBInfo = procedure(Sender: TSQLDBStatement; const Msg: RawUTF8) of object;

  /// actions implemented by TSQLDBConnectionProperties.SharedTransaction()
  TSQLDBSharedTransactionAction = (transBegin,
    transCommitWithoutException, transCommitWithException, transRollback);

  /// defines a callback signature able to handle multiple INSERT
  // - may execute e.g. for 2 fields and 3 data rows on a database engine
  // implementing INSERT with multiple VALUES (like MySQL, PostgreSQL, NexusDB,
  // MSSQL or SQlite3), as implemented by
  // TSQLDBConnectionProperties.MultipleValuesInsert() :
  // $ INSERT INTO TableName(FieldNames[0],FieldNames[1]) VALUES
  // $   (FieldValues[0][0],FieldValues[1][0]),
  // $   (FieldValues[0][1],FieldValues[1][1]),
  // $   (FieldValues[0][2],FieldValues[1][2]);
  // - for other kind of DB which do not support multi values INSERT, may
  // execute a dedicated driver command, like MSSQL "bulk insert" or Firebird
  // "execute block"
  TOnBatchInsert = procedure(Props: TSQLDBConnectionProperties;
    const TableName: RawUTF8; const FieldNames: TRawUTF8DynArray;
    const FieldTypes: TSQLDBFieldTypeArray; RowCount: integer;
    const FieldValues: TRawUTF8DynArrayDynArray) of object;

  /// specify the class of TSQLDBConnectionProperties
  // - sometimes used to create connection properties instances, from a set
  // of available classes (see e.g. SynDBExplorer or sample 16)
  TSQLDBConnectionPropertiesClass = class of TSQLDBConnectionProperties;

  /// abstract class used to set Database-related properties
  // - handle e.g. the Database server location and connection parameters (like
  // UserID and password)
  // - should also provide some Database-specific generic SQL statement creation
  // (e.g. how to create a Table), to be used e.g. by the mORMot layer
  // - this class level will handle a single "main connection" - you may inherit
  // from TSQLDBConnectionThreadSafe to maintain one connection per thread
  TSQLDBConnectionProperties = class
  protected
    fServerName: RawUTF8;
    fDatabaseName: RawUTF8;
    fPassWord: RawUTF8;
    fUserID: RawUTF8;
    fForcedSchemaName: RawUTF8;
    fMainConnection: TSQLDBConnection;
    fBatchSendingAbilities: TSQLDBStatementCRUDs;
    fBatchMaxSentAtOnce: integer;
    fLoggedSQLMaxSize: integer;
    fOnBatchInsert: TOnBatchInsert;
    fDBMS: TSQLDBDefinition;
    fUseCache, fStoreVoidStringAsNull, fLogSQLStatementOnException,
    fRollbackOnDisconnect, fReconnectAfterConnectionError,
    fFilterTableViewSchemaName: boolean;
    fDateTimeFirstChar: AnsiChar;
    {$ifndef UNICODE}
    fVariantWideString: boolean;
    {$endif}
    fStatementMaxMemory: Int64;
    fForeignKeys: TSynNameValue;
    fSQLCreateField: TSQLDBFieldTypeDefinition;
    fSQLCreateFieldMax: cardinal;
    fSQLGetServerTimestamp: RawUTF8;
    fEngineName: RawUTF8;
    fOnProcess: TOnSQLDBProcess;
    fOnStatementInfo: TOnSQLDBInfo;
    fStatementCacheReplicates: integer;
    fConnectionTimeOutTicks: Int64;
    fSharedTransactions: array of record
      SessionID: cardinal;
      RefCount: integer;
      Connection: TSQLDBConnection;
    end;
    fExecuteWhenConnected: TRawUTF8DynArray;
    procedure SetConnectionTimeOutMinutes(minutes: cardinal);
    function GetConnectionTimeOutMinutes: cardinal;
    // this default implementation just returns the fDBMS value or dDefault
    // (never returns dUnknwown)
    function GetDBMS: TSQLDBDefinition; virtual;
    function GetDBMSName: RawUTF8; virtual;
    function GetForeignKeysData: RawByteString;
    procedure SetForeignKeysData(const Value: RawByteString);
    function FieldsFromList(const aFields: TSQLDBColumnDefineDynArray; aExcludeTypes: TSQLDBFieldTypes): RawUTF8;
    function GetMainConnection: TSQLDBConnection; virtual;
    function GetDatabaseNameSafe: RawUTF8; virtual;
    /// any overriden TSQLDBConnectionProperties class should call it in the
    // initialization section of its implementation unit to be recognized
    class procedure RegisterClassNameForDefinition;
    /// will be called at the end of constructor
    // - this default implementation will do nothing
    procedure SetInternalProperties; virtual;
    /// Assign schema name to owner from ForceSchemaName or UserID or Database name
    procedure SetSchemaNameToOwner(out Owner: RawUTF8); virtual;
    /// SQL statement to get all field/column names for a specified Table
    // - used by GetFieldDefinitions public method
    // - should return a SQL "SELECT" statement with the field names as first
    // column, a textual field type as 2nd column, then field length, then
    // numeric precision and scale as 3rd, 4th and 5th columns, and the index
    // count in 6th column
    // - this default implementation just returns nothing
    // - if this method is overridden, the ColumnTypeNativeToDB() method should
    // also be overridden in order to allow conversion from native column
    // type into the corresponding TSQLDBFieldType
    function SQLGetField(const aTableName: RawUTF8): RawUTF8; virtual;
    /// SQL statement to get advanced information about all indexes for a Table
    // - should return a SQL "SELECT" statement with the index names as first
    function SQLGetIndex(const aTableName: RawUTF8): RawUTF8; virtual;
    /// SQL statement to get all parameter for a specified Stored Procedure
    // - used by GetProcedureParameters public method
    // - should return a SQL "SELECT" statement with the parameter names as first
    // column, a textual field type as 2nd column, then parameter length as 3rd, then
    // parameter direction as 4th
    // - this default implementation just returns nothing
    // - if this method is overridden, the ColumnTypeNativeToDB() method should
    // also be overridden in order to allow conversion from native column
    // type into the corresponding TSQLDBFieldType
    function SQLGetParameter(const aProcName: RawUTF8): RawUTF8; virtual;
    /// SQL statement to get all stored procedure names for current connection
    // - used by GetProcedureNames public method
    // - should return a SQL "SELECT" statement with the procedure names as unique column
    // - this default implementation just returns nothing
    // - if this method is overridden, the ColumnTypeNativeToDB() method should
    // also be overridden in order to allow conversion from native column
    // type into the corresponding TSQLDBFieldType
    function SQLGetProcedure: RawUTF8; virtual;
    /// SQL statement to get all table names
    // - used by GetTableNames public method
    // - should return a SQL "SELECT" statement with the table names as
    // first column (any other columns will be ignored)
    // - this default implementation just returns nothing
    function SQLGetTableNames: RawUTF8; virtual;
    /// SQL statement to get all view names
    // - used by GetViewNames public method
    // - should return a SQL "SELECT" statement with the view names as
    // first column (any other columns will be ignored)
    // - this default implementation just returns nothing
    function SQLGetViewNames: RawUTF8; virtual;
    /// should initialize fForeignKeys content with all foreign keys of this
    // database
    // - used by GetForeignKey method
    procedure GetForeignKeys; virtual; abstract;
    /// will use fSQLCreateField[Max] to create the SQL column definition
    // - this default virtual implementation will handle properly all supported
    // database engines, assuming aField.ColumnType as in TSQLDBFieldTypeDefinition
    // - if the field is a primary key, aAddPrimaryKey may be modified to contain
    // some text to be appended at the end of the ALTER/CREATE TABLE statement
    function SQLFieldCreate(const aField: TSQLDBColumnCreate;
      var aAddPrimaryKey: RawUTF8): RawUTF8; virtual;
    /// wrapper around GetIndexes() + set Fields[].ColumnIndexed in consequence
    // - used by some overridden versions of GetFields() method
    procedure GetIndexesAndSetFieldsColumnIndexed(const aTableName: RawUTF8;
      var Fields: TSQLDBColumnDefineDynArray);
    /// check if the exception or its error message is about DB connection error
    // - will be used by TSQLDBConnection.LastErrorWasAboutConnection method
    // - default method will check for the 'conne' sub-string in the message text
    // - should be overridden depending on the error message returned by the DB
    function ExceptionIsAboutConnection(aClass: ExceptClass; const aMessage: RawUTF8): boolean; virtual;
    /// generic method able to implement OnBatchInsert() with parameters
    // - for MySQL, PostgreSQL, MSSQL2008, NexusDB or SQlite3, will execute
    // (with parameters) the extended standard syntax:
    // $ INSERT INTO TableName(FieldNames[0],FieldNames[1]) VALUES
    // $   (FieldValues[0][0],FieldValues[1][0]),
    // $   (FieldValues[0][1],FieldValues[1][1]),
    // $   (FieldValues[0][2],FieldValues[1][2]);
    // - for Firebird, will run the corresponding EXECUTE BLOCK() statement
    // with parameters - but Firebird sounds slower than without any parameter
    // (as tested with ZDBC/ZEOS or UniDAC)
    // - for Oracle, will run (with parameters for values):
    // $ INSERT ALL
    // $  INTO TableName(FieldNames[0],FieldNames[1]) VALUES (?,?)
    // $  INTO TableName(FieldNames[0],FieldNames[1]) VALUES (?,?)
    // $  INTO TableName(FieldNames[0],FieldNames[1]) VALUES (?,?)
    // $ SELECT 1 FROM DUAL;
    procedure MultipleValuesInsert(Props: TSQLDBConnectionProperties;
      const TableName: RawUTF8; const FieldNames: TRawUTF8DynArray;
      const FieldTypes: TSQLDBFieldTypeArray; RowCount: integer;
      const FieldValues: TRawUTF8DynArrayDynArray);
    /// Firebird-dedicated method able to implement OnBatchInsert()
    // - will run an EXECUTE BLOCK statement without any parameters, but
    // including inlined values - sounds to be faster on ZEOS/ZDBC!
    procedure MultipleValuesInsertFirebird(Props: TSQLDBConnectionProperties;
      const TableName: RawUTF8; const FieldNames: TRawUTF8DynArray;
      const FieldTypes: TSQLDBFieldTypeArray; RowCount: integer;
      const FieldValues: TRawUTF8DynArrayDynArray);
  public
    /// initialize the properties
    // - children may optionaly handle the fact that no UserID or Password
    // is supplied here, by displaying a corresponding Dialog box
    constructor Create(const aServerName, aDatabaseName, aUserID, aPassWord: RawUTF8); virtual;
    /// release related memory, and close MainConnection
    destructor Destroy; override;
    /// save the properties into a persistent storage object
    // - you can use TSQLDBConnectionPropertiesDescription.CreateFrom()
    // later on to instantiate the proper TSQLDBConnectionProperties class
    // - current Definition.Key value will be used for the password encryption
    procedure DefinitionTo(Definition: TSynConnectionDefinition); virtual;
    /// save the properties into a JSON file
    // - you could use TSQLDBConnectionPropertiesDescription.CreateFromJSON()
    // later on to instantiate the proper TSQLDBConnectionProperties class
    // - you can specify a custom Key, if the default is not enough for you
    function DefinitionToJSON(Key: cardinal=0): RawUTF8; virtual;
    /// save the properties into a JSON file
    // - you could use TSQLDBConnectionPropertiesDescription.CreateFromFile()
    // later on to instantiate the proper TSQLDBConnectionProperties class
    // - you can specify a custom Key, if the default is not enough for you
    procedure DefinitionToFile(const aJSONFile: TFileName; Key: cardinal=0);
    /// create a new TSQLDBConnectionProperties instance from the stored values
    class function CreateFrom(aDefinition: TSynConnectionDefinition): TSQLDBConnectionProperties; virtual;
    /// create a new TSQLDBConnectionProperties instance from a JSON content
    // - as previously serialized with TSQLDBConnectionProperties.DefinitionToJSON
    // - you can specify a custom Key, if the default is not safe enough for you
    class function CreateFromJSON(const aJSONDefinition: RawUTF8;
      aKey: cardinal=0): TSQLDBConnectionProperties; virtual;
    /// create a new TSQLDBConnectionProperties instance from a JSON file
    // - as previously serialized with TSQLDBConnectionProperties.DefinitionToFile
    // - you can specify a custom Key, if the default is not safe enough for you
    class function CreateFromFile(const aJSONFile: TFileName;
      aKey: cardinal=0): TSQLDBConnectionProperties;
    /// retrieve the registered class from the aDefinition.Kind string
    class function ClassFrom(aDefinition: TSynConnectionDefinition): TSQLDBConnectionPropertiesClass;

    /// create a new connection
    // - call this method if the shared MainConnection is not enough (e.g. for
    // multi-thread access)
    // - the caller is responsible of freeing this instance
    function NewConnection: TSQLDBConnection; virtual;
    /// get a thread-safe connection
    // - this default implementation will return the MainConnection shared
    // instance, so the provider should be thread-safe by itself
    // - TSQLDBConnectionPropertiesThreadSafe will implement a per-thread
    // connection pool, via an internal TSQLDBConnection pool, per thread
    // if necessary (e.g. for OleDB, which expect one TOleDBConnection instance
    // per thread)
    function ThreadSafeConnection: TSQLDBConnection; virtual;
    /// release all existing connections
    // - can be called e.g. after a DB connection problem, to purge the
    // connection pool, and allow automatic reconnection
    // - is called automatically if ConnectionTimeOutMinutes property is set
    // - warning: no connection shall still be used on the background (e.g. in
    // multi-threaded applications), or some unexpected border effects may occur
    procedure ClearConnectionPool; virtual;
    /// specify a maximum period of inactivity after which all connections will
    // be flushed and recreated, to avoid potential broken connections issues
    // - in practice, recreating the connections after a while is safe and
    // won't slow done the process - on the contrary, it may help reducing the
    // consumpted resources, and stabilize long running n-Tier servers
    // - ThreadSafeConnection method will check for the last activity on this
    // TSQLDBConnectionProperties instance, then call ClearConnectionPool
    // to release all active connections if the idle time elapsed was too long
    // - warning: no connection shall still be used on the background (e.g. in
    // multi-threaded applications), or some unexpected issues may occur - for
    // instance, ensure that your mORMot ORM server runs all its statements in
    // blocking mode for both read and write:
    // ! aServer.AcquireExecutionMode[execORMGet] := am***;
    // ! aServer.AcquireExecutionMode[execORMWrite] := am***;
    // here, safe blocking am*** modes are any mode but amUnlocked, i.e. either
    // amLocked, amBackgroundThread or amMainThread
    property ConnectionTimeOutMinutes: cardinal
      read GetConnectionTimeOutMinutes write SetConnectionTimeOutMinutes;
    /// intercept connection errors at statement preparation and try to reconnect
    // - i.e. detect TSQLDBConnection.LastErrorWasAboutConnection in
    // TSQLDBConnection.NewStatementPrepared
    // - warning: no connection shall still be used on the background (e.g. in
    // multi-threaded applications), or some unexpected issues may occur - see
    // AcquireExecutionMode[] recommendations in ConnectionTimeOutMinutes
    property ReconnectAfterConnectionError: boolean
      read fReconnectAfterConnectionError write fReconnectAfterConnectionError;
    /// create a new thread-safe statement
    // - this method will call ThreadSafeConnection.NewStatement
    function NewThreadSafeStatement: TSQLDBStatement;
    /// create a new thread-safe statement from an internal cache (if any)
    // - will call ThreadSafeConnection.NewStatementPrepared
    // - this method should return a prepared statement instance on success
    // - on error, returns nil and you can check Connnection.LastErrorMessage /
    // Connection.LastErrorException to retrieve corresponding error information
    // (if RaiseExceptionOnError is left to default FALSE value, otherwise, it will
    // raise an exception)
    function NewThreadSafeStatementPrepared(const aSQL: RawUTF8;
       ExpectResults: Boolean; RaiseExceptionOnError: Boolean=false): ISQLDBStatement; overload;
    /// create a new thread-safe statement from an internal cache (if any)
    // - this method will call the overloaded NewThreadSafeStatementPrepared method
    // - here Args[] array does not refer to bound parameters, but to values
    // to be changed within SQLFormat in place of '%' characters (this method
    // will call FormatUTF8() internaly); parameters will be bound directly
    // on the returned TSQLDBStatement instance
    // - this method should return a prepared statement instance on success
    // - on error, returns nil and you can check Connnection.LastErrorMessage /
    // Connection.LastErrorException to retrieve correspnding error information
    // (if RaiseExceptionOnError is left to default FALSE value, otherwise, it will
    // raise an exception)
    function NewThreadSafeStatementPrepared(const SQLFormat: RawUTF8;
      const Args: array of const; ExpectResults: Boolean;
      RaiseExceptionOnError: Boolean=false): ISQLDBStatement; overload;
    /// create, prepare and bound inlined parameters to a thread-safe statement
    // - this implementation will call the NewThreadSafeStatement virtual method,
    // then bound inlined parameters as :(1234): and return the resulting statement
    // - raise an exception on error
    // - consider using ExecuteInlined() for direct execution
    function PrepareInlined(const aSQL: RawUTF8; ExpectResults: Boolean): ISQLDBStatement; overload;
    /// create, prepare and bound inlined parameters to a thread-safe statement
    // - overloaded method using FormatUTF8() and inlined parameters
    // - consider using ExecuteInlined() for direct execution
    function PrepareInlined(const SQLFormat: RawUTF8; const Args: array of const;
      ExpectResults: Boolean): ISQLDBStatement; overload;
    /// execute a SQL query, returning a statement interface instance to retrieve
    // the result rows corresponding to the supplied SELECT statement
    // - will call NewThreadSafeStatement method to retrieve a thread-safe
    // statement instance, then run the corresponding Execute() method
    // - raise an exception on error
    // - returns an ISQLDBRows to access any resulting rows (if ExpectResults is
    // TRUE), and provide basic garbage collection, as such:
    // ! procedure WriteFamily(const aName: RawUTF8);
    // ! var I: ISQLDBRows;
    // ! begin
    // !   I := MyConnProps.Execute('select * from table where name=?',[aName]);
    // !   while I.Step do
    // !     writeln(I['FirstName'],' ',DateToStr(I['BirthDate']));
    // !   I.ReleaseRows;
    // ! end;
    // - if RowsVariant is set, you can use it to row column access via late
    // binding, as such:
    // ! procedure WriteFamily(const aName: RawUTF8);
    // ! var R: Variant;
    // ! begin
    // !   with MyConnProps.Execute('select * from table where name=?',[aName],@R) do begin
    // !     while Step do
    // !       writeln(R.FirstName,' ',DateToStr(R.BirthDate));
    // !     ReleaseRows;
    // !   end;
    // ! end;
    // - you can any BLOB field to be returned as null with the ForceBlobAsNull
    // optional parameter
    function Execute(const aSQL: RawUTF8; const Params: array of const
      {$ifndef LVCL}{$ifndef DELPHI5OROLDER}; RowsVariant: PVariant=nil{$endif}{$endif};
      ForceBlobAsNull: boolean=false): ISQLDBRows;
    /// execute a SQL query, without returning any rows
    // - can be used to launch INSERT, DELETE or UPDATE statement, e.g.
    // - will call NewThreadSafeStatement method to retrieve a thread-safe
    // statement instance, then run the corresponding Execute() method
    // - return the number of modified rows, i.e. the ISQLDBStatement.UpdateCount
    // value (or 0 if the DB driver does not supply this value)
    function ExecuteNoResult(const aSQL: RawUTF8; const Params: array of const): integer;
    /// create, prepare, bound inlined parameters and execute a thread-safe statement
    // - this implementation will call the NewThreadSafeStatement virtual method,
    // then bound inlined parameters as :(1234): and call its Execute method
    // - raise an exception on error
    function ExecuteInlined(const aSQL: RawUTF8; ExpectResults: Boolean): ISQLDBRows; overload;
    /// create, prepare, bound inlined parameters and execute a thread-safe statement
    // - overloaded method using FormatUTF8() and inlined parameters
    function ExecuteInlined(const SQLFormat: RawUTF8; const Args: array of const;
      ExpectResults: Boolean): ISQLDBRows; overload;
    /// handle a transaction process common to all associated connections
    // - could be used to share a single transaction among several connections,
    // or to run nested transactions even on DB engines which do not allow them
    // - will use a simple reference counting mechanism to allow nested
    // transactions, identified by a session identifier
    // - will fail if the same connection is not used for the whole process,
    // which would induce a potentially incorrect behavior
    // - returns the connection corresponding to the session, nil on error
    function SharedTransaction(SessionID: cardinal;
      action: TSQLDBSharedTransactionAction): TSQLDBConnection; virtual;

    /// convert a textual column data type, as retrieved e.g. from SQLGetField,
    // into our internal primitive types
    // - default implementation will always return ftUTF8
    function ColumnTypeNativeToDB(const aNativeType: RawUTF8; aScale: integer): TSQLDBFieldType; virtual;
    /// returns the SQL statement used to create a Table
    // - should return the SQL "CREATE" statement needed to create a table with
    // the specified field/column names and types
    // - if aAddID is TRUE, "ID Int64 PRIMARY KEY" column is added as first,
    // and will expect the ORM to create an unique RowID value sent at INSERT
    // (could use "select max(ID) from table" to retrieve the last value) -
    // note that 'ID' is used instead of 'RowID' since it fails on Oracle e.g.
    // - this default implementation will use internal fSQLCreateField and
    // fSQLCreateFieldMax protected values, which contains by default the
    // ANSI SQL Data Types and maximum 1000 inlined WideChars: inherited classes
    // may change the default fSQLCreateField* content or override this method
    function SQLCreate(const aTableName: RawUTF8;
      const aFields: TSQLDBColumnCreateDynArray; aAddID: boolean): RawUTF8; virtual;
    /// returns the SQL statement used to add a column to a Table
    // - should return the SQL "ALTER TABLE" statement needed to add a column to
    // an existing table
    // - this default implementation will use internal fSQLCreateField and
    // fSQLCreateFieldMax protected values, which contains by default the
    // ANSI SQL Data Types and maximum 1000 inlined WideChars: inherited classes
    // may change the default fSQLCreateField* content or override this method
    function SQLAddColumn(const aTableName: RawUTF8;
      const aField: TSQLDBColumnCreate): RawUTF8; virtual;
    /// returns the SQL statement used to add an index to a Table
    // - should return the SQL "CREATE INDEX" statement needed to add an index
    // to the specified column names of an existing table
    // - index will expect UNIQUE values in the specified columns, if Unique
    // parameter is set to true
    // - this default implementation will return the standard SQL statement, i.e.
    // 'CREATE [UNIQUE] INDEX index_name ON table_name (column_name[s])'
    function SQLAddIndex(const aTableName: RawUTF8;
      const aFieldNames: array of RawUTF8; aUnique: boolean;
      aDescending: boolean=false;
      const aIndexName: RawUTF8=''): RawUTF8; virtual;
    /// used to compute a SELECT statement for the given fields
    // - should return the SQL "SELECT ... FROM ..." statement to retrieve
    // the specified column names of an existing table
    // - by default, all columns specified in aFields[] will be available:
    // it will return "SELECT * FROM TableName"
    // - but if you specify a value in aExcludeTypes, it will compute the
    // matching column names to ignore those kind of content (e.g. [stBlob] to
    // save time and space)
    function SQLSelectAll(const aTableName: RawUTF8;
      const aFields: TSQLDBColumnDefineDynArray; aExcludeTypes: TSQLDBFieldTypes): RawUTF8; virtual;
    /// SQL statement to create the corresponding database
    // - this default implementation will only handle dFirebird by now
    function SQLCreateDatabase(const aDatabaseName: RawUTF8;
      aDefaultPageSize: integer=0): RawUTF8; virtual;
    /// convert an ISO-8601 encoded time and date into a date appropriate to
    // be pasted in the SQL request
    // - this default implementation will return the quoted ISO-8601 value, i.e.
    // 'YYYY-MM-DDTHH:MM:SS' (as expected by Microsoft SQL server e.g.)
    // - returns  to_date('....','YYYY-MM-DD HH24:MI:SS')  for Oracle
    function SQLIso8601ToDate(const Iso8601: RawUTF8): RawUTF8; virtual;
    /// convert a TDateTime into a ISO-8601 encoded time and date, as expected
    // by the database provider
    // - e.g. SQLite3, DB2 and PostgreSQL will use non-standard ' ' instead of 'T'
    function SQLDateToIso8601Quoted(DateTime: TDateTime): RawUTF8; virtual;
    /// split a table name to its OWNER.TABLE full name (if applying)
    // - will use ForcedSchemaName property (if applying), or the OWNER. already
    // available within the supplied table name
    procedure SQLSplitTableName(const aTableName: RawUTF8; out Owner, Table: RawUTF8); virtual;
    /// split a procedure name to its OWNER.PACKAGE.PROCEDURE full name (if applying)
    // - will use ForcedSchemaName property (if applying), or the OWNER. already
    // available within the supplied table name
    procedure SQLSplitProcedureName(const aProcName: RawUTF8; out Owner, Package, ProcName: RawUTF8); virtual;
    /// return the fully qualified SQL table name
    // - will use ForcedSchemaName property (if applying), or return aTableName
    // - you can override this method to force the expected format
    function SQLFullTableName(const aTableName: RawUTF8): RawUTF8; virtual;
    /// return a SQL table name with quotes if necessary
    // - can be used e.g. with SELECT statements
    // - you can override this method to force the expected format
    function SQLTableName(const aTableName: RawUTF8): RawUTF8; virtual;

    /// retrieve the column/field layout of a specified table
    // - this default implementation will use protected SQLGetField virtual
    // method to retrieve the field names and properties
    // - used e.g. by GetFieldDefinitions
    // - will call ColumnTypeNativeToDB protected virtual method to guess the
    // each mORMot TSQLDBFieldType
    procedure GetFields(const aTableName: RawUTF8; out Fields: TSQLDBColumnDefineDynArray); virtual;
    /// retrieve the advanced indexed information of a specified Table
    //  - this default implementation will use protected SQLGetIndex virtual
    // method to retrieve the index names and properties
    // - currently only MS SQL and Oracle are supported
    procedure GetIndexes(const aTableName: RawUTF8; out Indexes: TSQLDBIndexDefineDynArray); virtual;
    /// get all field/column definition for a specified Table as text
    // - call the GetFields method and retrieve the column field name and
    // type as 'Name [Type Length Precision Scale]'
    // - if WithForeignKeys is set, will add external foreign keys as '% tablename'
    procedure GetFieldDefinitions(const aTableName: RawUTF8;
      out Fields: TRawUTF8DynArray; WithForeignKeys: boolean);
    /// get one field/column definition as text
    // - return column type as 'Name [Type Length Precision Scale]'
    class function GetFieldDefinition(const Column: TSQLDBColumnDefine): RawUTF8;
    /// get one field/column definition as text, targeting a TSQLRecord
    // published property
    // - return e.g. property type information as:
    // ! 'Name: RawUTF8 read fName write fName index 20;';
    class function GetFieldORMDefinition(const Column: TSQLDBColumnDefine): RawUTF8;
    /// check if the supplied text word is not a keyword for a given database engine
    class function IsSQLKeyword(aDB: TSQLDBDefinition; aWord: RawUTF8): boolean; overload; virtual;
    /// check if the supplied text word is not a keyword for the current database engine
    // - just a wrapper around the overloaded class function
    function IsSQLKeyword(aWord: RawUTF8): boolean; overload;
    /// retrieve a list of stored procedure names from current connection
    procedure GetProcedureNames(out Procedures: TRawUTF8DynArray); virtual;
    /// retrieve procedure input/output parameter information
    // - aProcName: stored procedure name to retrieve parameter infomation.
    // - Parameters: parameter list info (name, datatype, direction, default)
    procedure GetProcedureParameters(const aProcName: RawUTF8;
      out Parameters: TSQLDBProcColumnDefineDynArray); virtual;
    /// get all table names
    // - this default implementation will use protected SQLGetTableNames virtual
    // method to retrieve the table names
    procedure GetTableNames(out Tables: TRawUTF8DynArray); virtual;
    /// get all view names
    // - this default implementation will use protected SQLGetViewNames virtual
    // method to retrieve the view names
    procedure GetViewNames(out Views: TRawUTF8DynArray); virtual;
    /// retrieve a foreign key for a specified table and column
    // - first time it is called, it will retrieve all foreign keys from the
    // remote database using virtual protected GetForeignKeys method into
    // the protected fForeignKeys list: this may be slow, depending on the
    // database access (more than 10 seconds waiting is possible)
    // - any further call will use this internal list, so response will be
    // immediate
    // - the whole foreign key list is shared by all connections
    function GetForeignKey(const aTableName, aColumnName: RawUTF8): RawUTF8;

    /// returns the information to adapt the LIMIT # clause in the SQL SELECT
    // statement to a syntax matching the underlying DBMS
    // - e.g. TSQLRestStorageExternal.AdaptSQLForEngineList() calls this
    // to let TSQLRestServer.URI by-pass virtual table mechanism
    function SQLLimitClause(AStmt: TSynTableStatement): TSQLDBDefinitionLimitClause; virtual;
    /// determine if the SQL statement can be cached
    // - used by TSQLDBConnection.NewStatementPrepared() for handling cache
    function IsCachable(P: PUTF8Char): boolean; virtual;
    /// return the database engine name, as computed from the class name
    // - 'TSQLDBConnectionProperties' will be trimmed left side of the class name
    class function EngineName: RawUTF8;

    /// return a shared connection, corresponding to the given database
    // - call the ThreadSafeConnection method instead e.g. for multi-thread
    // access, or NewThreadSafeStatement for direct retrieval of a new statement
    property MainConnection: TSQLDBConnection read GetMainConnection;
    /// the associated User Password, as specified at creation
    // - not published, for security reasons (may be serialized otherwise)
    property PassWord: RawUTF8 read fPassWord;
    /// the associated database name, as specified at creation
    // - not published, for security reasons (may be serialized otherwise)
    // - DatabaseNameSafe will be published, and delete any matching
    // PasswordValue in DatabaseName
    property DatabaseName: RawUTF8 read fDatabaseName;
    /// can be used to store the fForeignKeys[] data in an external BLOB
    // - since GetForeignKeys can be (somewhat) slow, could save a lot of time
    property ForeignKeysData: RawByteString
      read GetForeignKeysData write SetForeignKeysData;
    /// this event handler will be called during all process
    // - can be used e.g. to change the desktop cursor, or be notified
    // on connection/disconnection/reconnection
    // - you can override this property directly in the TSQLDBConnection
    property OnProcess: TOnSQLDBProcess read fOnProcess write fOnProcess;
    /// this event handler will be called when statements trigger some low-level
    // information
    property OnStatementInfo: TOnSQLDBInfo read fOnStatementInfo write fOnStatementInfo;
    /// you can define a callback method able to handle multiple INSERT
    // - may execute e.g. INSERT with multiple VALUES (like MySQL, MSSQL, NexusDB,
    // PostgreSQL or SQlite3), as defined by MultipleValuesInsert() callback
    property OnBatchInsert: TOnBatchInsert read fOnBatchInsert write fOnBatchInsert;
  published { to be logged as JSON - no UserID nor Password for security :) }
    /// return the database engine name, as computed from the class name
    // - 'TSQLDBConnectionProperties' will be trimmed left side of the class name
    property Engine: RawUTF8 read fEngineName;
    /// the associated server name, as specified at creation
    property ServerName: RawUTF8 read fServerName;
    /// the associated database name, safely trimmed from the password
    // - would replace any matching Password value content from DatabaseName
    // by '***' for security reasons, e.g. before serialization
    property DatabaseNameSafe: RawUTF8 read GetDatabaseNameSafe;
    /// the associated User Identifier, as specified at creation
    property UserID: RawUTF8 read fUserID;
    /// the remote DBMS type, as stated by the inheriting class itself, or
    //  retrieved at connecton time (e.g. for ODBC)
    property DBMS: TSQLDBDefinition read GetDBMS;
    /// the remote DBMS type name, retrieved as text from the DBMS property
    property DBMSEngineName: RawUTF8 read GetDBMSName;
    /// the abilities of the database for batch sending
    // - e.g. Oracle will handle array DML binds, or MS SQL bulk insert
    property BatchSendingAbilities: TSQLDBStatementCRUDs read fBatchSendingAbilities;
    /// the maximum number of rows to be transmitted at once for batch sending
    // - e.g. Oracle handles array DML operation with iters <= 32767 at best
    // - if OnBatchInsert points to MultipleValuesInsert(), this value is
    // ignored, and the maximum number of parameters is guessed per DBMS type
    property BatchMaxSentAtOnce: integer read fBatchMaxSentAtOnce write fBatchMaxSentAtOnce;
    /// the maximum size, in bytes, of logged SQL statements
    // - setting 0 will log statement and parameters with no size limit
    // - setting -1 will log statement without any parameter value (just ?)
    // - setting any value >0 will log statement and parameters up to the
    // number of bytes (default set to 2048 to log up to 2KB per statement)
    property LoggedSQLMaxSize: integer read fLoggedSQLMaxSize write fLoggedSQLMaxSize;
    /// allow to log the SQL statement when any low-level ESQLDBException is raised
    property LogSQLStatementOnException: boolean read fLogSQLStatementOnException
      write fLogSQLStatementOnException;
    /// an optional Schema name to be used for SQLGetField() instead of UserID
    // - by default, UserID will be used as schema name, if none is specified
    // (i.e. if table name is not set as SCHEMA.TABLE)
    // - depending on the DBMS identified, the class may also set automatically
    // the default 'dbo' for MS SQL or 'public' for PostgreSQL
    // - you can set a custom schema to be used instead
    property ForcedSchemaName: RawUTF8 read fForcedSchemaName write fForcedSchemaName;
    /// if GetTableNames/GetViewNames should only return the table names
    // starting with 'ForcedSchemaName.' prefix
    property FilterTableViewSchemaName: boolean
      read fFilterTableViewSchemaName write fFilterTableViewSchemaName;
    /// TRUE if an internal cache of SQL statement should be used
    // - cache will be accessed for NewStatementPrepared() method only, by
    // returning ISQLDBStatement interface instances
    // - default value is TRUE for faster process (e.g. TTestSQLite3ExternalDB
    // regression tests will be two times faster with statement caching)
    // - will cache only statements containing ? parameters or a SELECT with no
    // WHERE clause within
    property UseCache: boolean read fUseCache write fUseCache;
    /// maximum bytes allowed for FetchAllToJSON/FetchAllToBinary methods
    // - if a result set exceeds this limit, an ESQLDBException is raised
    // - default is 512 shl 20, i.e. 512MB which is very high
    // - avoid unexpected OutOfMemory errors when incorrect statement is run
    property StatementMaxMemory: Int64
      read fStatementMaxMemory write fStatementMaxMemory;
    /// if UseCache is true, how many statement replicates can be generated
    // if the cached ISQLDBStatement is already used
    // - such replication is normally not needed in a per-thread connection,
    // unless ISQLDBStatement are not released as soon as possible
    // - above this limit, no cache will be made, and a dedicated single-time
    // statement will be prepared
    // - default is 0 to cache statements once - but you may try to increase
    // this value if you run identical SQL with long-standing ISQLDBStatement;
    // or you can set -1 if you don't want the warning log to appear
    property StatementCacheReplicates: integer read fStatementCacheReplicates
      write fStatementCacheReplicates;
    /// defines if TSQLDBConnection.Disconnect shall Rollback any pending
    // transaction
    // - some engines executes a COMMIT when the client is disconnected, others
    // do raise an exception: this parameter ensures that any pending transaction
    // is roll-backed before disconnection
    // - is set to TRUE by default
    property RollbackOnDisconnect: Boolean read fRollbackOnDisconnect write fRollbackOnDisconnect;
    /// defines if '' string values are to be stored as SQL null
    // - by default, '' will be stored as ''
    // - but some DB engines (e.g. Jet or MS SQL) does not allow by default to
    // store '' values, but expect NULL to be stored instead
    property StoreVoidStringAsNull: Boolean read fStoreVoidStringAsNull write fStoreVoidStringAsNull;
    /// customize the ISO-8601 text format expected by the database provider
    // - is 'T' by default, as expected by the ISO-8601 standard
    // - will be changed e.g. for PostgreSQL, which expects ' ' instead
    // - as used by SQLDateToIso8601Quoted() and BindArray()
    property DateTimeFirstChar: AnsiChar read fDateTimeFirstChar write fDateTimeFirstChar;
    {$ifndef UNICODE}
    /// set to true to force all variant conversion to WideString instead of
    // the default faster AnsiString, for pre-Unicode version of Delphi
    // - by default, the conversion to Variant will create an AnsiString kind
    // of variant: for pre-Unicode Delphi, avoiding WideString/OleStr content
    // will speed up the process a lot, if you are sure that the current
    // charset matches the expected one (which is very likely)
    // - set this property to TRUE so that the conversion to Variant will
    // create a WideString kind of variant, to avoid any character data loss:
    // the access to the property will be slower, but you won't have any
    // potential data loss
    // - starting with Delphi 2009, the TEXT content will be stored as an
    // UnicodeString in the variant, so this property is not necessary
    // - the Variant conversion is mostly used for the TQuery wrapper, or for
    // the ISQLDBRows.Column[] property or ISQLDBRows.ColumnVariant() method;
    // this won't affect other Column*() methods, or JSON production
    property VariantStringAsWideString: boolean read fVariantWideString write fVariantWideString;
    {$endif}
    /// SQL statements what will be executed for each new connection
    // usage scenarios examples:
    // - Oracle: force case-insensitive like
    // $  ['ALTER SESSION SET NLS_COMP=LINGUISTIC', 'ALTER SESSION SET NLS_SORT=BINARY_CI']
    //  - Postgres: disable notices and warnings
    // $  ['SET client_min_messages to ERROR']
    // - SQLite3: turn foreign keys ON
    // $  ['PRAGMA foreign_keys = ON']
    property ExecuteWhenConnected: TRawUTF8DynArray read fExecuteWhenConnected
      write fExecuteWhenConnected;
  end;

  {$ifdef WITH_PROXY}
  /// server-side implementation of a proxy connection to any SynDB engine
  // - this default implementation will send the data without compression,
  // digital signature, nor encryption
  // - inherit from this class to customize the transmission layer content
  TSQLDBProxyConnectionProtocol = class
  protected
    fAuthenticate: TSynAuthenticationAbstract;
    fTransactionSessionID: integer;
    fTransactionRetryTimeout: Int64;
    fTransactionActiveTimeout: Int64;
    fTransactionActiveAutoReleaseTicks: Int64;
    fLock: TRTLCriticalSection;
    function GetAuthenticate: TSynAuthenticationAbstract;
    /// default Handle*() will just return the incoming value
    function HandleInput(const input: RawByteString): RawByteString; virtual;
    function HandleOutput(const output: RawByteString): RawByteString; virtual;
    /// default trial transaction
    function TransactionStarted(connection: TSQLDBConnection;
      sessionID: integer): boolean; virtual;
    procedure TransactionEnd(sessionID: integer); virtual;
  public
    /// initialize a protocol, with a given authentication scheme
    // - if no authentication is given, none will be processed
    constructor Create(aAuthenticate: TSynAuthenticationAbstract); reintroduce;
    /// release associated authentication class
    destructor Destroy; override;
    /// the associated authentication information
    // - you can manage users via AuthenticateUser/DisauthenticateUser methods
    property Authenticate: TSynAuthenticationAbstract read GetAuthenticate write fAuthenticate;
  end;

  /// server-side implementation of a remote connection to any SynDB engine
  // - implements digitally signed SynLZ-compressed binary message format,
  // with simple symmetric encryption, as expected by SynDBRemote.pas
  TSQLDBRemoteConnectionProtocol = class(TSQLDBProxyConnectionProtocol)
  protected
    /// SynLZ decompression + digital signature + encryption
    function HandleInput(const input: RawByteString): RawByteString; override;
    /// SynLZ compression + digital signature + encryption
    function HandleOutput(const output: RawByteString): RawByteString; override;
  public
  end;

  /// specify the class of a proxy/remote connection to any SynDB engine
  TSQLDBProxyConnectionProtocolClass = class of TSQLDBProxyConnectionProtocol;
  {$endif WITH_PROXY}

  /// abstract connection created from TSQLDBConnectionProperties
  // - more than one TSQLDBConnection instance can be run for the same
  // TSQLDBConnectionProperties
  TSQLDBConnection = class
  protected
    fProperties: TSQLDBConnectionProperties;
    fErrorException: ExceptClass;
    fErrorMessage: RawUTF8;
    fTransactionCount: integer;
    fServerTimestampOffset: TDateTime;
    fServerTimestampAtConnection: TDateTime;
    fCache: TRawUTF8List;
    fOnProcess: TOnSQLDBProcess;
    fTotalConnectionCount: integer;
    fInternalProcessActive: integer;
    fRollbackOnDisconnect: Boolean;
    fLastAccessTicks: Int64;
    function IsOutdated(tix: Int64): boolean; // do not make virtual
    function GetInTransaction: boolean; virtual;
    function GetServerTimestamp: TTimeLog;
    function GetServerDateTime: TDateTime; virtual;
    function GetLastErrorWasAboutConnection: boolean;
    /// raise an exception if IsConnected returns false
    procedure CheckConnection;
    /// call OnProcess() call back event, if needed
    procedure InternalProcess(Event: TOnSQLDBProcessEvent);
  public
    /// connect to a specified database engine
    constructor Create(aProperties: TSQLDBConnectionProperties); virtual;
    /// release memory and connection
    destructor Destroy; override;

    /// connect to the specified database
    // - should raise an Exception on error
    // - this default implementation will notify OnProgress callback for
    // sucessfull re-connection: it should be called in overridden methods
    // AFTER actual connection process
    procedure Connect; virtual;
    /// stop connection to the specified database
    // - should raise an Exception on error
    // - this default implementation will release all cached statements: so it
    // should be called in overridden methods BEFORE actual disconnection
    procedure Disconnect; virtual;
    /// return TRUE if Connect has been already successfully called
    function IsConnected: boolean; virtual; abstract;
    /// initialize a new SQL query statement for the given connection
    // - the caller should free the instance after use
    function NewStatement: TSQLDBStatement; virtual; abstract;
    /// initialize a new SQL query statement for the given connection
    // - this default implementation will call the NewStatement method, and
    // implement handle statement caching is UseCache=true - in this case,
    // the TSQLDBStatement.Reset method shall have been overridden to allow
    // binding and execution of the very same prepared statement
    // - the same aSQL can cache up to 9 statements in this TSQLDBConnection
    // - this method should return a prepared statement instance on success
    // - on error, if RaiseExceptionOnError=false (by default), it returns nil
    // and you can check LastErrorMessage and LastErrorException properties to
    // retrieve corresponding error information
    // - if TSQLDBConnectionProperties.ReconnectAfterConnectionError is set,
    // any connection error will be trapped, unless AllowReconnect is false
    // - on error, if RaiseExceptionOnError=true, an exception is raised
    function NewStatementPrepared(const aSQL: RawUTF8; ExpectResults: Boolean;
      RaiseExceptionOnError: Boolean=false; AllowReconnect: Boolean=true): ISQLDBStatement; virtual;
    /// begin a Transaction for this connection
    // - this default implementation will check and set TransactionCount
    procedure StartTransaction; virtual;
    /// commit changes of a Transaction for this connection
    // - StartTransaction method must have been called before
    // - this default implementation will check and set TransactionCount
    procedure Commit; virtual;
    /// discard changes of a Transaction for this connection
    // - StartTransaction method must have been called before
    // - this default implementation will check and set TransactionCount
    procedure Rollback; virtual;

    /// direct export of a DB statement rows into a new table of this database
    // - the corresponding table will be created within the current connection,
    // if it does not exist
    // - if the column types are not set, they will be identified from the
    // first row of data
    // - INSERTs will be nested within a transaction if WithinTransaction is TRUE
    // - will raise an Exception in case of error
    function NewTableFromRows(const TableName: RawUTF8;
      Rows: TSQLDBStatement; WithinTransaction: boolean;
      ColumnForcedTypes: TSQLDBFieldTypeDynArray=nil): integer;
    {$ifdef WITH_PROXY}
    /// server-side implementation of a remote connection to any SynDB engine
    // - follow the compressed binary message format expected by the
    // TSQLDBRemoteConnectionPropertiesAbstract.ProcessMessage method
    // - any transmission protocol could call this method to execute the
    // corresponding TSQLDBProxyConnectionCommand on the current connection
    procedure RemoteProcessMessage(const Input: RawByteString;
      out Output: RawByteString; Protocol: TSQLDBProxyConnectionProtocol); virtual;
    {$endif WITH_PROXY}

    /// the current Date and Time, as retrieved from the server
    // - note that this value is the DB_SERVERTIME[] constant SQL value, so
    // will most likely return a local time, not an UTC time
    // - this property will return the timestamp in TTimeLog / TTimeLogBits /
    // Int64 value
    property ServerTimestamp: TTimeLog read GetServerTimestamp;
    /// the current Date and Time, as retrieved from the server
    // - note that this value is the DB_SERVERTIME[] constant SQL value, so
    // will most likely return a local time, not an UTC time
    // - this property will return the value as regular TDateTime
    property ServerDateTime: TDateTime read GetServerDateTime;
    /// this event handler will be called during all process
    // - can be used e.g. to change the desktop cursor
    // - by default, will follow TSQLDBConnectionProperties.OnProcess property
    property OnProcess: TOnSQLDBProcess read fOnProcess write fOnProcess;
  published { to be logged as JSON }
    /// returns TRUE if the connection was set
    property Connected: boolean read IsConnected;
    /// the time returned by the server when the connection occurred
    property ServerTimestampAtConnection: TDateTime read fServerTimestampAtConnection;
    /// number of sucessfull connections for this instance
    // - can be greater than 1 in case of re-connection via Disconnect/Connect
    property TotalConnectionCount: integer read fTotalConnectionCount;
    /// number of nested StartTransaction calls
    // - equals 0 if no transaction is active
    property TransactionCount: integer read fTransactionCount;
    /// TRUE if StartTransaction has been called
    // - check if TransactionCount>0
    property InTransaction: boolean read GetInTransaction;
    /// defines if Disconnect shall Rollback any pending transaction
    // - some engines executes a COMMIT when the client is disconnected, others
    // do raise an exception: this parameter ensures that any pending transaction
    // is roll-backed before disconnection
    // - is set to TRUE by default
    property RollbackOnDisconnect: Boolean
      read fRollbackOnDisconnect write fRollbackOnDisconnect;
    /// some error message, e.g. during execution of NewStatementPrepared
    property LastErrorMessage: RawUTF8 read fErrorMessage write fErrorMessage;
    /// some error exception, e.g. during execution of NewStatementPrepared
    property LastErrorException: ExceptClass read fErrorException;
    /// TRUE if last error is a broken connection, e.g. during execution of
    // NewStatementPrepared
    // - i.e. LastErrorException/LastErrorMessage concerns the database connection
    // - will use TSQLDBConnectionProperties.ExceptionIsAboutConnection virtual method
    property LastErrorWasAboutConnection: boolean read GetLastErrorWasAboutConnection;
    /// the associated database properties
    property Properties: TSQLDBConnectionProperties read fProperties;
  end;

  /// generic abstract class to implement a prepared SQL query
  // - inherited classes should implement the DB-specific connection in its
  // overridden methods, especially Bind*(), Prepare(), ExecutePrepared, Step()
  // and Column*() methods
  TSQLDBStatement = class(TInterfacedObject, ISQLDBRows, ISQLDBStatement)
  protected
    fStripSemicolon: boolean;
    fConnection: TSQLDBConnection;
    fSQL: RawUTF8;
    fExpectResults: boolean;
    fParamCount: integer;
    fColumnCount: integer;
    fTotalRowsRetrieved: Integer;
    fCurrentRow: Integer;
    fForceBlobAsNull: boolean;
    fForceDateWithMS: boolean;
    fDBMS: TSQLDBDefinition;
    {$ifndef SYNDB_SILENCE}
    fSQLLogLog: TSynLog;
    fSQLLogLevel: TSynLogInfo;
    {$endif}
    fSQLWithInlinedParams: RawUTF8;
    fSQLLogTimer: TPrecisionTimer;
    fCacheIndex: integer;
    fSQLPrepared: RawUTF8;
    function GetSQLCurrent: RawUTF8;
    function GetSQLWithInlinedParams: RawUTF8;
    procedure ComputeSQLWithInlinedParams;
    function GetForceBlobAsNull: boolean;
    procedure SetForceBlobAsNull(value: boolean);
    function GetForceDateWithMS: boolean;
    procedure SetForceDateWithMS(value: boolean);
    /// raise an exception if Col is out of range according to fColumnCount
    procedure CheckCol(Col: integer); {$ifdef HASINLINE}inline;{$endif}
    /// will set a Int64/Double/Currency/TDateTime/RawUTF8/TBlobData Dest variable
    // from a given column value
    // - internal conversion will use a temporary Variant and ColumnToVariant method
    // - expects Dest to be of the exact type (e.g. Int64, not Integer)
    function ColumnToTypedValue(Col: integer; DestType: TSQLDBFieldType; var Dest): TSQLDBFieldType;
    /// append the inlined value of a given parameter, mainly for GetSQLWithInlinedParams
    // - optional MaxCharCount will truncate the text to a given number of chars
    procedure AddParamValueAsText(Param: integer; Dest: TTextWriter; MaxCharCount: integer); virtual;
    {$ifndef LVCL}
    /// return a Column as a variant
    function GetColumnVariant(const ColName: RawUTF8): Variant;
    {$endif}
    /// return the associated statement instance for a ISQLDBRows interface
    function Instance: TSQLDBStatement;
    /// wrappers to compute sllSQL/sllDB SQL context with a local timer
    function SQLLogBegin(level: TSynLogInfo): TSynLog;
    function SQLLogEnd(const Fmt: RawUTF8; const Args: array of const): Int64; overload;
    function SQLLogEnd(msg: PShortString=nil): Int64; overload;
  public
    /// create a statement instance
    constructor Create(aConnection: TSQLDBConnection); virtual;

    /// bind a NULL value to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - some providers (e.g. OleDB during MULTI INSERT statements) expect the
    // proper column type to be set in BoundType, even for NULL values
    procedure BindNull(Param: Integer; IO: TSQLDBParamInOutType=paramIn;
      BoundType: TSQLDBFieldType=ftNull); virtual; abstract;
    /// bind an integer value to a parameter
    // - the leftmost SQL parameter has an index of 1
    procedure Bind(Param: Integer; Value: Int64;
      IO: TSQLDBParamInOutType=paramIn); overload; virtual; abstract;
    /// bind a double value to a parameter
    // - the leftmost SQL parameter has an index of 1
    procedure Bind(Param: Integer; Value: double;
      IO: TSQLDBParamInOutType=paramIn); overload; virtual; abstract;
    /// bind a TDateTime value to a parameter
    // - the leftmost SQL parameter has an index of 1
    procedure BindDateTime(Param: Integer; Value: TDateTime;
      IO: TSQLDBParamInOutType=paramIn); overload; virtual; abstract;
    /// bind a currency value to a parameter
    // - the leftmost SQL parameter has an index of 1
    procedure BindCurrency(Param: Integer; Value: currency;
      IO: TSQLDBParamInOutType=paramIn); overload; virtual; abstract;
    /// bind a UTF-8 encoded string to a parameter
    // - the leftmost SQL parameter has an index of 1
    procedure BindTextU(Param: Integer; const Value: RawUTF8;
      IO: TSQLDBParamInOutType=paramIn); overload; virtual; abstract;
    /// bind a UTF-8 encoded buffer text (#0 ended) to a parameter
    // - the leftmost SQL parameter has an index of 1
    procedure BindTextP(Param: Integer; Value: PUTF8Char;
      IO: TSQLDBParamInOutType=paramIn); overload; virtual; abstract;
    /// bind a UTF-8 encoded string to a parameter
    // - the leftmost SQL parameter has an index of 1
    procedure BindTextS(Param: Integer; const Value: string;
      IO: TSQLDBParamInOutType=paramIn); overload; virtual; abstract;
    /// bind a UTF-8 encoded string to a parameter
    // - the leftmost SQL parameter has an index of 1
    procedure BindTextW(Param: Integer; const Value: WideString;
      IO: TSQLDBParamInOutType=paramIn); overload; virtual; abstract;
    /// bind a Blob buffer to a parameter
    // - the leftmost SQL parameter has an index of 1
    procedure BindBlob(Param: Integer; Data: pointer; Size: integer;
      IO: TSQLDBParamInOutType=paramIn); overload; virtual; abstract;
    /// bind a Blob buffer to a parameter
    // - the leftmost SQL parameter has an index of 1
    procedure BindBlob(Param: Integer; const Data: RawByteString;
      IO: TSQLDBParamInOutType=paramIn); overload; virtual; abstract;
    /// bind a Variant value to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - will call all virtual Bind*() methods from the Data type
    // - if DataIsBlob is TRUE, will call BindBlob(RawByteString(Data)) instead
    // of BindTextW(WideString(Variant)) - used e.g. by TQuery.AsBlob/AsBytes
    procedure BindVariant(Param: Integer; const Data: Variant; DataIsBlob: boolean;
      IO: TSQLDBParamInOutType=paramIn); virtual;
    /// bind one TSQLVar value
    // - the leftmost SQL parameter has an index of 1
    // - this default implementation will call corresponding Bind*() method
    procedure Bind(Param: Integer; const Data: TSQLVar;
      IO: TSQLDBParamInOutType=paramIn); overload; virtual;
    /// bind one RawUTF8 encoded value
    // - the leftmost SQL parameter has an index of 1
    // - the value should match the BindArray() format, i.e. be stored as in SQL
    // (i.e. number, 'quoted string', 'YYYY-MM-DD hh:mm:ss', null) - e.g. as
    // computed by TJSONObjectDecoder.Decode()
    procedure Bind(Param: Integer; ParamType: TSQLDBFieldType; const Value: RawUTF8;
      ValueAlreadyUnquoted: boolean; IO: TSQLDBParamInOutType=paramIn); overload; virtual;
    /// bind an array of const values
    // - parameters marked as ? should be specified as method parameter in Params[]
    // - BLOB parameters can be bound with this method, when set after encoding
    // via BinToBase64WithMagic() call
    // - TDateTime parameters can be bound with this method, when encoded via
    // a DateToSQL() or DateTimeToSQL() call
    // - any variant parameter will be bound with BindVariant(i,VVariant^,true,IO)
    // i.e. with DataIsBlob=true
    // - this default implementation will call corresponding Bind*() method
    procedure Bind(const Params: array of const;
      IO: TSQLDBParamInOutType=paramIn); overload; virtual;
    /// bind an array of fields from an existing SQL statement
    // - can be used e.g. after ColumnsToSQLInsert() method call for fast data
    // conversion between tables
    procedure BindFromRows(const Fields: TSQLDBFieldTypeDynArray;
      Rows: TSQLDBStatement);
    /// bind a special CURSOR parameter to be returned as a SynDB result set
    // - Cursors are not handled internally by mORMot, but some databases (e.g.
    // Oracle) usually use such structures to get data from strored procedures
    // - such parameters are mapped as ftUnknown
    // - use BoundCursor() method to retrieve the corresponding ISQLDBRows after
    // execution of the statement
    // - this default method will raise an exception about unexpected behavior
    procedure BindCursor(Param: integer); virtual;
    /// return a special CURSOR parameter content as a SynDB result set
    // - this method is not about a column, but a parameter defined with
    // BindCursor() before method execution
    // - Cursors are not handled internally by mORMot, but some databases (e.g.
    // Oracle) usually use such structures to get data from strored procedures
    // - this method allow direct access to the data rows after execution
    // - this default method will raise an exception about unexpected behavior
    function BoundCursor(Param: Integer): ISQLDBRows; virtual;

    /// bind an array of values to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - values are stored as in SQL (i.e. number, 'quoted string',
    // 'YYYY-MM-DD hh:mm:ss', null)
    // - this default implementation will raise an exception if the engine
    // does not support array binding
    procedure BindArray(Param: Integer; ParamType: TSQLDBFieldType;
      const Values: TRawUTF8DynArray; ValuesCount: integer); overload; virtual;
    /// bind an array of integer values to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - this default implementation will raise an exception if the engine
    // does not support array binding
    procedure BindArray(Param: Integer; const Values: array of Int64); overload; virtual;
    /// bind an array of double values to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - this default implementation will raise an exception if the engine
    // does not support array binding
    procedure BindArray(Param: Integer; const Values: array of double); overload; virtual;
    /// bind an array of TDateTime values to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - values are stored as in SQL (i.e. 'YYYY-MM-DD hh:mm:ss')
    // - this default implementation will raise an exception if the engine
    // does not support array binding
    procedure BindArrayDateTime(Param: Integer; const Values: array of TDateTime); virtual;
    /// bind an array of currency values to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - this default implementation will raise an exception if the engine
    // does not support array binding
    procedure BindArrayCurrency(Param: Integer; const Values: array of currency); virtual;
    /// bind an array of RawUTF8 values to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - values are stored as in SQL (i.e. 'quoted string')
    // - this default implementation will raise an exception if the engine
    // does not support array binding
    procedure BindArray(Param: Integer; const Values: array of RawUTF8); overload; virtual;

    /// Prepare an UTF-8 encoded SQL statement
    // - parameters marked as ? will be bound later, before ExecutePrepared call
    // - if ExpectResults is TRUE, then Step() and Column*() methods are available
    // to retrieve the data rows
    // - should raise an Exception on any error
    // - this default implementation will just store aSQL content and the
    // ExpectResults parameter, and connect to the remote server is was not
    // already connected
    procedure Prepare(const aSQL: RawUTF8; ExpectResults: Boolean); overload; virtual;
    /// Execute a prepared SQL statement
    // - parameters marked as ? should have been already bound with Bind*() functions
    // - should raise an Exception on any error
    // - this void default implementation will call set fConnection.fLastAccess
    procedure ExecutePrepared; virtual;
    /// release cursor memory and resources once Step loop is finished
    // - this method call is optional, but is better be used if the ISQLDBRows
    // statement from taken from cache, and returned a lot of content which
    // may still be in client (and server) memory
    // - override to free cursor memory when ISQLDBStatement is back in cache
    procedure ReleaseRows; virtual;
    /// Reset the previous prepared statement
    // - some drivers expect an explicit reset before binding parameters and
    // executing the statement another time
    // - this default implementation will just do nothing
    procedure Reset; virtual;
    /// Prepare and Execute an UTF-8 encoded SQL statement
    // - parameters marked as ? should have been already bound with Bind*()
    //  functions above
    // - if ExpectResults is TRUE, then Step() and Column*() methods are available
    //  to retrieve the data rows
    // - should raise an Exception on any error
    // - this method will call Prepare then ExecutePrepared methods
    procedure Execute(const aSQL: RawUTF8; ExpectResults: Boolean); overload;
    /// Prepare and Execute an UTF-8 encoded SQL statement
    // - parameters marked as ? should be specified as method parameter in Params[]
    // - BLOB parameters could not be bound with this method, but need an explicit
    // call to BindBlob() method
    // - if ExpectResults is TRUE, then Step() and Column*() methods are available
    // to retrieve the data rows
    // - should raise an Exception on any error
    // - this method will bind parameters, then call Excecute() virtual method
    procedure Execute(const aSQL: RawUTF8; ExpectResults: Boolean;
      const Params: array of const); overload;
    /// Prepare and Execute an UTF-8 encoded SQL statement
    // - parameters marked as % will be replaced by Args[] value in the SQL text
    // - parameters marked as ? should be specified as method parameter in Params[]
    // - so could be used as such, mixing both % and ? parameters:
    // ! Statement.Execute('SELECT % FROM % WHERE RowID=?',true,[FieldName,TableName],[ID])
    // - BLOB parameters could not be bound with this method, but need an explicit
    // call to BindBlob() method
    // - if ExpectResults is TRUE, then Step() and Column*() methods are available
    // to retrieve the data rows
    // - should raise an Exception on any error
    // - this method will bind parameters, then call Excecute() virtual method
    procedure Execute(const SQLFormat: RawUTF8; ExpectResults: Boolean;
      const Args, Params: array of const); overload;
    /// execute a prepared SQL statement and return all rows content as a JSON string
    // - JSON data is retrieved with UTF-8 encoding
    // - if Expanded 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 Expanded is false, JSON data is serialized (used in TSQLTableJSON)
    // & { "FieldCount":1,"Values":["col1","col2",val11,"val12",val21,..] }
    // - BLOB field value is saved as Base64, in the '"\uFFF0base64encodedbinary"'
    // format and contains true BLOB data
    // - this virtual implementation calls ExecutePrepared then FetchAllAsJSON()
    procedure ExecutePreparedAndFetchAllAsJSON(Expanded: boolean; out JSON: RawUTF8); virtual;
    /// gets a number of updates made by latest executed statement
    // - default implementation returns 0
    function UpdateCount: integer; virtual;
    {$ifndef LVCL}
    /// retrieve the parameter content, after SQL execution
    // - the leftmost SQL parameter has an index of 1
    // - to be used e.g. with stored procedures:
    // ! query :=  'BEGIN TEST_PKG.DUMMY(?, ?, ?, ?, ?); END;';
    // ! stmt := Props.NewThreadSafeStatementPrepared(query, false);
    // ! stmt.Bind(1, in1, paramIn);
    // ! stmt.BindTextU(2, in2, paramIn);
    // ! stmt.BindTextU(3, in3, paramIn);
    // ! stmt.BindTextS(4, '', paramOut); //  to be retrieved with out1: string
    // ! stmt.Bind(5, 0, paramOut);       //  to be retrieved with out2: integer
    // ! stmt.ExecutePrepared;
    // ! stmt.ParamToVariant(4, out1, true);
    // ! stmt.ParamToVariant(5, out2, true);
    // - the parameter should have been bound with IO=paramOut or IO=paramInOut
    // if CheckIsOutParameter is TRUE
    // - this implementation just check that Param is correct: overridden method
    // should fill Value content
    function ParamToVariant(Param: Integer; var Value: Variant;
      CheckIsOutParameter: boolean=true): TSQLDBFieldType; virtual;
    {$endif}

    /// After a statement has been prepared via Prepare() + ExecutePrepared() or
    // Execute(), this method must be called one or more times to evaluate it
    // - you shall call this method before calling any Column*() methods
    // - return TRUE on success, with data ready to be retrieved by Column*()
    // - return FALSE if no more row is available (e.g. if the SQL statement
    // is not a SELECT but an UPDATE or INSERT command)
    // - access the first or next row of data from the SQL Statement result:
    // 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
    // - should raise an Exception on any error
    // - typical use may be (see also e.g. the mORMotDB unit):
    // ! var Query: ISQLDBStatement;
    // ! begin
    // !   Query := Props.NewThreadSafeStatementPrepared('select AccountNumber from Sales.Customer where AccountNumber like ?', ['AW000001%'],true);
    // !   if Query<>nil then begin
    // !     assert(SameTextU(Query.ColumnName(0),'AccountNumber'));
    // !     while Query.Step do //  loop through all matching data rows
    // !       assert(Copy(Query.ColumnUTF8(0),1,8)='AW000001');
    // !     Query.ReleaseRows;
    // !   end;
    // ! end;
    function Step(SeekFirst: boolean=false): boolean; virtual; abstract;
    /// the column/field count of the current Row
    function ColumnCount: integer;
    /// the Column name of the current Row
    // - Columns numeration (i.e. Col value) starts with 0
    // - it's up to the implementation to ensure than all column names are unique
    function ColumnName(Col: integer): RawUTF8; virtual; abstract;
    /// returns the Column index of a given Column name
    // - Columns numeration (i.e. Col value) starts with 0
    // - returns -1 if the Column name is not found (via case insensitive search)
    function ColumnIndex(const aColumnName: RawUTF8): integer; virtual; abstract;
    /// the Column type of the current Row
    // - FieldSize can be set to store the size in chars of a ftUTF8 column
    // (0 means BLOB kind of TEXT column)
    function ColumnType(Col: integer; FieldSize: PInteger=nil): TSQLDBFieldType; virtual; abstract;
    /// returns TRUE if the column contains NULL
    function ColumnNull(Col: integer): boolean; virtual; abstract;
    /// return a Column integer value of the current Row, first Col is 0
    function ColumnInt(Col: integer): Int64; overload; virtual; abstract;
    /// return a Column floating point value of the current Row, first Col is 0
    function ColumnDouble(Col: integer): double; overload; virtual; abstract;
    /// return a Column date and time value of the current Row, first Col is 0
    function ColumnDateTime(Col: integer): TDateTime; overload; virtual; abstract;
    /// return a column date and time value of the current Row, first Col is 0
    // - call ColumnDateTime or ColumnUTF8 to convert into TTimeLogBits/Int64 time
    // stamp from a TDateTime or text
    function ColumnTimestamp(Col: integer): TTimeLog; overload;
    /// return a Column currency value of the current Row, first Col is 0
    function ColumnCurrency(Col: integer): currency; overload; virtual; abstract;
    /// return a Column UTF-8 encoded text value of the current Row, first Col is 0
    function ColumnUTF8(Col: integer): RawUTF8; overload; virtual; abstract;
    /// return a Column text value as generic VCL string of the current Row, first Col is 0
    // - this default implementation will call ColumnUTF8
    function ColumnString(Col: integer): string; overload; virtual;
    /// return a Column as a blob value of the current Row, first Col is 0
    function ColumnBlob(Col: integer): RawByteString; overload; virtual; abstract;
    /// return a Column as a blob value of the current Row, first Col is 0
    // - this function will return the BLOB content as a TBytes
    // - this default virtual method will call ColumnBlob()
    function ColumnBlobBytes(Col: integer): TBytes; overload; virtual;
    /// read a blob Column into the Stream parameter
    // - default implementation will just call ColumnBlob(), whereas some
    // providers (like SynDBOracle) may implement direct support
    procedure ColumnBlobToStream(Col: integer; Stream: TStream); overload; virtual;
    /// write a blob Column into the Stream parameter
    // - expected to be used with 'SELECT .. FOR UPDATE' locking statements
    // - default implementation will through an exception, since it is highly
    // provider-specific; SynDBOracle e.g. implements it properly
    procedure ColumnBlobFromStream(Col: integer; Stream: TStream); overload; virtual;
    {$ifndef LVCL}
    /// return a Column as a variant, first Col is 0
    // - this default implementation will call ColumnToVariant() method
    // - a ftUTF8 TEXT content will be mapped into a generic WideString variant
    // for pre-Unicode version of Delphi, and a generic UnicodeString (=string)
    // since Delphi 2009: you may not loose any data during charset conversion
    // - a ftBlob BLOB content will be mapped into a TBlobData AnsiString variant
    function ColumnVariant(Col: integer): Variant; overload;
    /// return a Column as a variant, first Col is 0
    // - this default implementation will call Column*() method above
    // - a ftUTF8 TEXT content will be mapped into a generic WideString variant
    // for pre-Unicode version of Delphi, and a generic UnicodeString (=string)
    // since Delphi 2009: you may not loose any data during charset conversion
    // - a ftBlob BLOB content will be mapped into a TBlobData AnsiString variant
    function ColumnToVariant(Col: integer; var Value: Variant): TSQLDBFieldType; virtual;
    {$endif}
    /// return a Column as a TSQLVar value, first Col is 0
    // - the specified Temp variable will be used for temporary storage of
    // svtUTF8/svtBlob values
    procedure ColumnToSQLVar(Col: Integer; var Value: TSQLVar;
      var Temp: RawByteString); virtual;
    /// return a special CURSOR Column content as a SynDB result set
    // - Cursors are not handled internally by mORMot, but some databases (e.g.
    // Oracle) usually use such structures to get data from strored procedures
    // - such columns are mapped as ftNull internally - so this method is the only
    // one giving access to the data rows
    // - this default method will raise an exception about unexpected behavior
    function ColumnCursor(Col: integer): ISQLDBRows; overload; virtual;
    /// return a Column integer value of the current Row, from a supplied column name
    function ColumnInt(const ColName: RawUTF8): Int64; overload;
    /// return a Column floating point value of the current Row, from a supplied column name
    function ColumnDouble(const ColName: RawUTF8): double; overload;
    /// return a Column date and time value of the current Row, from a supplied column name
    function ColumnDateTime(const ColName: RawUTF8): TDateTime; overload;
    /// return a column date and time value of the current Row, from a supplied column name
    // - call ColumnDateTime or ColumnUTF8 to convert into TTimeLogBits/Int64 time
    // stamp from a TDateTime or text
    function ColumnTimestamp(const ColName: RawUTF8): TTimeLog; overload;
    /// return a Column currency value of the current Row, from a supplied column name
    function ColumnCurrency(const ColName: RawUTF8): currency; overload;
    /// return a Column UTF-8 encoded text value of the current Row, from a supplied column name
    function ColumnUTF8(const ColName: RawUTF8): RawUTF8; overload;
    /// return a Column text value as generic VCL string of the current Row, from a supplied column name
    function ColumnString(const ColName: RawUTF8): string; overload;
    /// return a Column as a blob value of the current Row, from a supplied column name
    function ColumnBlob(const ColName: RawUTF8): RawByteString; overload;
    /// return a Column as a blob value of the current Row, from a supplied column name
    function ColumnBlobBytes(const ColName: RawUTF8): TBytes; overload;
    /// read a blob Column into the Stream parameter
    procedure ColumnBlobToStream(const ColName: RawUTF8; Stream: TStream); overload;
    /// write a blob Column into the Stream parameter
    // - expected to be used with 'SELECT .. FOR UPDATE' locking statements
    procedure ColumnBlobFromStream(const ColName: RawUTF8; Stream: TStream); overload;
    {$ifndef LVCL}
    /// return a Column as a variant, from a supplied column name
    function ColumnVariant(const ColName: RawUTF8): Variant; overload;
    {$ifndef DELPHI5OROLDER}
    /// create a TSQLDBRowVariantType able to access any field content via late binding
    // - i.e. you can use Data.Name to access the 'Name' column of the current row
    // - this Variant will point to the corresponding TSQLDBStatement instance,
    // so it's not necessary to retrieve its value for each row
    // - typical use is:
    // ! var Row: Variant;
    // ! (...)
    // !  with MyConnProps.Execute('select * from table where name=?',[aName]) do begin
    // !    Row := RowDaa;
    // !    while Step do
    // !      writeln(Row.FirstName,Row.BirthDate);
    // !    ReleaseRows;
    // !  end;
    function RowData: Variant; virtual;
    /// create a TDocVariant custom variant containing all columns values
    // - will create a "fast" TDocVariant object instance with all fields
    procedure RowDocVariant(out aDocument: variant;
      aOptions: TDocVariantOptions=JSON_OPTIONS_FAST); virtual;
    {$endif}
    {$endif}
    /// return a special CURSOR Column content as a SynDB result set
    // - Cursors are not handled internally by mORMot, but some databases (e.g.
    // Oracle) usually use such structures to get data from strored procedures
    // - such columns are mapped as ftNull internally - so this method is the only
    // one giving access to the data rows
    // - this default method will raise an exception about unexpected behavior
    function ColumnCursor(const ColName: RawUTF8): ISQLDBRows; overload;
    /// append all columns values of the current Row to a JSON stream
    // - will use WR.Expand to guess the expected output format
    // - this default implementation will call Column*() methods above, but you
    // should also implement a custom version with no temporary variable
    // - BLOB field value is saved as Base64, in the '"\uFFF0base64encodedbinary"
    // format and contains true BLOB data (unless ForceBlobAsNull property was set)
    procedure ColumnsToJSON(WR: TJSONWriter); virtual;
    /// compute the SQL INSERT statement corresponding to this columns row
    // - and populate the Fields[] array with columns information (type and name)
    // - if the current column value is NULL, will return ftNull: it is up to the
    // caller to set the proper field type
    // - the SQL statement is prepared with bound parameters, e.g.
    // $ insert into TableName (Col1,Col2) values (?,N)
    // - used e.g. to convert some data on the fly from one database to another,
    // via the TSQLDBConnection.NewTableFromRows method
    function ColumnsToSQLInsert(const TableName: RawUTF8;
      var Fields: TSQLDBColumnCreateDynArray): RawUTF8; virtual;
    // append all rows content as a JSON stream
    // - JSON data is added to the supplied TStream, with UTF-8 encoding
    // - if Expanded 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 Expanded is false, JSON data is serialized (used in TSQLTableJSON)
    // & { "FieldCount":1,"Values":["col1","col2",val11,"val12",val21,..] }
    // - BLOB field value is saved as Base64, in the '"\uFFF0base64encodedbinary"'
    // format and contains true BLOB data
    // - similar to corresponding TSQLRequest.Execute method in SynSQLite3 unit
    // - returns the number of row data returned (excluding field names)
    // - warning: TSQLRestStorageExternal.EngineRetrieve in mORMotDB unit
    // expects the Expanded=true format to return '[{...}]'#10
    function FetchAllToJSON(JSON: TStream; Expanded: boolean): PtrInt;
    // Append all rows content as a CSV stream
    // - CSV data is added to the supplied TStream, with UTF-8 encoding
    // - 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
    // - BLOB fields will be appended as "blob" with no data
    // - returns the number of row data returned
    function FetchAllToCSVValues(Dest: TStream; Tab: boolean; CommaSep: AnsiChar=',';
      AddBOM: boolean=true): PtrInt;
    // return all rows content as a JSON string
    // - JSON data is retrieved with UTF-8 encoding
    // - if Expanded 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 Expanded is false, JSON data is serialized (used in TSQLTableJSON)
    // & { "FieldCount":1,"Values":["col1","col2",val11,"val12",val21,..] }
    // - BLOB field value is saved as Base64, in the '"\uFFF0base64encodedbinary"'
    // format and contains true BLOB data
    // - if ReturnedRowCount points to an integer variable, it will be filled with
    // the number of row data returned (excluding field names)
    // - similar to corresponding TSQLRequest.Execute method in SynSQLite3 unit
    function FetchAllAsJSON(Expanded: boolean; ReturnedRowCount: PPtrInt=nil): RawUTF8;
    /// append all rows content as binary stream
    // - will save the column types and name, then every data row in optimized
    // binary format (faster and smaller than JSON)
    // - you can specify a LIMIT for the data extent (default 0 meaning all data)
    // - generates the format expected by TSQLDBProxyStatement
    function FetchAllToBinary(Dest: TStream; MaxRowCount: cardinal=0;
      DataRowPosition: PCardinalDynArray=nil): cardinal; virtual;
    /// append current row content as binary stream
    // - will save one data row in optimized binary format (if not in Null)
    // - virtual method called by FetchAllToBinary()
    // - follows the format expected by TSQLDBProxyStatement
    procedure ColumnsToBinary(W: TFileBufferWriter;
      Null: pointer; const ColTypes: TSQLDBFieldTypeDynArray); virtual;
    /// low-level access to the Timer used for last DB operation
    property SQLLogTimer: TPrecisionTimer read fSQLLogTimer;
    /// after a call to Prepare(), contains the query text to be passed to the DB
    // - depending on the DB, parameters placeholders are replaced by ?, :1, $1 etc
    // - this SQL is ready to be used in any DB tool, e.g. to check the real 
    // execution plan/timing
    property SQLPrepared: RawUTF8 read fSQLPrepared;
    /// the prepared SQL statement, in its current state
    // - if statement is prepared, then equals SQLPrepared, otherwise, contains
    // the raw SQL property content
    // - used internally by the implementation units, e.g. for errors logging
    property SQLCurrent: RawUTF8 read GetSQLCurrent;
    /// low-level access to the statement cache index, after a call to Prepare()
    // - contains >= 0 if the database supports prepared statement cache 
    //(Oracle, Postgres) and query plan is cached; contains -1 in other cases
    property CacheIndex: integer read fCacheIndex;
  published
    /// the prepared SQL statement, as supplied to Prepare() method
    property SQL: RawUTF8 read fSQL;
    /// the prepared SQL statement, with all '?' changed into the supplied
    // parameter values
    // - such statement query plan usually differ from a real execution plan
    // for prepared statements with parameters - see SQLPrepared property instead
    property SQLWithInlinedParams: RawUTF8 read GetSQLWithInlinedParams;
    /// the current row after Execute/Step call, corresponding to Column*() methods
    // - contains 0 before initial Step call, or a number >=1 during data retrieval
    property CurrentRow: Integer read fCurrentRow;
    /// the total number of data rows retrieved by this instance
    // - is not reset when there is no more row of available data (Step returns
    // false), or when Step() is called with SeekFirst=true
    property TotalRowsRetrieved: Integer read fTotalRowsRetrieved;
    /// the associated database connection
    property Connection: TSQLDBConnection read fConnection;
    /// strip last semicolon in query
    // - expectation may vary, depending on the SQL statement and the engine
    // - default is true
    property StripSemicolon: boolean read fStripSemicolon write fStripSemicolon;
  end;

  /// abstract connection created from TSQLDBConnectionProperties
  // - this overridden class will defined an hidden thread ID, to ensure
  // that one connection will be create per thread
  // - e.g. OleDB, ODBC and Oracle connections will inherit from this class
  TSQLDBConnectionThreadSafe = class(TSQLDBConnection)
  protected
    fThreadID: TThreadID;
  end;

  /// threading modes set to TSQLDBConnectionPropertiesThreadSafe.ThreadingMode
  // - default mode is to use a Thread Pool, i.e. one connection per thread
  // - or you can force to use the main connection
  // - or you can use a shared background thread process (not implemented yet)
  // - last two modes could be used for embedded databases (SQLite3/FireBird),
  // when multiple connections may break stability, consume too much resources
  // and/or decrease performance
  TSQLDBConnectionPropertiesThreadSafeThreadingMode = (
    tmThreadPool,
    tmMainConnection,
    tmBackgroundThread);

  /// connection properties which will implement an internal Thread-Safe
  // connection pool
  TSQLDBConnectionPropertiesThreadSafe = class(TSQLDBConnectionProperties)
  protected
    fConnectionPool: TSynObjectListLocked;
    fLatestConnectionRetrievedInPool: integer;
    fThreadingMode: TSQLDBConnectionPropertiesThreadSafeThreadingMode;
    /// returns -1 if none was defined yet
    function CurrentThreadConnectionIndex: Integer;
    /// overridden method to properly handle multi-thread
    function GetMainConnection: TSQLDBConnection; override;
  public
    /// initialize the properties
    // - this overridden method will initialize the internal per-thread connection pool
    constructor Create(const aServerName, aDatabaseName, aUserID, aPassWord: RawUTF8); override;
    /// release related memory, and all per-thread connections
    destructor Destroy; override;
    /// get a thread-safe connection
    // - this overridden implementation will define a per-thread TSQLDBConnection
    // connection pool, via an internal  pool
    function ThreadSafeConnection: TSQLDBConnection; override;
    /// release all existing connections
    // - this overridden implementation will release all per-thread
    // TSQLDBConnection internal connection pool
    // - warning: no connection shall still be used on the background (e.g. in
    // multi-threaded applications), or some unexpected border effects may occur
    procedure ClearConnectionPool; override;
    /// you can call this method just before a thread is finished to ensure
    // that the associated Connection will be released
    // - could be used e.g. in a try...finally block inside a TThread.Execute
    // overridden method
    // - could be used e.g. to call CoUnInitialize from thread in which
    // CoInitialize was made, for instance via a method defined as such:
    // ! procedure TMyServer.OnHttpThreadTerminate(Sender: TObject);
    // ! begin
    // !   fMyConnectionProps.EndCurrentThread;
    // ! end;
    // - 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
    // - within the mORMot server, mORMotDB unit will call this method
    // for every terminating thread created for TSQLRestServerNamedPipeResponse
    // or TSQLHttpServer multi-thread process
    procedure EndCurrentThread; virtual;
    /// set this property if you want to disable the per-thread connection pool
    // - to be used e.g. in database embedded mode (SQLite3/FireBird), when
    // multiple connections may break stability and decrease performance
    // - see TSQLDBConnectionPropertiesThreadSafeThreadingMode for the
    // possible values
    property ThreadingMode: TSQLDBConnectionPropertiesThreadSafeThreadingMode
      read fThreadingMode write fThreadingMode;
  end;

  /// a structure used to store a standard binding parameter
  // - you can use your own internal representation of parameters
  // (TOleDBStatement use its own TOleDBStatementParam type), but
  // this type can be used to implement a generic parameter
  // - used e.g. by TSQLDBStatementWithParams as a dynamic array
  // (and its inherited TSQLDBOracleStatement)
  // - don't change this structure, since it will be serialized as binary
  // for TSQLDBProxyConnectionCommandExecute
  TSQLDBParam = packed record
    /// storage used for TEXT (ftUTF8) and BLOB (ftBlob) values
    // - ftBlob are stored as RawByteString
    // - ftUTF8 are stored as RawUTF8
    // - sometimes, may be ftInt64 or ftCurrency provided as SQLT_AVC text,
    // or ftDate value converted to SQLT_TIMESTAMP
    VData: RawByteString;
    /// storage used for bound array values
    // - number of items in array is stored in VInt64
    // - values are stored as in SQL (i.e. number, 'quoted string',
    // 'YYYY-MM-DD hh:mm:ss', null)
    VArray: TRawUTF8DynArray;
    /// the column/parameter Value type
    VType: TSQLDBFieldType;
    /// define if parameter can be retrieved after a stored procedure execution
    VInOut: TSQLDBParamInOutType;
    /// used e.g. by TSQLDBOracleStatement
    VDBType: word;
    /// storage used for ftInt64, ftDouble, ftDate and ftCurrency value
    VInt64: Int64;
  end;

  PSQLDBParam = ^TSQLDBParam;

  /// dynamic array used to store standard binding parameters
  // - used e.g. by TSQLDBStatementWithParams (and its
  // inherited TSQLDBOracleStatement)
  TSQLDBParamDynArray = array of TSQLDBParam;

  /// generic abstract class handling prepared statements with binding
  // - will provide protected fields and methods for handling standard
  // TSQLDBParam parameters
  TSQLDBStatementWithParams = class(TSQLDBStatement)
  protected
    fParams: TSQLDBParamDynArray;
    fParam: TDynArray;
    fParamsArrayCount: integer;
    function CheckParam(Param: Integer; NewType: TSQLDBFieldType;
      IO: TSQLDBParamInOutType): PSQLDBParam; overload;
    function CheckParam(Param: Integer; NewType: TSQLDBFieldType;
      IO: TSQLDBParamInOutType; ArrayCount: integer): PSQLDBParam; overload;
    /// append the inlined value of a given parameter
    // - faster overridden method
    procedure AddParamValueAsText(Param: integer; Dest: TTextWriter;
      MaxCharCount: integer); override;
  public
    /// create a statement instance
    // - this overridden version will initialize the internal fParam* fields
    constructor Create(aConnection: TSQLDBConnection); override;
    /// bind a NULL value to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - raise an Exception on any error
    // - some providers (only OleDB during MULTI INSERT statements, so never used
    // in this class) expect the  proper column type to be set in BoundType
    procedure BindNull(Param: Integer; IO: TSQLDBParamInOutType=paramIn;
      BoundType: TSQLDBFieldType=ftNull); override;
    /// bind an integer value to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - raise an Exception on any error
    procedure Bind(Param: Integer; Value: Int64;
      IO: TSQLDBParamInOutType=paramIn); overload; override;
    /// bind a double value to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - raise an Exception on any error
    procedure Bind(Param: Integer; Value: double;
      IO: TSQLDBParamInOutType=paramIn); overload; override;
    /// bind a TDateTime value to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - raise an Exception on any error
    procedure BindDateTime(Param: Integer; Value: TDateTime;
      IO: TSQLDBParamInOutType=paramIn); overload; override;
    /// bind a currency value to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - raise an Exception on any error
    procedure BindCurrency(Param: Integer; Value: currency;
      IO: TSQLDBParamInOutType=paramIn); overload; override;
    /// bind a UTF-8 encoded string to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - raise an Exception on any error
    procedure BindTextU(Param: Integer; const Value: RawUTF8;
      IO: TSQLDBParamInOutType=paramIn); overload; override;
    /// bind a UTF-8 encoded buffer text (#0 ended) to a parameter
    // - the leftmost SQL parameter has an index of 1
    procedure BindTextP(Param: Integer; Value: PUTF8Char;
      IO: TSQLDBParamInOutType=paramIn); overload; override;
    /// bind a VCL string to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - raise an Exception on any error
    procedure BindTextS(Param: Integer; const Value: string;
      IO: TSQLDBParamInOutType=paramIn); overload; override;
    /// bind an OLE WideString to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - raise an Exception on any error }
    procedure BindTextW(Param: Integer; const Value: WideString;
      IO: TSQLDBParamInOutType=paramIn); overload; override;
    /// bind a Blob buffer to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - raise an Exception on any error
    procedure BindBlob(Param: Integer; Data: pointer; Size: integer;
      IO: TSQLDBParamInOutType=paramIn); overload; override;
    /// bind a Blob buffer to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - raise an Exception on any error M
    procedure BindBlob(Param: Integer; const Data: RawByteString;
      IO: TSQLDBParamInOutType=paramIn); overload; override;

    /// bind an array of values to a parameter using OCI bind array feature
    // - the leftmost SQL parameter has an index of 1
    // - values are stored as in SQL (i.e. number, 'quoted string',
    // 'YYYY-MM-DD hh:mm:ss', null)
    // - values are stored as in SQL (i.e. 'YYYY-MM-DD hh:mm:ss')
    procedure BindArray(Param: Integer; ParamType: TSQLDBFieldType;
      const Values: TRawUTF8DynArray; ValuesCount: integer); overload; override;
    /// bind an array of integer values to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - this default implementation will call BindArray() after conversion into
    // RawUTF8 items, stored in TSQLDBParam.VArray
    procedure BindArray(Param: Integer; const Values: array of Int64); overload; override;
    /// bind an array of double values to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - this default implementation will raise an exception if the engine
    // does not support array binding
    // - this default implementation will call BindArray() after conversion into
    // RawUTF8 items, stored in TSQLDBParam.VArray
    procedure BindArray(Param: Integer; const Values: array of double); overload; override;
    /// bind an array of TDateTime values to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - values are stored as in SQL (i.e. 'YYYY-MM-DD hh:mm:ss')
    // - this default implementation will raise an exception if the engine
    // does not support array binding
    // - this default implementation will call BindArray() after conversion into
    // RawUTF8 items, stored in TSQLDBParam.VArray
    procedure BindArrayDateTime(Param: Integer; const Values: array of TDateTime); override;
    /// bind an array of currency values to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - this default implementation will raise an exception if the engine
    // does not support array binding
    // - this default implementation will call BindArray() after conversion into
    // RawUTF8 items, stored in TSQLDBParam.VArray
    procedure BindArrayCurrency(Param: Integer; const Values: array of currency); override;
    /// bind an array of RawUTF8 values to a parameter
    // - the leftmost SQL parameter has an index of 1
    // - values are stored as 'quoted string'
    // - this default implementation will raise an exception if the engine
    // does not support array binding
    procedure BindArray(Param: Integer; const Values: array of RawUTF8); overload; override;

    /// start parameter array binding per-row process
    // - BindArray*() methods expect the data to be supplied "verticaly": this
    // method allow-per row binding
    // - call this method, then BindArrayRow() with the corresponding values for
    // one statement row, then Execute to send the query
    procedure BindArrayRowPrepare(const aParamTypes: array of TSQLDBFieldType;
      aExpectedMinimalRowCount: integer=0);
    /// bind a set of parameters for further array binding
    // - supplied parameters shall follow the BindArrayRowPrepare() supplied
    // types (i.e. RawUTF8, Integer/Int64, double);  you can also bind directly
    // a TDateTime value if the corresponding binding has been defined as ftDate
    // by BindArrayRowPrepare()
    procedure BindArrayRow(const aValues: array of const);
    /// bind an array of fields from an existing SQL statement for array binding
    // - supplied Rows columns shall follow the BindArrayRowPrepare() supplied
    // types (i.e. RawUTF8, Integer/Int64, double, date)
    // - can be used e.g. after ColumnsToSQLInsert() method call for fast data
    // conversion between tables
    procedure BindFromRows(Rows: TSQLDBStatement); virtual;

    {$ifndef LVCL}
    /// retrieve the parameter content, after SQL execution
    // - the leftmost SQL parameter has an index of 1
    // - to be used e.g. with stored procedures
    // - this overridden function will retrieve the value stored in the protected
    // fParams[] array: the ExecutePrepared method should have updated its
    // content as exepcted
    function ParamToVariant(Param: Integer; var Value: Variant;
      CheckIsOutParameter: boolean=true): TSQLDBFieldType; override;
    {$endif}

    /// Reset the previous prepared statement
    // - this overridden implementation will just do reset the internal fParams[]
    procedure Reset; override;
    /// Release used memory
    // - this overridden implementation will free the fParams[] members (e.g.
    // VData) but not the parameters themselves
    procedure ReleaseRows; override;
  end;

  /// generic abstract class handling prepared statements with binding
  // and column description
  // - will provide protected fields and methods for handling both TSQLDBParam
  // parameters and standard TSQLDBColumnProperty column description
  TSQLDBStatementWithParamsAndColumns = class(TSQLDBStatementWithParams)
  protected
    fColumns: TSQLDBColumnPropertyDynArray;
    fColumn: TDynArrayHashed;
  public
    /// create a statement instance
    // - this overridden version will initialize the internal fColumn* fields
    constructor Create(aConnection: TSQLDBConnection); override;
    /// retrieve a column name of the current Row
    // - Columns numeration (i.e. Col value) starts with 0
    // - it's up to the implementation to ensure than all column names are unique
    function ColumnName(Col: integer): RawUTF8; override;
    /// returns the Column index of a given Column name
    // - Columns numeration (i.e. Col value) starts with 0
    // - returns -1 if the Column name is not found (via case insensitive search)
    function ColumnIndex(const aColumnName: RawUTF8): integer; override;
    /// the Column type of the current Row
    // - ftCurrency type should be handled specifically, for faster process and
    // avoid any rounding issue, since currency is a standard OleDB type
    // - FieldSize can be set to store the size in chars of a ftUTF8 column
    // (0 means BLOB kind of TEXT column) - this implementation will store
    // fColumns[Col].ColumnValueDBSize if ColumnValueInlined=true
    function ColumnType(Col: integer; FieldSize: PInteger=nil): TSQLDBFieldType; override;
    /// direct access to the columns description
    // - gives more details than the default ColumnType() function
    property Columns: TSQLDBColumnPropertyDynArray read fColumns;
  end;

  /// generic Exception type, as used by the SynDB unit
  ESQLDBException = class(ESynException)
  protected
    fStatement: TSQLDBStatement;
  public
    /// constructor which will use FormatUTF8() instead of Format()
    // - if the first Args[0] is a TSQLDBStatement class instance, the current
    // SQL statement will be part of the exception message
    constructor CreateUTF8(const Format: RawUTF8; const Args: array of const);
  published
    /// associated TSQLDBStatement instance, if supplied as first parameter
    property Statement: TSQLDBStatement read fStatement;
  end;

{$ifdef WITH_PROXY}

  /// exception raised during remote connection process
  ESQLDBRemote = class(ESQLDBException);

  /// structure to embedd all needed parameters to execute a SQL statement
  // - used for cExecute, cExecuteToBinary, cExecuteToJSON and cExecuteToExpandedJSON
  // commands of TSQLDBProxyConnectionProperties.Process()
  // - set by TSQLDBProxyStatement.ParamsToCommand() protected method
  TSQLDBProxyConnectionCommandExecute = packed record
    /// the associated SQL statement
    SQL: RawUTF8;
    /// input parameters
    // - trunked to the exact number of parameters
    Params: TSQLDBParamDynArray;
    /// if input parameters expected BindArray() process
    ArrayCount: integer;
    /// how server side would handle statement execution
    // - fBlobAsNull and fDateWithMS do match ForceBlobAsNull and ForceDateWithMS
    // ISQLDBStatement properties
    // - fNoUpdateCount avoids to call ISQLDBStatement.UpdateCount method, e.g.
    // for performance reasons
    Force: set of (fBlobAsNull, fDateWithMS, fNoUpdateCount);
  end;

  /// implements a proxy-like virtual connection statement to a DB engine
  // - will generate TSQLDBProxyConnection kind of connection
  TSQLDBProxyConnectionPropertiesAbstract = class(TSQLDBConnectionProperties)
  protected
    fHandleConnection: boolean;
    fProtocol: TSQLDBProxyConnectionProtocol;
    fCurrentSession: integer;
    fStartTransactionTimeOut: Int64;
    /// abstract process of internal commands
    // - one rough unique method is used, in order to make easier several
    // implementation schemes and reduce data marshalling as much as possible
    // - should raise an exception on error
    // - returns the session ID (if any)
    function Process(Command: TSQLDBProxyConnectionCommand;
      const Input; var Output): integer; virtual; abstract;
    /// calls Process(cGetToken) + Process(cGetDBMS)
    // - override this method and set fProtocol before calling inherited
    procedure SetInternalProperties; override;
    /// calls Process(cGetForeignKeys,self,fForeignKeys)
    procedure GetForeignKeys; override;
  public
    /// will notify for proxy disconnection
    destructor Destroy; override;
    /// create a new TSQLDBProxyConnection instance
    // - the caller is responsible of freeing this instance
    function NewConnection: TSQLDBConnection; override;
    /// retrieve the column/field layout of a specified table
    // - calls Process(cGetFields,aTableName,Fields)
    procedure GetFields(const aTableName: RawUTF8; out Fields: TSQLDBColumnDefineDynArray); override;
    /// retrieve the advanced indexed information of a specified Table
    // - calls Process(cGetIndexes,aTableName,Indexes)
    procedure GetIndexes(const aTableName: RawUTF8; out Indexes: TSQLDBIndexDefineDynArray); override;
    /// get all table names
    // - this default implementation will use protected SQLGetTableNames virtual
    // - calls Process(cGetTableNames,self,Tables)
    procedure GetTableNames(out Tables: TRawUTF8DynArray); override;
    /// determine if the SQL statement can be cached
    // - always returns false, to force a new fake statement to be created
    function IsCachable(P: PUTF8Char): boolean; override;
  published
    /// Connect and Disconnect won't really connect nor disconnect the
    // remote connection
    // - you can set this property to TRUE if you expect the remote connection
    // by in synch with the remote proxy connection (should not be used in
    // most cases, unless you are sure you have only one single client at a time
    property HandleConnection: boolean read fHandleConnection write fHandleConnection;
    /// milliseconds to way until StartTransaction is allowed by the server
    // - in the current implementation, there should be a single transaction
    // at once on the server side: this is the time to try before reporting
    // an ESQLDBRemote exception failure
    property StartTransactionTimeOut: Int64
      read fStartTransactionTimeOut write fStartTransactionTimeOut;
  end;

  /// implements an abstract proxy-like virtual connection to a DB engine
  // - can be used e.g. for remote access or execution in a background thread
  TSQLDBProxyConnection = class(TSQLDBConnection)
  protected
    fConnected: boolean;
    fProxy: TSQLDBProxyConnectionPropertiesAbstract;
    function GetServerDateTime: TDateTime; override;
  public
    /// connect to a specified database engine
    constructor Create(aProperties: TSQLDBConnectionProperties); override;
    /// connect to the specified database
    procedure Connect; override;
    /// stop connection to the specified database
    procedure Disconnect; override;
    /// return TRUE if Connect has been already successfully called
    function IsConnected: boolean; override;
    /// initialize a new SQL query statement for the given connection
    function NewStatement: TSQLDBStatement; override;
    /// begin a Transaction for this connection
    procedure StartTransaction; override;
    /// commit changes of a Transaction for this connection
    procedure Commit; override;
    /// discard changes of a Transaction for this connection
    procedure Rollback; override;
  end;

  /// implements a proxy-like virtual connection statement to a DB engine
  // - abstract class, with no corresponding kind of connection, but allowing
  // access to the mapped data via Column*() methods
  // - will handle an internal binary buffer when the statement returned rows
  // data, as generated by TSQLDBStatement.FetchAllToBinary()
  TSQLDBProxyStatementAbstract = class(TSQLDBStatementWithParamsAndColumns)
  protected
    fDataRowCount: integer;
    fDataRowReaderOrigin, fDataRowReader: PByte;
    fDataRowNullSize: cardinal;
    fDataCurrentRowIndex: integer;
    fDataCurrentRowNullLen: cardinal;
    fDataCurrentRowNull: TByteDynArray;
    fDataCurrentRowValues: array of pointer;
    fDataCurrentRowValuesStart: pointer;
    fDataCurrentRowValuesSize: Cardinal;
    // per-row column type (SQLite3 only) e.g. select coalesce(column,0) from ..
    fDataCurrentRowColTypes: array of TSQLDBFieldType;
    function IntColumnType(Col: integer; out Data: PByte): TSQLDBFieldType;
      {$ifdef HASINLINE}inline;{$endif}
    procedure IntHeaderProcess(Data: PByte; DataLen: integer);
    procedure IntFillDataCurrent(var Reader: PByte; IgnoreColumnDataSize: boolean);
  public
    /// the Column type of the current Row
    function ColumnType(Col: integer; FieldSize: PInteger=nil): TSQLDBFieldType; override;
    /// returns TRUE if the column contains NULL
    function ColumnNull(Col: integer): boolean; override;
    /// return a Column integer value of the current Row, first Col is 0
    function ColumnInt(Col: integer): Int64; override;
    /// return a Column floating point value of the current Row, first Col is 0
    function ColumnDouble(Col: integer): double; override;
    /// return a Column floating point value of the current Row, first Col is 0
    function ColumnDateTime(Col: integer): TDateTime; override;
    /// return a Column currency value of the current Row, first Col is 0
    // - should retrieve directly the 64 bit Currency content, to avoid
    // any rounding/conversion error from floating-point types
    function ColumnCurrency(Col: integer): currency; override;
    /// return a Column UTF-8 encoded text value of the current Row, first Col is 0
    function ColumnUTF8(Col: integer): RawUTF8; override;
    /// return a Column text value as generic VCL string of the current Row, first Col is 0
    function ColumnString(Col: integer): string; override;
    /// return a Column as a blob value of the current Row, first Col is 0
    function ColumnBlob(Col: integer): RawByteString; override;
    /// return all columns values into JSON content
    procedure ColumnsToJSON(WR: TJSONWriter); override;
    /// direct access to the data buffer of the current row
    // - points to Double/Currency value, or variable-length Int64/UTF8/Blob
    // - points to nil if the column value is NULL
    function ColumnData(Col: integer): pointer;
    /// append current row content as binary stream
    // - will save one data row in optimized binary format (if not in Null)
    // - virtual method called by FetchAllToBinary()
    // - follows the format expected by TSQLDBProxyStatement
    procedure ColumnsToBinary(W: TFileBufferWriter;
      Null: pointer; const ColTypes: TSQLDBFieldTypeDynArray); override;

    /// read-only access to the number of data rows stored
    property DataRowCount: integer read fDataRowCount;
  end;

  /// implements a proxy-like virtual connection statement to a DB engine
  // - is generated by TSQLDBProxyConnection kind of connection
  // - will use an internal binary buffer when the statement returned rows data,
  // as generated by TSQLDBStatement.FetchAllToBinary() or JSON for
  // ExecutePreparedAndFetchAllAsJSON() method (as expected by our ORM)
  TSQLDBProxyStatement = class(TSQLDBProxyStatementAbstract)
  protected
    fDataInternalCopy: RawByteString;
    fUpdateCount: integer;
    fForceNoUpdateCount: boolean;
    procedure ParamsToCommand(var Input: TSQLDBProxyConnectionCommandExecute);
  public
    /// Execute a SQL statement
    // - for TSQLDBProxyStatement, preparation and execution are processed in
    // one step, when this method is executed - as such, Prepare() won't call
    // the remote process, but will just set fSQL
    // - this overridden implementation will use out optimized binary format
    //  as generated by TSQLDBStatement.FetchAllToBinary(), and not JSON
    procedure ExecutePrepared; override;
    /// execute a prepared SQL statement and return all rows content as a JSON string
    // - JSON data is retrieved with UTF-8 encoding
    // - if Expanded 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 Expanded is false, JSON data is serialized (used in TSQLTableJSON)
    // & { "FieldCount":1,"Values":["col1","col2",val11,"val12",val21,..] }
    // - BLOB field value is saved as Base64, in the '"\uFFF0base64encodedbinary"'
    // format and contains true BLOB data
    // - this overridden implementation will use JSON for transmission, and
    // binary encoding only for parameters (to avoid unneeded conversions, e.g.
    // when called from mORMotDB.pas)
    procedure ExecutePreparedAndFetchAllAsJSON(Expanded: boolean; out JSON: RawUTF8); override;
    /// append all rows content as binary stream
    // - will save the column types and name, then every data row in optimized
    // binary format (faster and smaller than JSON)
    // - you can specify a LIMIT for the data extent (default 0 meaning all data)
    // - generates the format expected by TSQLDBProxyStatement
    // - this overriden method will use the internal data copy of the binary
    // buffer retrieved by ExecutePrepared, so would be almost immediate,
    // and would allow e.g. direct consumption via our TSynSQLStatementDataSet
    // - note that DataRowPosition won't be set by this method: will be done
    // e.g. in TSQLDBProxyStatementRandomAccess.Create
    function FetchAllToBinary(Dest: TStream; MaxRowCount: cardinal=0;
      DataRowPosition: PCardinalDynArray=nil): cardinal; override;
    /// gets a number of updates made by latest executed statement
    // - this overriden method will return the integer value returned by
    // cExecute command
    function UpdateCount: integer; override;
    /// force no UpdateCount method call on server side
    // - may be needed to reduce server load, if this information is not needed
    property ForceNoUpdateCount: boolean read fForceNoUpdateCount write fForceNoUpdateCount;

    /// after a statement has been prepared via Prepare() + ExecutePrepared() or
    //   Execute(), this method must be called one or more times to evaluate it
    function Step(SeekFirst: boolean=false): boolean; override;
  end;

  /// client-side implementation of a remote connection to any SynDB engine
  // - will compute binary compressed messages for the remote processing,
  // ready to be served e.g. over HTTP via our SynDBRemote.pas unit
  // - abstract class which should override its protected ProcessMessage() method
  // e.g. by TSQLDBRemoteConnectionPropertiesTest or
  TSQLDBRemoteConnectionPropertiesAbstract = class(TSQLDBProxyConnectionPropertiesAbstract)
  protected
    /// will build and interpret binary messages to be served with ProcessMessage
    // - would raise an exception in case of error, even on the server side
    function Process(Command: TSQLDBProxyConnectionCommand;
      const Input; var Output): integer; override;
    /// abstract method to override for the expected transmission protocol
    // - could raise an exception on transmission error
    procedure ProcessMessage(const Input: RawByteString; out Output: RawByteString);
      virtual; abstract;
  end;

  /// fake proxy class for testing the remote connection to any SynDB engine
  // - resulting overhead due to our binary messaging: unnoticeable :)
  TSQLDBRemoteConnectionPropertiesTest = class(TSQLDBRemoteConnectionPropertiesAbstract)
  protected
    fProps: TSQLDBConnectionProperties;
    // this overriden method will just call fProps.RemoteProcessMessage()
    procedure ProcessMessage(const Input: RawByteString; out Output: RawByteString); override;
  public
    /// create a test redirection to an existing local connection property
    // - you can specify a User/Password credential pair to also test the
    // authentication via TSynAuthentication
    constructor Create(aProps: TSQLDBConnectionProperties;
      const aUserID,aPassword: RawUTF8; aProtocol: TSQLDBProxyConnectionProtocolClass); reintroduce;
  end;


  /// implements a virtual statement with direct data access
  // - is generated with no connection, but allows direct random access to any
  // data row retrieved from TSQLDBStatement.FetchAllToBinary() binary data
  // - GotoRow() method allows direct access to a row data via Column*()
  // - is used e.g. by TSynSQLStatementDataSet of SynDBVCL unit
  TSQLDBProxyStatementRandomAccess = class(TSQLDBProxyStatementAbstract)
  protected
    fRowData: TCardinalDynArray;
  public
    /// initialize the internal structure from a given memory buffer
    // - by default, ColumnDataSize would be computed from the supplied data,
    // unless you set IgnoreColumnDataSize=true to set the value to 0 (and
    // force e.g. SynDBVCL TSynBinaryDataSet.InternalInitFieldDefs define the
    // field as ftDefaultMemo)
    constructor Create(Data: PByte; DataLen: integer;
      DataRowPosition: PCardinalDynArray=nil; IgnoreColumnDataSize: boolean=false); reintroduce;

    /// Execute a prepared SQL statement
    // - this unexpected overridden method will raise a ESQLDBException
    procedure ExecutePrepared; override;
    /// Change cursor position to the next available row
    // - this unexpected overridden method will raise a ESQLDBException
    function Step(SeekFirst: boolean=false): boolean; override;

    /// change the current data Row
    // - if Index<DataRowCount, returns TRUE and you can access to the data
    // via regular Column*() methods
    // - can optionally raise an ESQLDBException if Index is not correct
    function GotoRow(Index: integer; RaiseExceptionOnWrongIndex: Boolean=false): boolean;
  end;

{$endif WITH_PROXY}


const
  /// TSQLDBFieldType kind of columns which have a fixed width
  FIXEDLENGTH_SQLDBFIELDTYPE = [ftInt64, ftDouble, ftCurrency, ftDate];

  /// conversion matrix from TSQLDBFieldType into variant type
  MAP_FIELDTYPE2VARTYPE: array[TSQLDBFieldType] of Word = (
    varEmpty, varNull, varInt64, varDouble, varCurrency, varDate,
    varSynUnicode, varString);
// ftUnknown, ftNull, ftInt64, ftDouble, ftCurrency, ftDate, ftUTF8, ftBlob


/// function helper logging some column truncation information text
procedure LogTruncatedColumn(const Col: TSQLDBColumnProperty);

/// retrieve a table name without any left schema
// - e.g. TrimLeftSchema('SCHEMA.TABLENAME')='TABLENAME'
function TrimLeftSchema(const TableName: RawUTF8): RawUTF8;

/// replace all '?' in the SQL statement with named parameters like :AA :AB..
// - returns the number of ? parameters found within aSQL
// - won't generate any SQL keyword parameters (e.g. :AS :OF :BY), to be
// compliant with Oracle OCI expectations
// - any ending ';' character is deleted, unless aStripSemicolon is unset
function ReplaceParamsByNames(const aSQL: RawUTF8; var aNewSQL: RawUTF8;
  aStripSemicolon: boolean=true): integer;

/// replace all '?' in the SQL statement with indexed parameters like $1 $2 ...
// - returns the number of ? parameters found within aSQL
// - as used e.g. by PostgreSQL & Oracle (:1 :2) library
// - if AllowSemicolon is false (by default), reject any statement with ;
// (Postgres do not allow ; inside prepared statement); it should be 
// true for Oracle
function ReplaceParamsByNumbers(const aSQL: RawUTF8; var aNewSQL: RawUTF8;
  IndexChar: AnsiChar = '$'; AllowSemicolon: boolean = false): integer;

/// create a JSON array from an array of UTF-8 bound values
// - as generated during array binding, i.e. with quoted strings
// 'one','t"wo' -> '{"one","t\"wo"}'   and  1,2,3 -> '{1,2,3}'
// - as used e.g. by PostgreSQL library
function BoundArrayToJSONArray(const Values: TRawUTF8DynArray): RawUTF8;


{ -------------- native connection interfaces, without OleDB }

type
  /// access to a native library
  // - this generic class is to be used for any native connection using an
  // external library
  // - is used e.g. in SynDBOracle by TSQLDBOracleLib to access the OCI library,
  // or by SynDBODBC to access the ODBC library
  TSQLDBLib = class
  protected
    fHandle: {$ifdef FPC}TLibHandle{$else}HMODULE{$endif};
    fLibraryPath: TFileName;
    /// same as SafeLoadLibrary() but setting fLibraryPath and cwd on Windows
    function TryLoadLibrary(const aLibrary: array of TFileName;
      aRaiseExceptionOnFailure: ESynExceptionClass): boolean; virtual;
  public
    /// release associated memory and linked library
    destructor Destroy; override;
    /// the associated library handle
    property Handle: {$ifdef FPC}TLibHandle{$else}HMODULE{$endif} read fHandle write fHandle;
    /// the loaded library path
    property LibraryPath: TFileName read fLibraryPath;
  end;


{$ifdef EMULATES_TQUERY}

{ -------------- TQuery TField TParam emulation classes and types }

type
  /// generic Exception type raised by the TQuery class
  ESQLQueryException = class(ESynException)
  public
    constructor CreateFromError(aMessage: string; aConnection: TSQLDBConnection);
  end;

  /// generic type used by TQuery / TQueryValue for BLOBs fields
  TBlobData = RawByteString;

  /// represent the use of parameters on queries or stored procedures
  // - same enumeration as with the standard DB unit from VCL
  TParamType = (ptUnknown, ptInput, ptOutput, ptInputOutput, ptResult);

  TQuery = class;

  /// pseudo-class handling a TQuery bound parameter or column value
  // - will mimic both TField and TParam classes as defined in standard DB unit,
  // by pointing both classes types to PQueryValue
  // - usage of an object instead of a class allow faster access via a
  // dynamic array (and our TDynArrayHashed wrapper) for fast property name
  // handling (via name hashing) and pre-allocation
  // - it is based on an internal Variant to store the parameter or column value
  {$ifdef USERECORDWITHMETHODS}TQueryValue = record
    {$else}TQueryValue = object{$endif}
  private
    /// fName should be the first property, i.e. the searched hashed value
    fName: string;
    fValue: Variant;
    fValueBlob: boolean;
    fParamType: TParamType;
    // =-1 if empty, =0 if eof, >=1 if cursor on row data
    fRowIndex: integer;
    fColumnIndex: integer;
    fQuery: TQuery;
    procedure CheckExists;
    procedure CheckValue;
    function GetIsNull: boolean;
    function GetDouble: double;
    function GetString: string;
    function GetAsWideString: SynUnicode;
    function GetCurrency: Currency;
    function GetDateTime: TDateTime;
    function GetVariant: Variant;
    function GetInteger: integer;
    function GetInt64: Int64;
    function GetBlob: TBlobData;
    function GetAsBytes: TBytes;
    function GetBoolean: Boolean;
    procedure SetDouble(const aValue: double);
    procedure SetString(const aValue: string);
    procedure SetAsWideString(const aValue: SynUnicode);
    procedure SetCurrency(const aValue: Currency);
    procedure SetDateTime(const aValue: TDateTime);
    procedure SetVariant(const aValue: Variant);
    procedure SetInteger(const aValue: integer);
    procedure SetInt64(const aValue: Int64);
    procedure SetBlob(const aValue: TBlobData);
    procedure SetAsBytes(const Value: TBytes);
    procedure SetBoolean(const aValue: Boolean);
    procedure SetBound(const aValue: Boolean);
  public
    /// set the column value to null
    procedure Clear;
    /// the associated (field) name
    property FieldName: string read fName;
    /// the associated (parameter) name
    property Name: string read fName;
    /// parameter type for queries or stored procedures
    property ParamType: TParamType read fParamType write fParamType;
    /// returns TRUE if the stored Value is null
    property IsNull: Boolean read GetIsNull;
    /// just do nothing - here for compatibility reasons with Clear + Bound := true
    property Bound: Boolean write SetBound;
    /// access the Value as Integer
    property AsInteger: integer read GetInteger write SetInteger;
    /// access the Value as Int64
    // - note that under Delphi 5, Int64 is not handled: the Variant type
    // only handle integer types, in this Delphi version :(
    property AsInt64: Int64 read GetInt64 write SetInt64;
    /// access the Value as Int64
    // - note that under Delphi 5, Int64 is not handled: the Variant type
    // only handle integer types, in this Delphi version :(
    property AsLargeInt: Int64 read GetInt64 write SetInt64;
    /// access the Value as boolean
    property AsBoolean: Boolean read GetBoolean write SetBoolean;
    /// access the Value as String
    // - used in the VCL world for both TEXT and BLOB content (BLOB content
    // will only work in pre-Unicode Delphi version, i.e. before Delphi 2009)
    property AsString: string read GetString write SetString;
    /// access the Value as an unicode String
    // - will return a WideString before Delphi 2009, and an UnicodeString
    // for Unicode versions of the compiler (i.e. our SynUnicode type)
    property AsWideString: SynUnicode read GetAsWideString write SetAsWideString;
    /// access the BLOB Value as an AnsiString
    // - will work for all Delphi versions, including Unicode versions (i.e.
    // since Delphi 2009)
    // - for a BLOB parameter or column, you should use AsBlob or AsBlob
    // properties instead of AsString (this later won't work after Delphi 2007)
    property AsBlob: TBlobData read GetBlob write SetBlob;
    /// access the BLOB Value as array of byte (TBytes)
     // - will work for all Delphi versions, including Unicode versions (i.e.
    // since Delphi 2009)
    // - for a BLOB parameter or column, you should use AsBlob or AsBlob
    // properties instead of AsString (this later won't work after Delphi 2007)
    property AsBytes: TBytes read GetAsBytes write SetAsBytes;
    /// access the Value as double
    property AsFloat: double read GetDouble write SetDouble;
    /// access the Value as TDateTime
    property AsDateTime: TDateTime read GetDateTime write SetDateTime;
    /// access the Value as TDate
    property AsDate: TDateTime read GetDateTime write SetDateTime;
    /// access the Value as TTime
    property AsTime: TDateTime read GetDateTime write SetDateTime;
    /// access the Value as Currency
    // - avoid any rounding conversion, as with AsFloat
    property AsCurrency: Currency read GetCurrency write SetCurrency;
    /// access the Value as Variant
    property AsVariant: Variant read GetVariant write SetVariant;
  end;

  /// a dynamic array of TQuery bound parameters or column values
  // - TQuery will use TDynArrayHashed for fast search
  TQueryValueDynArray = array of TQueryValue;

  /// pointer to TQuery bound parameter or column value
  PQueryValue = ^TQueryValue;

  /// pointer mapping the VCL DB TField class
  // - to be used e.g. with code using local TField instances in a loop
  TField = PQueryValue;

  /// pointer mapping the VCL DB TParam class
  // - to be used e.g. with code using local TParam instances
  TParam = PQueryValue;

  /// class mapping VCL DB TQuery for direct database process
  // - this class can mimic basic TQuery VCL methods, but won't need any BDE
  // installed, and will be faster for field and parameters access than the
  // standard TDataSet based implementation; in fact, OleDB replaces the BDE
  // or the DBExpress layer, or access directly to the client library
  // (e.g. for TSQLDBOracleConnectionProperties which calls oci.dll)
  // - it is able to run basic queries as such:
  // !  Q := TQuery.Create(aSQLDBConnection);
  // !  try
  // !    Q.SQL.Clear; // optional
  // !    Q.SQL.Add('select * from DOMAIN.TABLE');
  // !    Q.SQL.Add('  WHERE ID_DETAIL=:detail;');
  // !    Q.ParamByName('DETAIL').AsString := '123420020100000430015';
  // !    Q.Open;
  // !    Q.First;    // optional
  // !    while not Q.Eof do begin
  // !      assert(Q.FieldByName('id_detail').AsString='123420020100000430015');
  // !      Q.Next;
  // !    end;
  // !    Q.Close;    // optional
  // !  finally
  // !    Q.Free;
  // !  end;
  // - since there is no underlying TDataSet, you can't have read and write
  // access, or use the visual DB components of the VCL: it's limited to
  // direct emulation of low-level SQL as in the above code, with one-direction
  // retrieval (e.g. the Edit, Post, Append, Cancel, Prior, Locate, Lookup
  // methods do not exist within this class)
  // - use ToDataSet() function from SynDBVCL.pas to create a TDataSet
  // from such a TQuery instance, and link this request to visual DB components
  // - this class is Unicode-ready even before Delphi 2009 (via the TQueryValue
  // AsWideString method), will natively handle Int64/TBytes field or parameter
  // data, and will have less overhead than the standard DB components of the VCL
  // - you should better use TSQLDBStatement instead of this wrapper, but
  // having such code-compatible TQuery replacement could make easier some
  // existing code upgrade (e.g. to avoid deploying the deprecated BDE, generate
  // smaller executable, access any database without paying a big fee,
  // avoid rewriting a lot of existing code lines of a big application...)
  TQuery = class
  protected
    fSQL: TStringList;
    fPrepared: ISQLDBStatement;
    fRowIndex: Integer;
    fConnection: TSQLDBConnection;
    fParams: TQueryValueDynArray;
    fResults: TQueryValueDynArray;
    fResult: TDynArrayHashed;
    fResultCount: integer;
    fParam: TDynArrayHashed;
    fParamCount: Integer;
    fTag: PtrInt;
    function GetIsEmpty: Boolean;
    function GetActive: Boolean;
    function GetFieldCount: integer;
    function GetParamCount: integer;
    function GetField(aIndex: integer): TField;
    function GetParam(aIndex: integer): TParam;
    function GetEof: boolean;
    function GetBof: Boolean;
    function GetRecordCount: integer;
    function GetSQLAsText: string;
    procedure OnSQLChange(Sender: TObject);
    /// prepare and execute the SQL query
    procedure Execute(ExpectResults: Boolean);
  public
    /// initialize a query for the associated database connection
    constructor Create(aConnection: TSQLDBConnection);
    /// release internal memory and statements
    destructor Destroy; override;
    /// a do-nothing method, just available for compatibility purpose
    procedure Prepare;
    /// begin the SQL query, for a SELECT statement
    // - will parse the entered SQL statement, and bind parameters
    // - will then execute the SELECT statement, ready to use First/Eof/Next
    // methods, the returned rows being available via FieldByName methods
    procedure Open;
    /// begin the SQL query, for a non SELECT statement
    // - will parse the entered SQL statement, and bind parameters
    // - the query will be released with a call to Close within this method
    // - will return the number of updated rows (i.e.
    // PreparedSQLDBStatement.UpdateCount)
    procedure ExecSQL;
    /// begin the SQL query, for a non SELECT statement
    // - will parse the entered SQL statement, and bind parameters
    // - the query will be released with a call to Close within this method
    // - this method will return the number of updated rows (i.e.
    // PreparedSQLDBStatement.UpdateCount)
    function ExecSQLAndReturnUpdateCount: integer;
    /// after a successfull Open, will get the first row of results
    procedure First;
    /// after successfull Open and First, go the the next row of results
    procedure Next;
    /// end the SQL query
    // - will release the SQL statement, results and bound parameters
    // - the query should be released with a call to Close before reopen
    procedure Close;
    /// access a SQL statement parameter, entered as :aParamName in the SQL
    // - if the requested parameter do not exist yet in the internal fParams
    // list, AND if CreateIfNotExisting=true, a new TQueryValue instance
    // will be created and registered
    function ParamByName(const aParamName: string; CreateIfNotExisting: boolean=true): TParam;
    /// retrieve a column value from the current opened SQL query row
    // - will raise an ESQLQueryException error in case of error, e.g. if no column
    // name matchs the supplied name
    function FieldByName(const aFieldName: string): TField;
    /// retrieve a column value from the current opened SQL query row
    // - will return nil in case of error, e.g. if no column name matchs the
    // supplied name
    function FindField(const aFieldName: string): TField;
    /// the associated database connection
    property Connection: TSQLDBConnection read fConnection;
    /// the SQL statement to be executed
    // - statement will be prepared and executed via Open or ExecSQL methods
    // - SQL.Clear will force a call to the Close method (i.e. reset the query,
    // just as with the default VCL implementation)
    property SQL: TStringList read fSQL;
    /// the SQL statement with inlined bound parameters
    property SQLAsText: string read GetSQLAsText;
    /// equals true if there is some rows pending
    property Eof: Boolean read GetEof;
    /// equals true if on first row
    property Bof: Boolean read GetBof;
    /// returns 0 if no record was retrievd, 1 if there was some records
    // - not the exact count: just here for compatibility purpose with code
    // like   if aQuery.RecordCount>0 then ...
    property RecordCount: integer read GetRecordCount;
    /// equals true if there is no row returned
    property IsEmpty: Boolean read GetIsEmpty;
    /// equals true if the query is opened
    property Active: Boolean read GetActive;
    /// the number of columns in the current opened SQL query row
    property FieldCount: integer read GetFieldCount;
    /// the number of bound parameters in the current SQL statement
    property ParamCount: integer read GetParamCount;
    /// retrieve a column value from the current opened SQL query row
    // - will return nil in case of error, e.g. out of range index
    property Fields[aIndex: integer]: TField read GetField;
    /// retrieve a  bound parameters in the current SQL statement
    // - will return nil in case of error, e.g. out of range index
    property Params[aIndex: integer]: TParam read GetParam;
    /// non VCL property to access the internal SynDB prepared statement
    // - is nil if the TQuery is not prepared (e.g. after Close)
    property PreparedSQLDBStatement: ISQLDBStatement read fPrepared;
    /// user-customizable number attached to this instance
    // - for compatibility with TComponent
    property Tag: PtrInt read fTag write fTag;
  end;

{$endif EMULATES_TQUERY}

var
  /// the TSynLog class used for logging for all our SynDB related units
  // - you may override it with TSQLLog, if available from mORMot.pas
  // - since not all exceptions are handled specificaly by this unit, you
  // may better use a common TSynLog class for the whole application or module
  SynDBLog: TSynLogClass=TSynLog;



{ -------------- Database specific classes - shared by several SynDB units }

const
  /// the known column data types corresponding to our TSQLDBFieldType types
  // - will be used e.g. for TSQLDBConnectionProperties.SQLFieldCreate()
  // - see TSQLDBFieldTypeDefinition documentation to find out the mapping
  DB_FIELDS: array[TSQLDBDefinition] of TSQLDBFieldTypeDefinition = (
  // ftUnknown=int32, ftNull=UTF8, ftInt64, ftDouble, ftCurrency, ftDate, ftUTF8, ftBlob

  // dUnknown
  (' INT',' NVARCHAR(%)',' BIGINT',' DOUBLE',' NUMERIC(19,4)',' TIMESTAMP',
   ' CLOB',' BLOB'),

  // dDefault
  (' INT',' NVARCHAR(%)',' BIGINT',' DOUBLE',' NUMERIC(19,4)',' TIMESTAMP',
   ' CLOB',' BLOB'),

  // dOracle
  (' NUMBER(22,0)',' NVARCHAR2(%)',' NUMBER(22,0)',' BINARY_DOUBLE',' NUMBER(19,4)',
   ' DATE',' NCLOB',' BLOB'),
    // NCLOB (National Character Large Object) is an Oracle data type that can hold
    // up to 4 GB of character data. It's similar to a CLOB, but characters are
    // stored in a NLS or multibyte national character set (like NVARCHAR2)

  // dMSSQL
  (' int',' nvarchar(%)',' bigint',' float',' money',' datetime',' nvarchar(max)',
   ' varbinary(max)'),

  // dJet
  (' Long',' VarChar(%)',' Decimal(19,0)',' Double',' Currency',' DateTime',
   ' LongText',' LongBinary'),

  // dMySQL
  (' int',' varchar(%) character set UTF8',' bigint',' double',' decimal(19,4)',
   ' datetime',' mediumtext character set UTF8',' mediumblob'),

  // dSQLite
  (' INTEGER',' TEXT',' INTEGER',' FLOAT',' FLOAT',' TEXT',' TEXT',' BLOB'),

  // dFirebird
  (' INTEGER',' VARCHAR(%) CHARACTER SET UTF8',' BIGINT',' FLOAT',' DECIMAL(18,4)',
   ' TIMESTAMP',' BLOB SUB_TYPE 1 SEGMENT SIZE 2000 CHARACTER SET UTF8',
   ' BLOB SUB_TYPE 0 SEGMENT SIZE 2000'),
   // about BLOB: http://www.ibphoenix.com/resources/documents/general/doc_54

  // dNexusDB
  (' INTEGER',' NVARCHAR(%)',' LARGEINT',' REAL',' MONEY',' DATETIME',' NCLOB',' BLOB'),
    // VARCHAR(%) CODEPAGE 65001 just did not work well with Delphi<2009

  // dPostgreSQL
  (' INTEGER',' TEXT',' BIGINT',' DOUBLE PRECISION',' NUMERIC(19,4)',
   ' TIMESTAMP',' TEXT',' BYTEA'),
    // like SQLite3, we will create TEXT column instead of VARCHAR(%), as stated
    // by http://www.postgresql.org/docs/current/static/datatype-character.html

  // dDB2 (for CCSID Unicode tables)
  (' int',' varchar(%)',' bigint',' real',' decimal(19,4)',' timestamp',' clob', ' blob'),
    { note: bigint needs 9.1 and up }

  // dInformix
  (' int',' lvarchar(%)',' bigint',' smallfloat',' decimal(19,4)',
   ' datetime year to fraction(3)',' clob', ' blob')
  );

  /// the known column data types corresponding to our TSQLDBFieldType types
  // - will be used e.g. for TSQLDBConnectionProperties.SQLFieldCreate()
  // - SQLite3 doesn't expect any field length, neither PostgreSQL, so set to 0
  DB_FIELDSMAX: array[TSQLDBDefinition] of cardinal = (
    1000, 1000, 1333, { =4000/3 since WideChar is up to 3 bytes in UTF-8 }
    4000, 255, 4000, 0, 32760, 32767, 0, 32700, 32700);

  /// the known SQL statement to retrieve the server date and time
  DB_SERVERTIME: array[TSQLDBDefinition] of RawUTF8 = (
    '','', // return local server time by default
    'select sysdate from dual',
    'select GETDATE()',
    '', // Jet is local -> return local time
    'SELECT NOW()',
    '', // SQlite is local -> return local time
    'select current_timestamp from rdb$database',
    'SELECT CURRENT_TIMESTAMP',
    'SELECT LOCALTIMESTAMP',
    'select current timestamp from sysibm.sysdummy1',
    'select CURRENT YEAR TO FRACTION(3) from SYSTABLES where tabid = 1'
  );

const
  /// the known SQL syntax to limit the number of returned rows in a SELECT
  // - Positon indicates if should be included within the WHERE clause,
  // at the beginning of the SQL statement, or at the end of the SQL statement
  // - InsertFmt will replace '%' with the maximum number of lines to be retrieved
  // - used by TSQLDBConnectionProperties.AdaptSQLLimitForEngineList()
  DB_SQLLIMITCLAUSE: array[TSQLDBDefinition] of TSQLDBDefinitionLimitClause  = (
    (Position: posNone;   InsertFmt:nil),                         { dUnknown    }
    (Position: posNone;   InsertFmt:nil),                         { dDefault    }
    (Position: posWhere;  InsertFmt:'rownum<=%'),                 { dOracle     }
    (Position: posSelect; InsertFmt:'top(%) '),                   { dMSSQL      }
    (Position: posSelect; InsertFmt:'top % '),                    { dJet        }
    (Position: posAfter;  InsertFmt:' limit %'),                  { dMySQL      }
    (Position: posAfter;  InsertFmt:' limit %'),                  { dSQLite     }
    (Position: posSelect; InsertFmt:'first % '),                  { dFirebird   }
    (Position: posSelect; InsertFmt:'top % '),                    { dNexusDB    }
    (Position: posAfter;  InsertFmt:' limit %'),                  { dPostgreSQL }
    (Position: posAfter;  InsertFmt:' fetch first % rows only'),  { dDB2        }
    (Position: posAfter;  InsertFmt:' first % '));                { dInformix   }

  /// the known database engines handling CREATE INDEX IF NOT EXISTS statement
  DB_HANDLECREATEINDEXIFNOTEXISTS = [dSQLite, dPostgreSQL];

  /// the known database engines handling CREATE INDEX on BLOB columns
  // - SQLite3 does not have any issue about indexing any column
  // - PostgreSQL is able to index TEXT columns, which are some kind of CLOB
  DB_HANDLEINDEXONBLOBS = [dSQLite,dPostgreSQL];

  /// where the DESC clause shall be used for a CREATE INDEX statement
  // - only identified syntax exception is for FireBird
  DB_SQLDESENDINGINDEXPOS: array[TSQLDBDefinition] of
    (posWithColumn, posGlobalBefore) = (
    posWithColumn, posWithColumn, posWithColumn, posWithColumn, posWithColumn,
    posWithColumn, posWithColumn, posGlobalBefore, posWithColumn, posWithColumn,
    posWithColumn, posWithColumn);

  /// the SQL text corresponding to the identified WHERE operators for a SELECT
  DB_SQLOPERATOR: array[opEqualTo..opLike] of RawUTF8 = (
    '=','<>','<','<=','>','>=',' in ',' is null',' is not null',' like ');


/// retrieve the text of a given Database SQL dialect enumeration
// - see also TSQLDBConnectionProperties.GetDBMSName() method
function ToText(DBMS: TSQLDBDefinition): PShortString; overload;

/// retrieve the text of a given Database field type enumeration
// - see also TSQLDBFieldTypeToString() function
function ToText(Field: TSQLDBFieldType): PShortString; overload;

/// retrieve the ready-to-be displayed text of a given Database field
// type enumeration
function TSQLDBFieldTypeToString(aType: TSQLDBFieldType): TShort16;

{$ifdef WITH_PROXY}
/// retrieve the ready-to-be displayed text of proxy commands implemented by
// TSQLDBProxyConnectionProperties.Process()
function ToText(cmd: TSQLDBProxyConnectionCommand): PShortString; overload;
{$endif WITH_PROXY}


implementation

function ToText(DBMS: TSQLDBDefinition): PShortString;
begin
  result := GetEnumName(TypeInfo(TSQLDBDefinition),ord(DBMS));
end;

function ToText(Field: TSQLDBFieldType): PShortString;
begin
  result := GetEnumName(TypeInfo(TSQLDBFieldType),ord(Field));
end;

{$ifdef WITH_PROXY}
function ToText(cmd: TSQLDBProxyConnectionCommand): PShortString;
begin
  result := GetEnumName(TypeInfo(TSQLDBProxyConnectionCommand),ord(cmd));
end;
{$endif}

function TSQLDBFieldTypeToString(aType: TSQLDBFieldType): TShort16;
begin
  if aType<=high(aType) then
    result := TrimLeftLowerCaseToShort(ToText(aType)) else
    FormatShort16('#%',[ord(aType)],result);
end;


{$ifdef EMULATES_TQUERY}

{ ESQLQueryException }

constructor ESQLQueryException.CreateFromError(aMessage: string; aConnection: TSQLDBConnection);
begin
  if aMessage='' then
    aMessage := 'Error';
  if (aConnection=nil) or (aConnection.fErrorMessage='') then
    Create(aMessage) else
  if aConnection.fErrorException=nil then
    CreateUTF8('% [%]',[aMessage,aConnection.fErrorMessage]) else
    CreateUTF8('% as % with message [%]',
      [aMessage,aConnection.fErrorException,aConnection.fErrorMessage]);
end;

{ TQueryValue }

procedure TQueryValue.CheckExists;
begin
  if @self=nil then
    raise ESQLQueryException.Create('Parameter/Field not existing');
end;

procedure TQueryValue.CheckValue;
begin
  CheckExists;
  if fQuery=nil then
    exit; // Params already have updated value
  if fQuery.fRowIndex<=0 then // =-1 if empty, =0 if eof, >=1 if row data
    SetVariantNull(fValue) else
  if fRowIndex<>fQuery.fRowIndex then begin // get column value once per row
    fRowIndex := fQuery.fRowIndex;
    fQuery.fPrepared.ColumnToVariant(fColumnIndex,fValue);
  end;
end;

// in code below, most of the work should have been done by the Variants unit :)
// but since Delphi 5 does not handle varInt64 type, we had do handle it :(
// in all cases, our version should speed up process a little bit ;)

procedure TQueryValue.Clear;
begin
  SetVariantNull(fValue);
end;

function TQueryValue.GetAsBytes: TBytes;
var tmp: TBlobData;
begin
  CheckValue;
  VariantToRawByteString(fValue,tmp);
  RawByteStringToBytes(tmp,result);
end;

function TQueryValue.GetAsWideString: SynUnicode;
begin
  CheckValue;
  with TVarData(fValue) do
  case VType of
    varNull:     result := '';
    varInt64:    UTF8ToSynUnicode(Int64ToUtf8(VInt64),result);
    varString:   UTF8ToSynUnicode(RawUTF8(VAny),result);
    {$ifdef HASVARUSTRING}
    varUString:  result := UnicodeString(VAny);
    {$endif}
  else result := SynUnicode(fValue);
  end;
end;

function TQueryValue.GetBlob: TBlobData;
begin
  CheckValue;
  VariantToRawByteString(fValue,result);
end;

function TQueryValue.GetBoolean: Boolean;
begin
  Result := GetInt64<>0;
end;

function TQueryValue.GetCurrency: Currency;
begin
  CheckValue;
  with TVarData(fValue) do
  case VType of
    varNull:     result := 0;
    varInteger:  result := VInteger;
    varInt64:    result := VInt64;
    varCurrency: result := VCurrency;
    varDouble, varDate: result := VDouble;
    else result := fValue;
  end;
end;

function TQueryValue.GetDateTime: TDateTime;
begin
  CheckValue;
  with TVarData(fValue) do
  case VType of
    varString:     result := Iso8601ToDateTime(RawUTF8(VAny));
    varSynUnicode: result := Iso8601ToDateTime(SynUnicodeToUtf8(SynUnicode(VAny)));
  else result := GetDouble;
  end;
end;

function TQueryValue.GetDouble: double;
begin
  CheckValue;
  with TVarData(fValue) do
  case VType of
    varNull:     result := 0;
    varInteger:  result := VInteger;
    varInt64:    result := VInt64;
    varCurrency: result := VCurrency;
    varDouble, varDate: result := VDouble;
    else result := fValue;
  end;
end;

function TQueryValue.GetInt64: Int64;
begin
  CheckValue;
  with TVarData(fValue) do
  case VType of
    varNull:     result := 0;
    varInteger:  result := VInteger;
    varInt64:    result := VInt64;
    varCurrency: result := trunc(VCurrency);
    varDouble, varDate: result := trunc(VDouble);
    else result := {$ifdef DELPHI5OROLDER}integer{$endif}(fValue);
  end;
end;

function TQueryValue.GetInteger: integer;
begin
  CheckValue;
  with TVarData(fValue) do
  case VType of
    varNull:     result := 0;
    varInteger:  result := VInteger;
    varInt64:    result := VInt64;
    varCurrency: result := trunc(VCurrency);
    varDouble, varDate:   result := trunc(VDouble);
    else result := fValue;
  end;
end;

function TQueryValue.GetIsNull: boolean;
begin
  CheckValue;
  result := TVarData(fValue).VType=varNull;
end;

function TQueryValue.GetString: string;
begin
  CheckValue;
  with TVarData(fValue) do
  case VType of
    varNull:     result := '';
    varInteger:  result := IntToString(VInteger);
    varInt64:    result := IntToString(VInt64);
    varCurrency: result := Curr64ToString(VInt64);
    varDouble:   result := DoubleToString(VDouble);
    varDate:     result := Ansi7ToString(DateTimeToIso8601Text(VDate,' '));
    varString:   result := UTF8ToString(RawUTF8(VAny));
    {$ifdef HASVARUSTRING}
    varUString:  result := string(UnicodeString(VAny));
    {$endif HASVARUSTRING}
    varOleStr:   result := string(WideString(VAny));
    else result := fValue;
  end;
end;

function TQueryValue.GetVariant: Variant;
begin
  CheckValue;
  {$ifdef DELPHI5OROLDER}
  with TVarData(fValue) do // Delphi 5 need conversion to float to avoid overflow
  if VType=varInt64 then
    if (VInt64<low(Integer)) or (VInt64>high(Integer)) then
      result := VInt64*1.0 else
      result := integer(VInt64) else
  {$endif}
    result := fValue;
end;

procedure TQueryValue.SetAsBytes(const Value: TBytes);
begin
  CheckExists;
  RawByteStringToVariant(pointer(Value),length(Value),fValue);
  fValueBlob := true;
end;

procedure TQueryValue.SetAsWideString(const aValue: SynUnicode);
begin
  CheckExists;
  fValue := aValue;
end;

procedure TQueryValue.SetBlob(const aValue: TBlobData);
begin
  CheckExists;
  RawByteStringToVariant(aValue,fValue);
  fValueBlob := true;
end;

procedure TQueryValue.SetBoolean(const aValue: Boolean);
begin
  CheckExists;
  fValue := ord(aValue); // store 0/1 in the DB, not 0/65535
end;

procedure TQueryValue.SetBound(const aValue: Boolean);
begin
  ; // just do nothing
end;

procedure TQueryValue.SetCurrency(const aValue: Currency);
begin
  CheckExists;
  fValue := aValue;
end;

procedure TQueryValue.SetDateTime(const aValue: TDateTime);
begin
  CheckExists;
  fValue := aValue;
end;

procedure TQueryValue.SetDouble(const aValue: double);
begin
  CheckExists;
  fValue := aValue;
end;

procedure TQueryValue.SetInt64(const aValue: Int64);
begin
  CheckExists;
{$ifdef DELPHI5OROLDER}
  with TVarData(fValue) do begin
    VarClear(fValue);
    VType := varInt64;
    VInt64 := aValue;
  end;
{$else}
  fValue := aValue;
{$endif}
end;

procedure TQueryValue.SetInteger(const aValue: integer);
begin
  CheckExists;
  fValue := aValue;
end;

procedure TQueryValue.SetString(const aValue: string);
begin
  CheckExists;
  {$ifdef UNICODE}
  fValue := aValue;
  {$else}
  VarClear(fValue);
  with TVarData(fValue) do begin
    VType := varString;
    VAny := nil; // avoid GPF below when assigning a string variable to VAny
    StringToUTF8(aValue,RawUTF8(VAny));
  end;
  {$endif}
end;

procedure TQueryValue.SetVariant(const aValue: Variant);
begin
  CheckExists;
  fValue := aValue;
end;


{ TQuery }

procedure TQuery.Close;
begin
  try
    if Assigned(fPrepared) then begin
      fPrepared.ReleaseRows;
      fPrepared := nil;
    end;
  finally
    //fSQL.Clear; // original TQuery expect SQL content to be preserved
    fParam.Clear;
    fParam.ReHash; // ensure no GPF if reOpen
    fResult.Clear;
    fResult.ReHash; // ensure no GPF if reOpen
    fRowIndex := -1; // =-1 if empty
  end;
end;

constructor TQuery.Create(aConnection: TSQLDBConnection);
begin
  inherited Create;
  fConnection := aConnection;
  fSQL := TStringList.Create;
  fSQL.OnChange := OnSQLChange;
  fParam.InitSpecific(TypeInfo(TQueryValueDynArray),fParams,djString,@fParamCount,true);
  fResult.InitSpecific(TypeInfo(TQueryValueDynArray),fResults,djString,@fResultCount,true);
end;

destructor TQuery.Destroy;
begin
  try
    Close;
  finally
    fSQL.Free;
    inherited;
  end;
end;

procedure TQuery.Prepare;
begin
  // just available for compatibility purpose
end;

procedure TQuery.ExecSQL;
begin
  Execute(false);
  Close;
end;

function TQuery.ExecSQLAndReturnUpdateCount: integer;
begin
  Execute(false);
  result := fPrepared.UpdateCount;
  Close;
end;

function TQuery.FieldByName(const aFieldName: string): PQueryValue;
var i: integer;
begin
  if self=nil then
    result := nil else begin
    i := fResult.FindHashed(aFieldName);
    if i<0 then
      raise ESQLQueryException.CreateUTF8(
        '%.FieldByName("%"): unknown field name',[self,aFieldName]) else
      result := @fResults[i];
  end;
end;

function TQuery.FindField(const aFieldName: string): TField;
var i: integer;
begin
  result := nil;
  if (self=nil) or (fRowIndex<=0) then // -1=empty, 0=eof, >=1 if row data
    exit;
  i := fResult.FindHashed(aFieldName);
  if i>=0 then
    result := @fResults[i];
end;

procedure TQuery.First;
begin
  if (self=nil) or (fPrepared=nil) then
    raise ESQLQueryException.Create('First: Invalid call');
  if fRowIndex<>1 then // perform only if cursor not already on first data row
    if fPrepared.Step(true) then
      // cursor sucessfully set to 1st row
      fRowIndex := 1 else
      // no row is available, or unable to seek first row -> empty result
      fRowIndex := -1; // =-1 if empty, =0 if eof, >=1 if cursor on row data
end;

function TQuery.GetEof: boolean;
begin
  result := (Self=nil) or (fRowIndex<=0);
end;

function TQuery.GetRecordCount: integer;
begin
  if IsEmpty then
    result := 0 else
    result := 1;
end;

function TQuery.GetBof: Boolean;
begin
  result := (Self<>nil) and (fRowIndex=1);
end;

function TQuery.GetIsEmpty: Boolean;
begin
  result := (Self=nil) or (fRowIndex<0); // =-1 if empty, =0 if eof
end;

function TQuery.GetActive: Boolean;
begin
  result := (self<>nil) and (fPrepared<>nil);
end;

function TQuery.GetFieldCount: integer;
begin
  if IsEmpty then
    result := 0 else
    result := fResultCount;
end;

function TQuery.GetParamCount: integer;
begin
  if IsEmpty then
    result := 0 else
    result := fParamCount;
end;

function TQuery.GetField(aIndex: integer): TField;
begin
  if (Self=nil) or (fRowIndex<0) or (cardinal(aIndex)>=cardinal(fResultCount)) then
    result := nil else
    result := @fResults[aIndex];
end;

function TQuery.GetParam(aIndex: integer): TParam;
begin
  if (Self=nil) or (cardinal(aIndex)>=cardinal(fParamCount)) then
    result := nil else
    result := @fParams[aIndex];
end;

function TQuery.GetSQLAsText: string;
begin
  if (self=nil) or (fPrepared=nil) then
    result := '' else
    result := Utf8ToString(fPrepared.Instance.GetSQLWithInlinedParams);
end;

procedure TQuery.OnSQLChange(Sender: TObject);
begin
  if (self<>nil) and (SQL.Count=0) then
    Close; // expected previous behavior
end;

procedure TQuery.Next;
begin
  if (self=nil) or (fPrepared=nil) then
    raise ESQLQueryException.Create('Next: Invalid call');
  Connection.InternalProcess(speActive);
  try
    if fPrepared.Step(false) then
      inc(fRowIndex) else
      // no more row is available
      fRowIndex := 0;
  finally
    Connection.InternalProcess(speNonActive);
  end;
end;

procedure TQuery.Open;
var i, h: integer;
    added: boolean;
    ColumnName: string;
begin
  if fResultCount>0 then
    Close;
  Execute(true);
  for i := 0 to fPrepared.ColumnCount-1 do begin
    ColumnName := UTF8ToString(fPrepared.ColumnName(i));
    h := fResult.FindHashedForAdding(ColumnName,added);
    if not added then
      raise ESQLQueryException.CreateUTF8('Duplicated column name [%]',[ColumnName]);
    with fResults[h] do begin
      fQuery := self;
      fRowIndex := 0;
      fColumnIndex := i;
      fName := ColumnName;
    end;
  end;
  if fResultCount<>fPrepared.ColumnCount then
    raise ESQLQueryException.CreateUTF8('%.Open count %<>%',[self,fResultCount,fPrepared.ColumnCount]);
  First; // always read the first row
end;

function TQuery.ParamByName(const aParamName: string;
  CreateIfNotExisting: boolean): PQueryValue;
var i: integer;
    added: boolean;
begin
  if CreateIfNotExisting then begin
    i := fParam.FindHashedForAdding(aParamName,added);
    result := @fParams[i];
    if added then
      result^.fName := aParamName;
  end else begin
    i := fParam.FindHashed(aParamName);
    if i>=0 then
      result := @fParams[i] else
      result := nil;
  end;
end;

procedure TQuery.Execute(ExpectResults: Boolean);
const
  DB2OLE: array[TParamType] of TSQLDBParamInOutType = (
     paramIn,  paramIn, paramOut, paramInOut,    paramIn);
 // ptUnknown, ptInput, ptOutput, ptInputOutput, ptResult
var req, new, tmp: RawUTF8;
    paramName: string; // just like TQueryValue.Name: string
    P, B: PUTF8Char;
    col, i: Integer;
    cols: TIntegerDynArray;
begin
  if (self=nil) or (fResultCount>0) or
     (fConnection=nil) or (fPrepared<>nil) then
    raise ESQLQueryException.Create('TQuery.Prepare called with no previous Close');
  fRowIndex := -1;
  if fConnection=nil then
    raise ESQLQueryException.Create('No Connection to DB specified');
  req := Trim(StringToUTF8(SQL.Text));
  P := pointer(req);
  if P=nil then
    raise ESQLQueryException.Create('No SQL statement');
  col := 0;
  repeat
    B := P;
    while not (P^ in [':',#0]) do begin
      case P^ of
      '''': begin
        repeat // ignore chars inside ' quotes
          inc(P);
        until (P[0]=#0) or ((P[0]='''')and(P[1]<>''''));
        if P[0]=#0 then break;
        end;
      #1..#31:
        P^ := ' '; // convert #13/#10 into ' '
      end;
      inc(P);
    end;
    FastSetString(tmp,B,P-B);
    if P^=#0 then begin
      new := new+tmp;
      break;
    end;
    new := new+tmp+'?';
    inc(P); // jump ':'
    B := P;
    while tcIdentifier in TEXT_CHARS[P^] do
      inc(P); // go to end of parameter name
    paramName := UTF8DecodeToString(B,P-B);
    i := fParam.FindHashed(paramName);
    if i<0 then
      raise ESQLQueryException.CreateUTF8('Parameter [%] not bound for [%]',[paramName,req]);
    if col=length(cols) then
      SetLength(cols,col+64);
    cols[col] := i;
    inc(col);
  until P^=#0;
  Connection.InternalProcess(speActive);
  try
    fPrepared := Connection.NewStatementPrepared(new,ExpectResults,
      {raiseexc=}false,{allowreconnect=}false);
    if fPrepared=nil then
      try
        if Connection.LastErrorWasAboutConnection then begin
          SynDBLog.Add.Log(sllDB,'TQuery.Execute() now tries to reconnect');
          Connection.Disconnect;
          Connection.Connect;
          fPrepared := Connection.NewStatementPrepared(new,ExpectResults,false,false);
          if fPrepared=nil then
            raise ESQLQueryException.CreateFromError('Unable to reconnect DB',Connection);
        end else
          raise ESQLQueryException.CreateFromError('DB Error',Connection);
      finally
        if fPrepared=nil then
          Connection.InternalProcess(speConnectionLost);
      end;
    for i := 0 to col-1 do
      try
        with fParams[cols[i]] do // the leftmost SQL parameter has an index of 1
          fPrepared.BindVariant(i+1,fValue,fValueBlob,DB2OLE[fParamType]);
      except
        on E: Exception do
          raise ESQLQueryException.CreateUTF8(
            '% [%] when binding value for parameter [%] in [%]',
            [E,E.Message,fParams[cols[i]].fName,req]);
      end;
    fPrepared.ExecutePrepared;
  finally
    Connection.InternalProcess(speNonActive);
  end;
end;

{$endif EMULATES_TQUERY}


{ TSQLDBConnection }

procedure TSQLDBConnection.CheckConnection;
begin
  if self=nil then
    raise ESQLDBException.Create('TSQLDBConnection not created');
  if not Connected then
    raise ESQLDBException.CreateUTF8('% on %/% should be connected',
      [self,Properties.ServerName,Properties.DataBaseName]);
end;

procedure TSQLDBConnection.InternalProcess(Event: TOnSQLDBProcessEvent);
begin
  if (self=nil) or not Assigned(OnProcess) then
    exit;
  case Event of // thread-safe handle of speActive/peNonActive nested calls
  speActive:
    if InterlockedIncrement(fInternalProcessActive)=1 then
      OnProcess(self,Event);
  speNonActive:
    if InterlockedDecrement(fInternalProcessActive)=0 then
      OnProcess(self,Event);
  else
    OnProcess(self,Event);
  end;
end;

procedure TSQLDBConnection.Commit;
begin
  CheckConnection;
  if TransactionCount<=0 then
    raise ESQLDBException.CreateUTF8('Invalid %.Commit call',[self]);
  dec(fTransactionCount);
  InternalProcess(speCommit);
end;

constructor TSQLDBConnection.Create(aProperties: TSQLDBConnectionProperties);
begin
  fProperties := aProperties;
  if aProperties<>nil then begin
    fOnProcess := aProperties.OnProcess;
    fRollbackOnDisconnect := aProperties.RollbackOnDisconnect;
  end;
end;

procedure TSQLDBConnection.Connect;
var i: integer;
begin
  inc(fTotalConnectionCount);
  InternalProcess(speConnected);
  if fTotalConnectionCount>1 then
    InternalProcess(speReconnected);
  if fServerTimestampAtConnection=0 then
    try
      fServerTimestampAtConnection := ServerDateTime;
    except
      fServerTimestampAtConnection := Now;
    end;
  for i := 0 to length(fProperties.ExecuteWhenConnected)-1 do
    with NewStatement do
    try
      Execute(fProperties.ExecuteWhenConnected[i],false);
    finally
      Free;
    end;
end;

procedure TSQLDBConnection.Disconnect;
var i: PtrInt;
    Obj: PPointerArray;
begin
  InternalProcess(speDisconnected);
  if fCache<>nil then begin
    InternalProcess(speActive);
    try
      Obj := fCache.ObjectPtr;
      if Obj<>nil then
        for i := 0 to fCache.Count-1 do
          TSQLDBStatement(Obj[i]).FRefCount := 0; // force clean release
      FreeAndNil(fCache); // release all cached statements
    finally
      InternalProcess(speNonActive);
    end;
  end;
  if InTransaction then
    try
      if RollbackOnDisconnect then begin
        fTransactionCount := 1; // flush transaction nesting level
        Rollback;
      end;
    finally
      fTransactionCount := 0; // flush transaction nesting level
    end;
end;

destructor TSQLDBConnection.Destroy;
begin
  try
    Disconnect;
  except
    on E: Exception do
      SynDBLog.Add.Log(sllError,E);
  end;
  inherited;
end;

function TSQLDBConnection.IsOutdated(tix: Int64): boolean;
begin
  result := false;
  if (self=nil) or (fProperties.fConnectionTimeOutTicks=0) then
    exit;
  if fLastAccessTicks<0 then begin // was forced by ClearConnectionPool
    result := true;
    exit;
  end;
  if (fLastAccessTicks=0) or (tix-fLastAccessTicks<fProperties.fConnectionTimeOutTicks) then
    // brand new connection, or active enough connection
    fLastAccessTicks := tix else
    // notify connection is clearly outdated
    result := true;
end;

function TSQLDBConnection.GetInTransaction: boolean;
begin
  result := TransactionCount>0;
end;

function TSQLDBConnection.GetServerTimestamp: TTimeLog;
begin
  PTimeLogBits(@result)^.From(GetServerDateTime);
end;

function TSQLDBConnection.GetServerDateTime: TDateTime;
var Current: TDateTime;
begin
  Current := NowUTC; // so won't conflict with any potential time zone change
  if (fServerTimestampOffset=0) and
     (fProperties.fSQLGetServerTimestamp<>'') then begin
    with fProperties do
      with Execute(fSQLGetServerTimestamp,[]) do
        if Step then
        fServerTimestampOffset := ColumnDateTime(0)-Current;
    if fServerTimestampOffset=0 then
      fServerTimestampOffset := 0.000001; // request server only once
  end;
  result := Current+fServerTimestampOffset;
end;

function TSQLDBConnection.GetLastErrorWasAboutConnection: boolean;
begin
  result := (self<>nil) and (Properties<>nil) and (fErrorMessage<>'') and
    Properties.ExceptionIsAboutConnection(fErrorException,fErrorMessage);
end;

function TSQLDBConnection.NewStatementPrepared(const aSQL: RawUTF8;
  ExpectResults, RaiseExceptionOnError, AllowReconnect: Boolean): ISQLDBStatement;
var Stmt: TSQLDBStatement;
    ToCache: boolean;
    ndx,altern: integer;
    cachedSQL: RawUTF8;

  procedure TryPrepare(doraise: boolean);
  var Stmt: TSQLDBStatement;
  begin
    Stmt := nil;
    try
      InternalProcess(speActive);
      try
        Stmt := NewStatement;
        Stmt.Prepare(aSQL,ExpectResults);
        if ToCache then begin
          if fCache=nil then
            fCache := TRawUTF8List.Create([fObjectsOwned,fNoDuplicate,fCaseSensitive]);
          if fCache.AddObject(cachedSQL,Stmt)>=0 then
            Stmt._AddRef else // will be owned by fCache.Objects[]
            SynDBLog.Add.Log(sllWarning,'NewStatementPrepared: unexpected '+
              'cache duplicate for %',[Stmt.SQLWithInlinedParams],self);
        end;
        result := Stmt;
      finally
        InternalProcess(speNonActive);
      end;
    except
      on E: Exception do begin
        {$ifndef SYNDB_SILENCE}
        with SynDBLog.Add do
          if [sllSQL,sllDB,sllException,sllError]*Family.Level<>[] then
            LogLines(sllSQL,pointer(Stmt.SQLWithInlinedParams),self,'--');
        {$endif}
        Stmt.Free;
        result := nil;
        StringToUTF8(E.Message,fErrorMessage);
        fErrorException := PPointer(E)^;
        if doraise then
          raise;
      end;
    end;
  end;
begin
  result := nil;
  fErrorMessage := '';
  fErrorException := nil;
  if length(aSQL)<5 then
    exit;
  // first check if could be retrieved from cache
  cachedSQL := aSQL;
  ToCache := fProperties.IsCachable(Pointer(aSQL));
  if ToCache and (fCache<>nil) then begin
    ndx := fCache.IndexOf(cachedSQL);
    if ndx>=0 then begin
      Stmt := fCache.Objects[ndx];
      if Stmt.RefCount=1 then begin // ensure statement is not currently in use
        result := Stmt; // acquire the statement
        Stmt.Reset;
        exit;
      end else begin // in use -> create cached alternatives
        ToCache := false; // if all slots are used, won't cache this statement
        if fProperties.StatementCacheReplicates = 0 then
          SynDBLog.Add.Log(sllWarning, 'NewStatementPrepared: cached statement still in use ' +
            '-> you should release ISQLDBStatement ASAP [%]',[cachedSQL],self) else
          for altern := 1 to fProperties.StatementCacheReplicates do begin
            cachedSQL := aSQL+RawUTF8(AnsiChar(altern)); // safe SQL duplicate
            ndx := fCache.IndexOf(cachedSQL);
            if ndx>=0 then begin
              Stmt := fCache.Objects[ndx];
              if Stmt.RefCount=1 then begin
                result := Stmt;
                Stmt.Reset;
                exit;
              end;
            end else begin
              ToCache := true; // cache the statement in this void slot
              break;
            end;
          end;
      end;
    end;
  end;
  // not in cache (or not cachable) -> prepare now
  if fProperties.ReconnectAfterConnectionError and AllowReconnect then begin
    TryPrepare({doraise=}false);
    if result<>nil then
      exit; // success
    if LastErrorWasAboutConnection then
    try
      SynDBLog.Add.Log(sllDB, 'NewStatementPrepared: reconnect after %',[fErrorException],self);
      Disconnect;
      Connect;
      TryPrepare(RaiseExceptionOnError);
      if result=nil then begin
        SynDBLog.Add.Log(sllDB, 'NewStatementPrepared: unable to reconnect',self);
        InternalProcess(speConnectionLost);
      end;
    except
      if RaiseExceptionOnError then
        raise else
        result := nil;
    end
    else if RaiseExceptionOnError and (fErrorException<>nil) then
      // propagate error not related to connection (e.g. SQL syntax error)
      raise fErrorException.Create(UTF8ToString(fErrorMessage));
  end else
    // regular preparation, with no connection error interception
    TryPrepare(RaiseExceptionOnError);
end;

procedure TSQLDBConnection.Rollback;
begin
  CheckConnection;
  if TransactionCount<=0 then
    raise ESQLDBException.CreateUTF8('Invalid %.Rollback call',[self]);
  dec(fTransactionCount);
  InternalProcess(speRollback);
end;

procedure TSQLDBConnection.StartTransaction;
begin
  CheckConnection;
  inc(fTransactionCount);
  InternalProcess(speStartTransaction);
end;

function TSQLDBConnection.NewTableFromRows(const TableName: RawUTF8;
  Rows: TSQLDBStatement; WithinTransaction: boolean;
  ColumnForcedTypes: TSQLDBFieldTypeDynArray): integer;
var Fields: TSQLDBColumnCreateDynArray;
    aTableName, SQL: RawUTF8;
    Tables: TRawUTF8DynArray;
    Ins: TSQLDBStatement;
    i,n: integer;
begin
  result := 0;
  if (self=nil) or (Rows=nil) or (Rows.ColumnCount=0) then
    exit;
  aTableName := Properties.SQLTableName(TableName);
  if WithinTransaction then
    StartTransaction; // MUCH faster within a transaction
  try
    Ins := nil;
   InternalProcess(speActive);
    try
      while Rows.Step do begin
        // init when first row of data is available
        if Ins=nil then begin
          SQL := Rows.ColumnsToSQLInsert(aTableName,Fields);
          n := length(Fields);
          if Length(ColumnForcedTypes)<>n then begin
            SetLength(ColumnForcedTypes,n);
            for i := 0 to n-1 do
            case Fields[i].DBType of
            ftUnknown: ColumnForcedTypes[i] := ftInt64;
            ftNull:    ColumnForcedTypes[i] := ftBlob; // assume NULL is a BLOB
            else ColumnForcedTypes[i] := Fields[i].DBType;
            end;
          end;
          Properties.GetTableNames(Tables);
          if FindRawUTF8(Tables,TableName,false)<0 then
            with Properties do
              ExecuteNoResult(SQLCreate(aTableName,Fields,false),[]);
          Ins := NewStatement;
          Ins.Prepare(SQL,false);
        end;
        Rows.ReleaseRows;
        // write row data
        Ins.BindFromRows(ColumnForcedTypes,Rows);
        Ins.ExecutePrepared;
        Ins.Reset;
        inc(result);
      end;
      if WithinTransaction then
        Commit;
    finally
      Ins.Free;
      InternalProcess(speNonActive);
    end;
  except
    on Exception do begin
      if WithinTransaction then
        Rollback;
      raise;
    end;
  end;
end;

{$ifdef WITH_PROXY}

const
  REMOTE_MAGIC = 1;

type
  TRemoteMessageHeader = packed record
    Magic: byte;
    SessionID: integer;
    Command: TSQLDBProxyConnectionCommand;
  end;
  PRemoteMessageHeader = ^TRemoteMessageHeader;

constructor TSQLDBProxyConnectionProtocol.Create(aAuthenticate: TSynAuthenticationAbstract);
begin
  fAuthenticate := aAuthenticate;
  fTransactionRetryTimeout := 100;
  fTransactionActiveTimeout := 120000; // after 2 minutes, clear any transaction
  InitializeCriticalSection(fLock);
end;

function TSQLDBProxyConnectionProtocol.GetAuthenticate: TSynAuthenticationAbstract;
begin
  if self=nil then
    result := nil else
    result := fAuthenticate;
end;

function TSQLDBProxyConnectionProtocol.HandleInput(const input: RawByteString): RawByteString;
begin
  result := input;
end;

function TSQLDBProxyConnectionProtocol.HandleOutput(const output: RawByteString): RawByteString;
begin
  result := output;
end;

function TSQLDBProxyConnectionProtocol.TransactionStarted(connection: TSQLDBConnection;
  sessionID: integer): boolean;
var endTrial: Int64;
begin
  if sessionID=0 then
    raise ESQLDBRemote.Create('Remote transaction expects authentication/session');
  if connection.Properties.InheritsFrom(TSQLDBConnectionPropertiesThreadSafe) and
     (TSQLDBConnectionPropertiesThreadSafe(connection.Properties).ThreadingMode=tmThreadPool) then
    raise ESQLDBRemote.CreateUTF8('Remote transaction expects %.ThreadingMode<>tmThreadPool: '+
      'commit/execute/rollback should be in the same thread/connection',[connection.Properties]);
  endTrial := GetTickCount64+fTransactionRetryTimeout;
  repeat
    EnterCriticalSection(fLock);
    try
      if (fTransactionActiveAutoReleaseTicks<>0) and
         (GetTickCount64>fTransactionActiveAutoReleaseTicks) then
        try
          connection.Rollback;
        finally
          fTransactionSessionID := 0;
          fTransactionActiveAutoReleaseTicks := 0;
        end;
      result := fTransactionSessionID=0;
      if result then begin
        fTransactionSessionID := sessionID;
        fTransactionActiveAutoReleaseTicks := GetTickCount64+fTransactionActiveTimeout;
        connection.StartTransaction;
      end;
    finally
      LeaveCriticalSection(fLock);
    end;
    if result or (GetTickCount64>endTrial) then
      break;
    SleepHiRes(1);
  until false;
end;

procedure TSQLDBProxyConnectionProtocol.TransactionEnd(sessionID: integer);
begin
  if sessionID=0 then
    raise ESQLDBRemote.Create('Remote transaction expects authentication/session');
  EnterCriticalSection(fLock);
  try
    if sessionID<>fTransactionSessionID then
      raise ESQLDBRemote.CreateUTF8('Invalid %.TransactionEnd(%) - expected %',
        [self,sessionID,fTransactionSessionID]);
    fTransactionSessionID := 0;
    fTransactionActiveAutoReleaseTicks := 0;
  finally
    LeaveCriticalSection(fLock);
  end;
end;

destructor TSQLDBProxyConnectionProtocol.Destroy;
begin
  fAuthenticate.Free;
  DeleteCriticalSection(fLock);
  inherited Destroy;
end;

function TSQLDBRemoteConnectionProtocol.HandleInput(const input: RawByteString): RawByteString;
begin
  result := Input;
  SymmetricEncrypt(REMOTE_MAGIC,result);
  result := SynLZDecompress(result);
end;

function TSQLDBRemoteConnectionProtocol.HandleOutput(const output: RawByteString): RawByteString;
begin
  result := SynLZCompress(output);
  SymmetricEncrypt(REMOTE_MAGIC,result);
end;

procedure TSQLDBConnection.RemoteProcessMessage(const Input: RawByteString;
  out Output: RawByteString; Protocol: TSQLDBProxyConnectionProtocol);
var Stmt: ISQLDBStatement;
    Data: TRawByteStringStream;
    msgInput,msgOutput: RawByteString;
    header: PRemoteMessageHeader;
    O: PAnsiChar;
    i,session: Integer;
    user: RawUTF8;
    InputExecute: TSQLDBProxyConnectionCommandExecute;
    ExecuteWithResults: boolean;
    OutputSQLDBColumnDefineDynArray: TSQLDBColumnDefineDynArray;
    OutputSQLDBIndexDefineDynArray: TSQLDBIndexDefineDynArray;
    OutputRawUTF8DynArray: TRawUTF8DynArray;
procedure AppendOutput(value: Int64);
var len: integer;
begin
  len := Length(msgOutput);
  SetLength(msgOutput,len+sizeof(Int64));
  PInt64(@PByteArray(msgOutput)[len])^ := value;
end;
begin // follow TSQLDBRemoteConnectionPropertiesAbstract.Process binary layout
  if Protocol=nil then
    raise ESQLDBRemote.CreateUTF8('%.RemoteProcessMessage(protocol=nil)',[self]);
  msgInput := Protocol.HandleInput(Input);
  header := pointer(msgInput);
  if (header=nil) or (header.Magic<>REMOTE_MAGIC) then
    raise ESQLDBRemote.CreateUTF8('Wrong %.RemoteProcessMessage() input',[self]);
  if (Protocol.Authenticate<>nil) and (Protocol.Authenticate.UsersCount>0) and
     not (header.Command in [cGetToken,cGetDBMS]) then
    if not Protocol.Authenticate.SessionExists(header.SessionID) then
      raise ESQLDBRemote.Create('You do not have the right to be here');
  O := pointer(msgInput);
  inc(O,sizeof(header^));
  try
    msgOutput := copy(msgInput,1,SizeOf(header^));
    case header.Command of
    cGetToken:
      AppendOutput(Protocol.Authenticate.CurrentToken);
    cGetDBMS: begin
      session := 0;
      if (Protocol.Authenticate<>nil) and (Protocol.Authenticate.UsersCount>0) then begin
        GetNextItem(PUTF8Char(O),#1,user);
        session := Protocol.Authenticate.CreateSession(user,PCardinal(O)^);
        if session=0 then
          raise ESQLDBRemote.Create('Impossible to Open a Session - '+
           'check connection and User/Password');
      end;
      PRemoteMessageHeader(msgOutput)^.SessionID := session;
      msgOutput := msgOutput+AnsiChar(Properties.DBMS);
    end;
    cConnect:
      Connect;
    cDisconnect:
      Disconnect;
    cTryStartTransaction:
      msgOutput := msgOutput+AnsiChar(Protocol.TransactionStarted(self,header.SessionID));
    cCommit: begin
      Protocol.TransactionEnd(header.SessionID);
      Commit;
    end;
    cRollback: begin
      Protocol.TransactionEnd(header.SessionID);
      Rollback;
    end;
    cServerTimestamp:
      AppendOutput(ServerTimestamp);
    cGetFields: begin
      Properties.GetFields(O,OutputSQLDBColumnDefineDynArray);
      msgOutput := msgOutput+DynArraySave(
        OutputSQLDBColumnDefineDynArray,TypeInfo(TSQLDBColumnDefineDynArray));
    end;
    cGetIndexes:  begin
      Properties.GetIndexes(O,OutputSQLDBIndexDefineDynArray);
      msgOutput := msgOutput+DynArraySave(
        OutputSQLDBIndexDefineDynArray,TypeInfo(TSQLDBIndexDefineDynArray));
    end;
    cGetTableNames: begin
      Properties.GetTableNames(OutputRawUTF8DynArray);
      msgOutput := msgOutput+DynArraySave(OutputRawUTF8DynArray,TypeInfo(TRawUTF8DynArray));
    end;
    cGetForeignKeys: begin
      Properties.GetForeignKey('',''); // ensure Dest.fForeignKeys exists
      msgOutput := msgOutput+Properties.ForeignKeysData;
    end;
    cExecute, cExecuteToBinary, cExecuteToJSON, cExecuteToExpandedJSON: begin
      RecordLoad(InputExecute,O,TypeInfo(TSQLDBProxyConnectionCommandExecute));
      ExecuteWithResults := header.Command<>cExecute;
      Stmt := NewStatementPrepared(InputExecute.SQL,ExecuteWithResults,true);
      if fBlobAsNull in InputExecute.Force then
        Stmt.ForceBlobAsNull := true;
      if fDateWithMS in InputExecute.Force then
        Stmt.ForceDateWithMS := true;
      for i := 1 to Length(InputExecute.Params) do
      with InputExecute.Params[i-1] do
      if InputExecute.ArrayCount=0 then
        case VType of
          ftNull:     Stmt.BindNull(i,VInOut);
          ftInt64:    Stmt.Bind(i,VInt64,VInOut);
          ftDouble:   Stmt.Bind(i,unaligned(PDouble(@VInt64)^),VInOut);
          ftCurrency: Stmt.Bind(i,PCurrency(@VInt64)^,VInOut);
          ftDate:     Stmt.BindDateTime(i,PDateTime(@VInt64)^,VInOut);
          ftUTF8:     Stmt.BindTextU(i,VData,VInOut);
          ftBlob:     Stmt.BindBlob(i,VData,VInOut);
          else raise ESQLDBRemote.CreateUTF8(
            'Invalid VType=% parameter #% in %.ProcessExec(%)',
            [ord(VType),i,self,ToText(header.Command)^]);
        end else
        Stmt.BindArray(i,VType,VArray,InputExecute.ArrayCount);
      Stmt.ExecutePrepared;
      if ExecuteWithResults then begin
        Data := TRawByteStringStream.Create(msgOutput);
        try
          Data.Seek(0,soEnd); // include header
          case header.Command of
          cExecuteToBinary:
            Stmt.FetchAllToBinary(Data);
          cExecuteToJSON:
            Stmt.FetchAllToJSON(Data,false);
          cExecuteToExpandedJSON:
            Stmt.FetchAllToJSON(Data,true);
          end;
          msgOutput := Data.DataString;
        finally
          Data.Free;
        end;
      end else
        if not (fNoUpdateCount in InputExecute.Force) then
          msgOutput := msgOutput+ToUTF8(Stmt.UpdateCount);
    end;
    cQuit: begin
      if header.SessionID=Protocol.fTransactionSessionID then
        Protocol.TransactionEnd(header.SessionID);
      Protocol.Authenticate.RemoveSession(header.SessionID);
    end;
    else raise ESQLDBRemote.CreateUTF8(
      'Unknown %.RemoteProcessMessage() command %',[self,ord(header.Command)]);
    end;
  except
    on E: Exception do begin
      PRemoteMessageHeader(msgOutput)^.Command := cExceptionRaised;
      msgOutput := msgOutput+StringToUTF8(E.ClassName+#0+E.Message);
    end;
  end;
  Output := Protocol.HandleOutput(msgOutput);
end;

{$endif WITH_PROXY}


{ TSQLDBConnectionProperties }

constructor TSQLDBConnectionProperties.Create(const aServerName, aDatabaseName,
  aUserID, aPassWord: RawUTF8);
var aDBMS: TSQLDBDefinition;
begin
  fServerName := aServerName;
  fDatabaseName := aDatabaseName;
  fUserID := aUserID;
  fPassWord := aPassWord;
  fEngineName := EngineName;
  fRollbackOnDisconnect := true; // enabled by default
  fUseCache := true;
  fLoggedSQLMaxSize := 2048; // log up to 2KB of inlined SQL by default
  fStatementMaxMemory := 512 shl 20; // fetch to JSON/Binary up to 512MB
  SetInternalProperties; // virtual method used to override default parameters
  aDBMS := GetDBMS;
  if aDBMS in [dSQLite, dDB2, dPostgreSQL] then // for SQLDateToIso8601Quoted()
    fDateTimeFirstChar := ' ' else
    fDateTimeFirstChar := 'T';
  if fForcedSchemaName='' then
    case aDBMS of // should make every one life's easier
    dMSSQL:      fForcedSchemaName := 'dbo';
    dPostgreSql: fForcedSchemaName := 'public';
    end;
  if fSQLCreateField[ftUnknown]='' then
    fSQLCreateField := DB_FIELDS[aDBMS];
  if fSQLCreateFieldMax=0 then
    fSQLCreateFieldMax := DB_FIELDSMAX[aDBMS];
  if fSQLGetServerTimestamp='' then
    fSQLGetServerTimestamp := DB_SERVERTIME[aDBMS];
  case aDBMS of
  dMSSQL, dJet: fStoreVoidStringAsNull := true;
  end;
  if byte(fBatchSendingAbilities)=0 then // if not already handled by driver
    case aDBMS of
    dSQlite,dMySQL,dPostgreSQL,dNexusDB,dMSSQL,dDB2, // INSERT with multi VALUES
    //dFirebird,  EXECUTE BLOCK with params is slower (at least for embedded)
    dOracle: begin // Oracle expects weird INSERT ALL INTO ... statement
      fBatchSendingAbilities := [cCreate];
      fOnBatchInsert := MultipleValuesInsert;
      fBatchMaxSentAtOnce := 4096; // MultipleValuesInsert will do chunking
    end;
    dFirebird: begin // will run EXECUTE BLOCK without parameters
      fBatchSendingAbilities := [cCreate];
      fOnBatchInsert := MultipleValuesInsertFirebird;
      fBatchMaxSentAtOnce := 4096; // MultipleValuesInsert will do chunking
    end;
    end;
end;

destructor TSQLDBConnectionProperties.Destroy;
begin
  fMainConnection.Free;
  inherited;
end;

function TSQLDBConnectionProperties.Execute(const aSQL: RawUTF8;
  const Params: array of const
  {$ifndef LVCL}{$ifndef DELPHI5OROLDER}; RowsVariant: PVariant{$endif}{$endif};
  ForceBlobAsNull: boolean): ISQLDBRows;
var Stmt: ISQLDBStatement;
begin
  Stmt := NewThreadSafeStatementPrepared(aSQL,true,true);
  Stmt.ForceBlobAsNull := ForceBlobAsNull;
  Stmt.Bind(Params);
  Stmt.ExecutePrepared;
  result := Stmt;
  {$ifndef LVCL}
  {$ifndef DELPHI5OROLDER}
  if RowsVariant<>nil then
    if result=nil then
      SetVariantNull(RowsVariant^) else
      RowsVariant^ := result.RowData;
  {$endif}
  {$endif}
end;

function TSQLDBConnectionProperties.ExecuteNoResult(const aSQL: RawUTF8;
  const Params: array of const): integer;
var Stmt: ISQLDBStatement;
begin
  Stmt := NewThreadSafeStatementPrepared(aSQL,false,true);
  Stmt.Bind(Params);
  Stmt.ExecutePrepared;
  try
    result := Stmt.UpdateCount;
  except // may occur e.g. for Firebird's CREATE DATABASE
    result := 0;
  end;
end;

function TSQLDBConnectionProperties.PrepareInlined(const aSQL: RawUTF8; ExpectResults: Boolean): ISQLDBStatement;
var Query: ISQLDBStatement;
    i, maxParam: integer;
    Types: TSQLParamTypeDynArray;
    Nulls: TSQLFieldBits;
    Values: TRawUTF8DynArray;
    GenericSQL: RawUTF8;
begin
  result := nil; // returns nil interface on error
  if self=nil then
    exit;
  // convert inlined :(1234): parameters into Values[] for Bind*() calls
  GenericSQL := ExtractInlineParameters(aSQL,Types,Values,maxParam,Nulls);
  Query := NewThreadSafeStatementPrepared(GenericSQL,ExpectResults,true);
  if Query=nil then
    exit;
  for i := 0 to maxParam-1 do
  if i in Nulls then
    Query.BindNull(i+1) else
    case Types[i] of // returned sftInteger,sftFloat,sftUTF8Text,sftBlob,sftUnknown
      sptInteger:  Query.Bind(i+1,GetInt64(pointer(Values[i])));
      sptFloat:    Query.Bind(i+1,GetExtended(pointer(Values[i])));
      sptText:     Query.BindTextU(i+1,Values[i]);
      sptBlob:     if Values[i]='' then
                     Query.BindNull(i+1) else
                     Query.BindBlob(i+1,pointer(Values[i]),length(Values[i]));
      sptDateTime: Query.BindDateTime(i+1,Iso8601ToDateTime(Values[i]));
      else raise ESQLDBException.CreateUTF8(
        '%.PrepareInlined: Unrecognized parameter Type[%] = % in [%]',
        [self,i+1,ord(Types[i]),aSQL]);
    end;
  result := Query;
end;

function TSQLDBConnectionProperties.PrepareInlined(const SQLFormat: RawUTF8;
  const Args: array of const; ExpectResults: Boolean): ISQLDBStatement;
begin
  result := PrepareInlined(FormatUTF8(SQLFormat,Args),ExpectResults);
end;

function TSQLDBConnectionProperties.ExecuteInlined(const aSQL: RawUTF8;
  ExpectResults: Boolean): ISQLDBRows;
var Query: ISQLDBStatement;
begin
  result := nil; // returns nil interface on error
  if self=nil then
    exit;
  Query := PrepareInlined(aSQL,ExpectResults);
  if Query=nil then
    exit; // e.g. invalid aSQL
  Query.ExecutePrepared;
  result := Query;
end;

function TSQLDBConnectionProperties.ExecuteInlined(const SQLFormat: RawUTF8;
  const Args: array of const; ExpectResults: Boolean): ISQLDBRows;
begin
  result := ExecuteInlined(FormatUTF8(SQLFormat,Args),ExpectResults);
end;

procedure TSQLDBConnectionProperties.SetConnectionTimeOutMinutes(minutes: cardinal);
begin
  fConnectionTimeOutTicks := minutes*60000; // minutes to ms conversion
end;

function TSQLDBConnectionProperties.GetConnectionTimeOutMinutes: cardinal;
begin
  result := fConnectionTimeOutTicks div 60000;
end;

function TSQLDBConnectionProperties.GetMainConnection: TSQLDBConnection;
begin
  if fMainConnection.IsOutdated(GetTickCount64) then
    FreeAndNil(fMainConnection);
  if fMainConnection=nil then
    fMainConnection := NewConnection;
  result := fMainConnection;
end;

function TSQLDBConnectionProperties.NewConnection: TSQLDBConnection;
begin
  raise ESQLDBException.CreateUTF8('%.NewConnection',[self]);
end;

function TSQLDBConnectionProperties.ThreadSafeConnection: TSQLDBConnection;
begin
  result := MainConnection; // provider should be thread-safe
end;

procedure TSQLDBConnectionProperties.ClearConnectionPool;
begin
  FreeAndNil(fMainConnection);
end;

function TSQLDBConnectionProperties.NewThreadSafeStatement: TSQLDBStatement;
begin
  result := ThreadSafeConnection.NewStatement;
end;

function TSQLDBConnectionProperties.NewThreadSafeStatementPrepared(
  const aSQL: RawUTF8; ExpectResults, RaiseExceptionOnError: Boolean): ISQLDBStatement;
begin
  result := ThreadSafeConnection.NewStatementPrepared(
    aSQL,ExpectResults,RaiseExceptionOnError);
end;

function TSQLDBConnectionProperties.NewThreadSafeStatementPrepared(
  const SQLFormat: RawUTF8; const Args: array of const;
  ExpectResults, RaiseExceptionOnError: Boolean): ISQLDBStatement;
begin
  result := NewThreadSafeStatementPrepared(FormatUTF8(SQLFormat,Args),
    ExpectResults,RaiseExceptionOnError);
end;

function TSQLDBConnectionProperties.SharedTransaction(SessionID: cardinal;
  action: TSQLDBSharedTransactionAction): TSQLDBConnection;

  procedure SetResultToSameConnection(index: integer);
  begin
    result := ThreadSafeConnection;
    if result<>fSharedTransactions[index].Connection then
      raise ESQLDBException.CreateUTF8(
        '%.SharedTransaction(sessionID=%) with mixed thread connections: % and %',
          [self,SessionID,result,fSharedTransactions[index].Connection]);
  end;

var i,n: integer;
begin
  n := Length(fSharedTransactions);
  try
    for i := 0 to n-1 do
    if fSharedTransactions[i].SessionID=SessionID then begin
      SetResultToSameConnection(i);
      case action of
      transBegin: // nested StartTransaction
        InterlockedIncrement(fSharedTransactions[i].RefCount);
      else begin  // (nested) commit/rollback
        if InterlockedDecrement(fSharedTransactions[i].RefCount)=0 then begin
          dec(n);
          MoveFast(fSharedTransactions[i+1],fSharedTransactions[i],(n-i)*sizeof(fSharedTransactions[0]));
          SetLength(fSharedTransactions,n);
          case action of
          transCommitWithException, transCommitWithoutException:
            result.Commit;
          transRollback:
            result.Rollback;
          end;
        end;
      end;
      end;
      exit;
    end;
    case action of
    transBegin: begin
      result := ThreadSafeConnection;
      for i := 0 to n-1 do
        if fSharedTransactions[i].Connection=result then
        raise ESQLDBException.CreateUTF8(
          '%.SharedTransaction(sessionID=%) already started for sessionID=%',
          [self,SessionID,fSharedTransactions[i].SessionID]);
      if not result.Connected then
        result.Connect;
      result.StartTransaction;
      SetLength(fSharedTransactions,n+1);
      fSharedTransactions[n].SessionID := SessionID;
      fSharedTransactions[n].RefCount := 1;
      fSharedTransactions[n].Connection := result;
    end else
      raise ESQLDBException.CreateUTF8(
        'Unexpected %.SharedTransaction(%,%)',[self,SessionID,ord(action)]);
    end;
  except
    on Exception do begin
      result := nil; // result.StartTransaction/Commit/Rollback failed
      if action=transCommitWithException then
        raise;
    end;
  end;
end;


procedure TSQLDBConnectionProperties.SetInternalProperties;
begin
  // nothing to do yet
end;

procedure TSQLDBConnectionProperties.SetSchemaNameToOwner(out Owner: RawUTF8);
begin
  if fForcedSchemaName='' then
    case fDBMS of
    dMySql:    Owner := DatabaseName;
    dInformix: Owner := '';
    else       Owner := UserID;
    end
  else Owner := fForcedSchemaName;
end;

function TSQLDBConnectionProperties.IsCachable(P: PUTF8Char): boolean;
var NoWhere: Boolean;
begin // cachable if with ? parameter or SELECT without WHERE clause
  if (P<>nil) and fUseCache then begin
    while P^ in [#1..' '] do inc(P);
    NoWhere := IdemPChar(P,'SELECT ');
    if NoWhere or not (IdemPChar(P,'CREATE ') or IdemPChar(P,'ALTER ')) then begin
      result := true;
      while P^<>#0 do begin
        if P^='"' then begin // ignore chars within quotes
          repeat inc(P) until P^ in [#0,'"'];
          if P^=#0 then break;
        end else
        if P^='?' then
          exit else
        if (P^=' ') and IdemPChar(P+1,'WHERE ') then
          NoWhere := false;
        inc(P);
      end;
    end;
    result := NoWhere;
  end else
    result := false;
end;

class function TSQLDBConnectionProperties.GetFieldDefinition(
  const Column: TSQLDBColumnDefine): RawUTF8;
begin
  with Column do begin
    FormatUTF8('% [%',[ColumnName,ColumnTypeNative],result);
    if (ColumnLength<>0) or (Column.ColumnPrecision<>0) or (Column.ColumnScale<>0) then
      result := FormatUTF8('% % % %]',[result,ColumnLength,ColumnPrecision,ColumnScale]) else
      result := result+']';
    if ColumnIndexed then
      result := result+' *';
  end;
end;

class function TSQLDBConnectionProperties.GetFieldORMDefinition(
  const Column: TSQLDBColumnDefine): RawUTF8;
begin // 'Name: RawUTF8 index 20 read fName write fName;';
  with Column do begin
    FormatUTF8('property %: %',[ColumnName,SQLDBFIELDTYPE_TO_DELPHITYPE[ColumnType]],result);
    if (ColumnType=ftUTF8) and (ColumnLength>0) then
      result := FormatUTF8('% index %',[result,ColumnLength]);
    result := FormatUTF8('% read f% write f%;',[result,ColumnName,ColumnName]);
  end;
end;

var
  DB_KEYWORDS: array[TSQLDBDefinition] of TRawUTF8DynArray;

class function TSQLDBConnectionProperties.IsSQLKeyword(
  aDB: TSQLDBDefinition; aWord: RawUTF8): boolean;
const
  /// CSV of the known reserved keywords per database engine, in alphabetic order
  DB_KEYWORDS_CSV: array[TSQLDBDefinition] of PUTF8Char = (
  // dUnknown
  '',
  // dDefault = ODBC / SQL-92 keywords (always checked first)
  'absolute,action,ada,add,all,allocate,alter,and,any,are,as,asc,assertion,at,authorization,'+
  'avg,begin,between,bit,bit_length,both,by,cascade,cascaded,case,cast,catalog,char,'+
  'char_length,character,character_length,check,close,coalesce,collate,collation,'+
  'column,commit,connect,connection,constraint,constraints,continue,convert,'+
  'corresponding,count,create,cross,current,current_date,current_time,'+
  'current_timestamp,current_user,cursor,date,day,deallocate,dec,decimal,declare,'+
  'default,deferrable,deferred,delete,desc,describe,descriptor,diagnostics,disconnect,'+
  'distinct,domain,double,drop,else,end,end-exec,escape,except,exception,exec,execute,'+
  'exists,external,extract,false,fetch,first,float,for,foreign,fortran,found,from,full,get,'+
  'global,go,goto,grant,group,having,hour,identity,immediate,in,include,index,indicator,'+
  'initially,inner,input,insensitive,insert,int,integer,intersect,interval,into,is,'+
  'isolation,join,key,language,last,leading,left,level,like,local,lower,match,max,min,minute,'+
  'module,month,n,names,national,natural,nchar,next,no,none,not,null,nullif,numeric,'+
  'octet_length,of,on,only,open,option,or,order,outer,output,overlaps,pad,partial,pascal,'+
  'position,precision,prepare,preserve,primary,prior,privileges,procedure,public,read,'+
  'real,references,relative,restrict,revoke,right,rollback,rows,schema,scroll,second,'+
  'section,select,session,session_user,set,size,smallint,some,space,sql,sqlca,sqlcode,'+
  'sqlerror,sqlstate,sqlwarning,substring,sum,system_user,table,temporary,then,time,'+
  'timestamp,timezone_hour,timezone_minute,to,trailing,transaction,translate,'+
  'translation,trim,true,union,unique,unknown,update,upper,usage,user,using,value,values,'+
  'varchar,varying,view,when,whenever,where,with,work,write,year,zone',
  // dOracle specific keywords (in addition to dDefault)
  'access,audit,cluster,comment,compress,exclusive,file,identified,increment,initial,'+
  'lock,long,maxextents,minus,mode,noaudit,nocompress,nowait,number,offline,online,'+
  'pctfree',
  // dMSSQL specific keywords (in addition to dDefault)
  'admin,after,aggregate,alias,array,asensitive,asymmetric,atomic,backup,before,binary,'+
  'blob,boolean,breadth,break,browse,bulk,call,called,cardinality,checkpoint,class,clob,'+
  'clustered,collect,completion,compute,condition,constructor,contains,containstable,'+
  'corr,covar_pop,covar_samp,cube,cume_dist,current_catalog,'+
  'current_default_transform_group,current_path,current_role,current_schema,'+
  'current_transform_group_for_type,cycle,data,database,dbcc,deny,depth,deref,destroy,'+
  'destructor,deterministic,dictionary,disk,distributed,dump,dynamic,each,element,'+
  'equals,errlvl,every,exit,file,fillfactor,filter,free,freetext,freetexttable,'+
  'fulltexttable,function,fusion,general,grouping,hold,holdlock,host,identity_insert,'+
  'identitycol,if,ignore,initialize,inout,intersection,iterate,kill,large,lateral,less,'+
  'like_regex,limit,lineno,ln,load,localtime,localtimestamp,locator,map,member,merge,'+
  'method,mod,modifies,modify,multiset,nclob,new,nocheck,nonclustered,normalize,object,'+
  'occurrences_regex,off,offsets,old,opendatasource,openquery,openrowset,openxml,'+
  'operation,ordinality,out,over,overlay,parameter,parameters,partition,path,percent,'+
  'percent_rank,percentile_cont,percentile_disc,pivot,plan,position_regex,postfix,'+
  'prefix,preorder,print,proc,raiserror,range,reads,readtext,reconfigure,recursive,ref,'+
  'referencing,regr_avgx,regr_avgy,regr_count,regr_intercept,regr_r2,regr_slope,'+
  'regr_sxx,regr_sxy,regr_syy,release,replication,restore,result,return,returns,revert,'+
  'role,rollup,routine,row,rowcount,rowguidcol,rule,save,savepoint,scope,search,'+
  'securityaudit,semantickeyphrasetable,semanticsimilaritydetailstable,'+
  'semanticsimilaritytable,sensitive,sequence,sets,setuser,shutdown,similar,specific,'+
  'specifictype,sqlexception,start,state,statement,static,statistics,stddev_pop,'+
  'stddev_samp,structure,submultiset,substring_regex,symmetric,system,tablesample,'+
  'terminate,textsize,than,top,tran,translate_regex,treat,trigger,truncate,try_convert,'+
  'tsequal,uescape,under,unnest,unpivot,updatetext,use,var_pop,var_samp,variable,waitfor,'+
  'while,width_bucket,window,within,within group,without,writetext,xmlagg,'+
  'xmlattributes,xmlbinary,xmlcast,xmlcomment,xmlconcat,xmldocument,xmlelement,'+
  'xmlexists,xmlforest,xmliterate,xmlnamespaces,xmlparse,xmlpi,xmlquery,xmlserialize,'+
  'xmltable,xmltext,xmlvalidate',
  // dJet specific keywords (in addition to dDefault)
  'longtext,memo,money,note,number,oleobject,owneraccess,parameters,percent,pivot,short,'+
  'single,singlefloat,stdev,stdevp,string,tableid,text,top,transform,unsignedbyte,var,'+
  'varbinary,varp,yesno',
  // dMySQL specific keywords (in addition to dDefault)
  'accessible,analyze,asensitive,auto_increment,before,bigint,binary,blob,call,change,'+
  'condition,database,databases,day_hour,day_microsecond,day_minute,day_second,'+
  'delayed,deterministic,distinctrow,div,dual,each,elseif,enclosed,enum,escaped,exit,'+
  'explain,float4,float8,force,fulltext,general,high_priority,hour_microsecond,'+
  'hour_minute,hour_second,if,ignore,ignore_server_ids,infile,inout,int1,int2,int3,int4,'+
  'int8,iterate,keys,kill,leave,limit,linear,linear,lines,load,localtime,localtimestamp,'+
  'lock,long,longblob,longtext,loop,low_priority,master_heartbeat_period,'+
  'master_ssl_verify_server_cert,master_ssl_verify_server_cert,maxvalue,'+
  'mediumblob,mediumint,mediumtext,middleint,minute_microsecond,minute_second,mod,'+
  'modifies,no_write_to_binlog,optimize,optionally,out,outfile,purge,range,range,'+
  'read_only,read_only,read_write,read_write,reads,regexp,release,rename,repeat,replace,'+
  'require,resignal signal,return,rlike,schemas,second_microsecond,sensitive,'+
  'separator,show,slow,spatial,specific,sql_big_result,sql_calc_found_rows,'+
  'sql_small_result,sqlexception,ssl,starting,straight_join,terminated,text,tinyblob,'+
  'tinyint,tinytext,trigger,undo,unlock,unsigned,use,utc_date,utc_time,utc_timestamp,'+
  'varbinary,varcharacter,while,x509,xor,year_month,zerofillaccessible',
  // dSQLite keywords (dDefault is not added to this list)
  'abort,after,and,attach,before,cluster,conflict,copy,database,delete,delimiters,detach,'+
  'each,explain,fail,from,glob,ignore,insert,instead,isnull,limit,not,notnull,offset,or,'+
  'pragma,raise,replace,row,select,statement,temp,trigger,vacuum,where',
  // dFirebird specific keywords (in addition to dDefault)
  'active,after,ascending,base_name,before,blob,cache,check_point_length,computed,'+
  'conditional,containing,cstring,currency,database,debug,descending,deterministic,do,'+
  'entry_point,exit,file,filter,function,gdscode,gen_id,generator,'+
  'group_commit_wait_time,if,inactive,input_type,log_buffer_size,logfile,manual,'+
  'maximum_segment,merge,message,module_name,num_log_buffers,output_type,over,'+
  'overflow,page,page_size,pages,parameter,parent,password,plan,post_event,protected,'+
  'raw_partitions,rdb$db_key,record_version,reserv,reserving,retain,return,'+
  'returning_values,returns,segment,shadow,shared,singular,snapshot,sort,stability,'+
  'start,starting,starts,statistics,sub_type,suspend,trigger,type,variable,wait,while',
  // dNexusDB specific keywords (in addition to dDefault)
  'abs,achar,assert,astring,autoinc,blob,block,blocksize,bool,boolean,byte,bytearray,'+
  'ceiling,chr,datetime,dword,empty,exp,floor,grow,growsize,ignore,image,initial,'+
  'initialsize,kana,largeint,locale,log,money,nullstring,nvarchar,percent,power,rand,'+
  'round,shortint,sort,string,symbols,text,tinyint,top,type,use,width,word',
  // dPostgreSQL specific keywords (in addition to dDefault)
  'abort,access,admin,after,aggregate,also,always,analyse,analyze,array,assignment,'+
  'asymmetric,backward,before,bigint,binary,boolean,cache,called,chain,characteristics,'+
  'checkpoint,class,cluster,comment,committed,concurrently,configuration,content,'+
  'conversion,copy,cost,createdb,createrole,createuser,csv,current_role,cycle,database,'+
  'defaults,definer,delimiter,delimiters,dictionary,disable,discard,do,document,each,'+
  'enable,encoding,encrypted,enum,excluding,exclusive,explain,family,force,forward,'+
  'freeze,function,granted,greatest,handler,header,hold,if,ilike,immutable,implicit,'+
  'including,increment,indexes,inherit,inherits,inout,instead,invoker,isnull,'+
  'lancompiler,large,least,limit,listen,load,localtime,localtimestamp,location,lock,'+
  'login,mapping,maxvalue,minvalue,mode,move,new,nocreatedb,nocreaterole,nocreateuser,'+
  'noinherit,nologin,nosuperuser,nothing,notify,notnull,nowait,nulls,object,off,offset,'+
  'oids,old,operator,out,overlay,owned,owner,parser,password,placing,plans,prepared,'+
  'procedural,quote,reassign,recheck,reindex,release,rename,repeatable,replace,replica,'+
  'reset,restart,returning,returns,role,row,rule,savepoint,search,security,sequence,'+
  'serializable,setof,share,show,similar,simple,stable,standalone,start,statement,'+
  'statistics,stdin,stdout,storage,strict,strip,superuser,symmetric,sysid,system,'+
  'tablespace,temp,template,text,treat,trigger,truncate,trusted,type,uncommitted,'+
  'unencrypted,unlisten,until,vacuum,valid,validator,verbose,version,volatile,'+
  'whitespace,without,xml,xmlattributes,xmlconcat,xmlelement,xmlforest,xmlparse,xmlpi,'+
  'xmlroot,xmlserialize,yes',
  // dDB2 specific keywords (in addition to dDefault)
  'activate,document,dssize,dynamic,each,editproc,elseif,enable,encoding,encryption,'+
  'ending,erase,every,excluding,exclusive,exit,explain,fenced,fieldproc,file,final,free,'+
  'function,general,generated,graphic,handler,hash,hashed_value,hint,hold,hours,if,'+
  'including,inclusive,increment,inf,infinity,inherit,inout,integrity,isobid,iterate,jar,'+
  'java,keep,label,lateral,lc_ctype,leave,linktype,localdate,locale,localtime,'+
  'localtimestamp,locator,locators,lock,lockmax,locksize,long,loop,maintained,'+
  'materialized,maxvalue,microsecond,microseconds,minutes,minvalue,mode,modifies,'+
  'months,nan,new,new_table,nextval,nocache,nocycle,nodename,nodenumber,nomaxvalue,'+
  'nominvalue,noorder,normalized,nulls,numparts,obid,old,old_table,optimization,'+
  'optimize,out,over,overriding,package,padded,pagesize,parameter,part,partition,'+
  'partitioned,partitioning,partitions,password,path,piecesize,plan,prevval,priqty,'+
  'program,psid,query,queryno,range,rank,reads,recovery,referencing,refresh,release,'+
  'rename,repeat,reset,resignal,restart,result,result_set_locator,return,returns,role,'+
  'round_ceilingadd,round_downafter,round_flooralias,round_half_downall,'+
  'round_half_evenallocate,round_half_upallow,round_upalter,routineand,'+
  'row_numberas,rowany,rownumberasensitive,rowsassociate,rowsetasutime,rrnat,'+
  'runattributes,savepointaudit,schemaauthorization,scratchpadaux,scrollauxiliary,'+
  'searchbefore,secondbegin,secondsbetween,secqtybinary,securitybufferpool,selectby,'+
  'sensitivecache,sequencecall,session_usercapture,sessioncalled,setcardinality,'+
  'signalcascaded,simplecase,snancast,someccsid,sourcechar,specificcharacter,'+
  'sqlcheck,sqlidclone,stackedclose,standardcluster,startcollection,startingcollid,'+
  'statementcolumn,staticcomment,statmentcommit,stayconcat,stogroupcondition,'+
  'storesconnect,styleconnection,substringconstraint,summarycontains,'+
  'synonymcontinue,sysfuncount,sysibmcount_big,sysproccreate,system_usercurrent,'+
  'systemcross,tablecurrent_date,tablespacecurrent_lc_ctype,thencurrent_path,'+
  'timecurrent_schema,timestampcurrent_server,tocurrent_time,'+
  'transactioncurrent_timestamp,triggercurrent_timezone,trimcurrent_user,'+
  'truncatecursor,typecycle,undodata,uniondatabase,uniquedatapartitionname,'+
  'untildatapartitionnum,updatedate,usageday,userdays,usingdb2general,'+
  'validprocdb2genrl,valuedb2sql,valuesdbinfo,variabledbpartitionname,'+
  'variantdbpartitionnum,vcatdeallocate,versiondeclare,viewdefault,'+
  'volatiledefaults,volumesdefinition,whendelete,wheneverdense_rank,wheredenserank,'+
  'whiledescribe,withdescriptor,withoutdeter',
  // dInformix specific keywords (in addition to dDefault)
  '');
var db: TSQLDBDefinition;
begin // search using fast binary lookup in the alphabetic ordered arrays
  if DB_KEYWORDS[dDefault]=nil then
    for db := Low(DB_KEYWORDS) to high(DB_KEYWORDS) do
      CSVToRawUTF8DynArray(DB_KEYWORDS_CSV[db],DB_KEYWORDS[db]);
  aWord := Trim(LowerCase(aWord));
  if (aDB=dSQLite) or
     (FastFindPUTF8CharSorted(pointer(DB_KEYWORDS[dDefault]),
       high(DB_KEYWORDS[dDefault]),pointer(aWord))<0) then
    if aDB<=dDefault then
      result := false else
      result := FastFindPUTF8CharSorted(pointer(DB_KEYWORDS[aDB]),
        high(DB_KEYWORDS[aDB]),pointer(aWord))>=0 else
    result := true;
end;

function TSQLDBConnectionProperties.IsSQLKeyword(aWord: RawUTF8): boolean;
begin
  result := IsSQLKeyword(DBMS,aWord);
end;

procedure TSQLDBConnectionProperties.GetFieldDefinitions(const aTableName: RawUTF8;
  out Fields: TRawUTF8DynArray; WithForeignKeys: boolean);
var F: TSQLDBColumnDefineDynArray;
    Ref: RawUTF8;
    i: integer;
begin
  GetFields(aTableName,F);
  SetLength(Fields,length(F));
  for i := 0 to high(F) do begin
    Fields[i] := GetFieldDefinition(F[i]);
    if WithForeignKeys then begin
      Ref := GetForeignKey(aTableName,F[i].ColumnName);
      if Ref<>'' then
        Fields[i] := Fields[i] +' % '+Ref;
    end;
  end;
end;

procedure TSQLDBConnectionProperties.GetFields(const aTableName: RawUTF8;
  out Fields: TSQLDBColumnDefineDynArray);
var SQL: RawUTF8;
    n,i: integer;
    F: TSQLDBColumnDefine;
    FA: TDynArray;
begin
  FA.Init(TypeInfo(TSQLDBColumnDefineDynArray),Fields,@n);
  FA.Compare := SortDynArrayAnsiStringI; // FA.Find() case insensitive
  FillCharFast(F,sizeof(F),0);
  if fDBMS=dSQLite then begin // SQLite3 has a specific PRAGMA metadata query
    try
      with Execute('PRAGMA table_info(`'+aTableName+'`)',[]) do
      while Step do begin
        // cid=0,name=1,type=2,notnull=3,dflt_value=4,pk=5
        F.ColumnName := ColumnUTF8(1);
        F.ColumnTypeNative := ColumnUTF8(2);
        F.ColumnType := ColumnTypeNativeToDB(F.ColumnTypeNative,0);
        F.ColumnIndexed := (ColumnInt(5)=1); // by definition for SQLite3
        FA.Add(F);
      end;
    except
      on Exception do
        n := 0; // external SQLite3 providers (e.g. UniDAC) are buggy
    end;
    try
      with Execute('PRAGMA index_list(`'+aTableName+'`)',[]) do
      while Step do
        // seq=0,name=1,unique=2
        with Execute('PRAGMA index_info('+ColumnUTF8(1)+')',[]) do
          while Step do begin
            F.ColumnName := ColumnUTF8(2); // seqno=0,cid=1,name=2
            i := FA.Find(F);
            if i>=0 then
              Fields[i].ColumnIndexed := true;
          end;
    except
      on Exception do
        ; // ignore any exception if no index is defined
    end;
  end else begin
    SQL := SQLGetField(aTableName);
    if SQL='' then
      exit;
    with Execute(SQL,[]) do
      while Step do begin
        F.ColumnName := trim(ColumnUTF8(0));
        F.ColumnTypeNative := trim(ColumnUTF8(1));
        F.ColumnLength := ColumnInt(2);
        F.ColumnPrecision := ColumnInt(3);
        if ColumnNull(4) then // e.g. for plain NUMERIC in Oracle
          F.ColumnScale := -1 else
          F.ColumnScale := ColumnInt(4);
        F.ColumnType := ColumnTypeNativeToDB(F.ColumnTypeNative,F.ColumnScale);
        if ColumnInt(5)>0 then
          F.ColumnIndexed := true;
        FA.Add(F);
      end;
  end;
  SetLength(Fields,n);
end;

procedure TSQLDBConnectionProperties.GetIndexes(const aTableName: RawUTF8;
  out Indexes: TSQLDBIndexDefineDynArray);
var SQL: RawUTF8;
    n: integer;
    F: TSQLDBIndexDefine;
    FA: TDynArray;
begin
  SQL := SQLGetIndex(aTableName);
  if SQL='' then
    exit;
  FA.Init(TypeInfo(TSQLDBIndexDefineDynArray),Indexes,@n);
  with Execute(SQL,[]) do
    while Step do begin
      F.IndexName          := trim(ColumnUTF8(0));
      F.IsUnique           := ColumnInt (1)>0;
      F.TypeDesc           := trim(ColumnUTF8(2));
      F.IsPrimaryKey       := ColumnInt (3)>0;
      F.IsUniqueConstraint := ColumnInt (4)>0;
      F.Filter             := trim(ColumnUTF8(5));
      F.KeyColumns         := trim(ColumnUTF8(6));
      F.IncludedColumns    := trim(ColumnUTF8(7));
      FA.Add(F);
    end;
  SetLength(Indexes,n);
end;

procedure TSQLDBConnectionProperties.GetProcedureNames(out Procedures: TRawUTF8DynArray);
var SQL: RawUTF8;
    count: integer;
begin
  SQL := SQLGetProcedure;
  if SQL<>'' then
  try
    with Execute(SQL,[]) do begin
      count := 0;
      while Step do
        AddSortedRawUTF8(Procedures,count,trim(ColumnUTF8(0)));
      SetLength(Procedures,count);
    end;
  except
    on Exception do
      SetLength(Procedures,0); // if the supplied SQL query is wrong, just ignore
  end;
end;

procedure TSQLDBConnectionProperties.GetProcedureParameters(const aProcName: RawUTF8;
  out Parameters: TSQLDBProcColumnDefineDynArray);
var SQL: RawUTF8;
    n: integer;
    F: TSQLDBProcColumnDefine;
    FA: TDynArray;
begin
  FA.Init(TypeInfo(TSQLDBColumnDefineDynArray),Parameters,@n);
  FA.Compare := SortDynArrayAnsiStringI; // FA.Find() case insensitive
  FillcharFast(F,sizeof(F),0);
  SQL := SQLGetParameter(aProcName);
  if SQL='' then
    exit;
  with Execute(SQL,[]) do
    while Step do begin
      F.ColumnName := trim(ColumnUTF8(0));
      F.ColumnTypeNative := trim(ColumnUTF8(1));
      F.ColumnLength := ColumnInt(2);
      F.ColumnPrecision := ColumnInt(3);
      if ColumnNull(4) then // e.g. for plain NUMERIC in Oracle
        F.ColumnScale := -1 else
        F.ColumnScale := ColumnInt(4);
      F.ColumnType := ColumnTypeNativeToDB(F.ColumnTypeNative,F.ColumnScale);
      case FindCSVIndex('IN,OUT,INOUT',ColumnUTF8(5),',',false) of
        0: F.ColumnParamType := paramIn;
        2: F.ColumnParamType := paramInOut;
      else // any other is assumed as out
        F.ColumnParamType := paramOut;
      end;
      FA.Add(F);
    end;
  SetLength(Parameters,n);
end;

procedure TSQLDBConnectionProperties.GetTableNames(out Tables: TRawUTF8DynArray);
var SQL, table, checkschema: RawUTF8;
    count: integer;
begin
  SQL := SQLGetTableNames;
  if SQL<>'' then
  try
    if FilterTableViewSchemaName and (fForcedSchemaName<>'') then
      checkschema := UpperCase(fForcedSchemaName)+'.';
    with Execute(SQL,[]) do begin
      count := 0;
      while Step do begin
        table := trim(ColumnUTF8(0));
        if (checkschema='') or IdemPChar(pointer(table),pointer(checkschema)) then
          AddSortedRawUTF8(Tables,count,table);
      end;
      SetLength(Tables,count);
    end;
  except
    on Exception do
      SetLength(Tables,0); // if the supplied SQL query is wrong, just ignore
  end;
end;

procedure TSQLDBConnectionProperties.GetViewNames(out Views: TRawUTF8DynArray);
var SQL, table, checkschema: RawUTF8;
    count: integer;
begin
  SQL := SQLGetViewNames;
  if SQL<>'' then
  try
    if FilterTableViewSchemaName and (fForcedSchemaName<>'') then
      checkschema := UpperCase(fForcedSchemaName)+'.';
    with Execute(SQL,[]) do begin
      count := 0;
      while Step do begin
        table := trim(ColumnUTF8(0));
        if (checkschema='') or IdemPChar(pointer(table),pointer(checkschema)) then
          AddSortedRawUTF8(Views,count,table);
      end;
      SetLength(Views,count);
    end;
  except
    on Exception do
      SetLength(Views,0); // if the supplied SQL query is wrong, just ignore
  end;
end;

procedure TSQLDBConnectionProperties.SQLSplitTableName(const aTableName: RawUTF8;
  out Owner, Table: RawUTF8);
begin
  case fDBMS of
  dSQLite:
    Table := aTableName;
  else begin
    Split(aTableName,'.',Owner,Table);
    if Table='' then begin
      Table := Owner;
      if fForcedSchemaName='' then
        case fDBMS of
        dMySql:
          Owner := DatabaseName;
        else
          Owner := UserID;
        end else
        Owner := fForcedSchemaName;
    end;
  end;
  end;
end;

procedure TSQLDBConnectionProperties.SQLSplitProcedureName(
  const aProcName: RawUTF8; out Owner, Package, ProcName: RawUTF8);
var lOccur,i: Integer;
begin
  lOccur := 0;
  for i := 1 to length(aProcName) do
    if aProcName[i]='.' then
      inc(lOccur);
  if lOccur=0 then begin
    ProcName := aProcName;
    SetSchemaNameToOwner(Owner);
    Exit;
  end;
  case fDBMS of
  dSQLite:
    ProcName := aProcName;
  dOracle, dFirebird: begin // Firebird 3 has packages
    if lOccur=2 then begin // OWNER.PACKAGE.PROCNAME
      Split(aProcName,'.',Owner,Package);
      Split(Package,'.',Package,ProcName);
    end else begin // PACKAGE.PROCNAME
      Split(aProcName,'.',Package,ProcName);
      Owner := UserID;
    end;
  end else begin  // OWNER.PROCNAME
    Split(aProcName,'.',Owner,ProcName);
    if ProcName='' then begin
      ProcName := Owner;
      SetSchemaNameToOwner(Owner);
    end
    else if fDBMS=dMSSQL then
      // discard ;1 when MSSQL stored procedure name is ProcName;1
      Split(ProcName,';',ProcName);
  end;
  end;
end;

function TSQLDBConnectionProperties.SQLFullTableName(const aTableName: RawUTF8): RawUTF8;
begin
  if (aTableName<>'') and (fForcedSchemaName<>'') and (PosExChar('.',aTableName)=0) then
    result := fForcedSchemaName+'.'+aTableName else
    result := aTableName;
end;

function TSQLDBConnectionProperties.SQLGetField(const aTableName: RawUTF8): RawUTF8;
var Owner, Table: RawUTF8;
    FMT: RawUTF8;
begin
  result := '';
  case DBMS of
  dOracle: FMT :=
    'select c.column_name, c.data_type, c.data_length, c.data_precision, c.data_scale, '+
    ' (select count(*) from sys.all_indexes a, sys.all_ind_columns b'+
    '  where a.table_owner=c.owner and a.table_name=c.table_name and b.column_name=c.column_name'+
    '  and a.owner=b.index_owner and a.index_name=b.index_name and'+
    '  a.table_owner=b.table_owner and a.table_name=b.table_name) index_count'+
    ' from sys.all_tab_columns c'+
    ' where c.owner like ''%'' and c.table_name like ''%'';';
  dMSSQL, dMySQL, dPostgreSQL: FMT :=
    'select COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION,'+
    ' NUMERIC_SCALE, 0 INDEX_COUNT'+ // INDEX_COUNT=0 here (done via OleDB)
    ' from INFORMATION_SCHEMA.COLUMNS'+
    ' where UPPER(TABLE_SCHEMA) = ''%'' and UPPER(TABLE_NAME) = ''%''';
  dFirebird: begin
    result := // see http://edn.embarcadero.com/article/25259
      'select a.rdb$field_name, b.rdb$field_type || coalesce(b.rdb$field_sub_type, '''') as rdb$field_type,'+
      ' b.rdb$field_length, b.rdb$field_length, abs(b.rdb$field_scale) as rdb$field_scale,'+
      ' (select count(*) from rdb$indices i, rdb$index_segments s'+
      ' where i.rdb$index_name=s.rdb$index_name and i.rdb$index_name not like ''RDB$%'''+
      ' and i.rdb$relation_name=a.rdb$relation_name) as index_count '+
      'from rdb$relation_fields a left join rdb$fields b on a.rdb$field_source=b.rdb$field_name'+
      ' left join rdb$relations c on a.rdb$relation_name=c.rdb$relation_name '+
      'where a.rdb$relation_name='''+SynCommons.UpperCase(aTableName)+'''';
    exit;
  end;
  dNexusDB: begin
    result := 'select FIELD_NAME, FIELD_TYPE_SQL, FIELD_LENGTH, FIELD_UNITS,'+
    ' FIELD_DECIMALS, FIELD_INDEX from #fields where TABLE_NAME = '''+aTableName+'''';
    exit;
  end;
  else exit; // others (e.g. dDB2) will retrieve info from (ODBC) driver
  end;
  SQLSplitTableName(aTableName,Owner,Table);
  FormatUTF8(FMT,[SynCommons.UpperCase(Owner),SynCommons.UpperCase(Table)],result);
end;

function TSQLDBConnectionProperties.SQLGetIndex(const aTableName: RawUTF8): RawUTF8;
var Owner, Table: RawUTF8;
    FMT: RawUTF8;
begin
  result := '';
  case DBMS of
  dOracle: FMT :=
   'select  index_name, decode(max(uniqueness),''NONUNIQUE'',0,1) as is_unique, '+
   ' max(index_type) as type_desc, 0 as is_primary_key, 0 as unique_constraint, '+
   ' cast(null as varchar(100)) as index_filter, '+
   ' ltrim(max(sys_connect_by_path(column_name, '', '')), '', '') as key_columns, '+
   ' cast(null as varchar(100)) as included_columns '+
   'from '+
   '( select c.index_name as index_name, i.index_type, i.uniqueness, c.column_name, '+
   '   row_number() over (partition by c.table_name, c.index_name order by c.column_position) as rn '+
   '  from user_ind_columns c inner join user_indexes i on c.table_name = i.table_name and c.index_name = i.index_name '+
   '  where c.table_name = ''%'' '+
   ') start with rn = 1 connect by prior rn = rn - 1 and prior index_name = index_name '+
   'group by index_name order by index_name';
  dMSSQL: FMT :=
    'select i.name as index_name, i.is_unique, i.type_desc, is_primary_key, is_unique_constraint, '+
    '  i.filter_definition as index_filter, key_columns, included_columns as included_columns, '+
    '  t.name as table_name '+
    'from '+
    ' sys.tables t inner join sys.indexes i on i.object_id = t.object_id '+
    ' cross apply(select STUFF('+
    '   (select '',''+c.name from sys.index_columns ic '+
    '   inner join sys.columns c on c.object_id = t.object_id and c.column_id = ic.column_id '+
    '   where i.index_id = ic.index_id and i.object_id = ic.object_id and ic.is_included_column = 0 '+
    '   order by ic.key_ordinal for xml path('''') '+
    '   ),1,1,'''') as key_columns) AS c '+
    ' cross apply(select STUFF( '+
    '   (select '','' + c.name  from sys.index_columns ic '+
    '   inner join	sys.columns c on	c.object_id = t.object_id	and c.column_id = ic.column_id '+
    '   where i.index_id = ic.index_id and i.object_id = ic.object_id and ic.is_included_column = 1 '+
    '   order by ic.key_ordinal for xml path('''') '+
    '   ),1,1,'''') as included_columns) AS ic '+
    'where t.type = ''U'' and t.name like ''%''';
  dFirebird: FMT :=
    'select i.rdb$index_name, i.rdb$unique_flag, i.rdb$index_type, case rc.rdb$constraint_type '+
    ' when ''PRIMARY KEY'' then 1 else 0 end as is_primary_key, 0 as unique_constraint, '+
    ' null as index_filter, (select list(trim(rdb$field_name), '', '') from '+
    '  (select * from rdb$index_segments where rdb$index_name = i.rdb$index_name '+
    '  order by rdb$field_position)) as key_columns, null as included_columns '+
    'from rdb$indices i '+
    'left outer join rdb$relation_constraints rc on rc.rdb$index_name = i.rdb$index_name and '+
    '  rc.rdb$constraint_type=''PRIMARY KEY'' '+
    'where exists(select * from rdb$index_segments where rdb$index_name = i.rdb$index_name) and '+
    ' i.rdb$relation_name = ''%''';
  else exit; // others (e.g. dMySQL or dDB2) will retrieve info from (ODBC) driver
  end;
  Split(aTableName,'.',Owner,Table);
  if Table='' then begin
    Table := Owner;
    Owner := UserID;
  end;
  FormatUTF8(FMT,[SynCommons.UpperCase(Table)],result);
end;

function TSQLDBConnectionProperties.SQLGetParameter(const aProcName: RawUTF8): RawUTF8;
var Owner, Package, Proc: RawUTF8;
    FMT: RawUTF8;
begin
  result := '';
  SQLSplitProcedureName(aProcName,Owner,Package,Proc);
  case DBMS of
  dOracle: FMT :=
    'select a.argument_name, a.data_type, a.char_length, a.data_precision, a.data_scale, a.in_out ' +
    'from   sys.all_arguments a ' +
    'where  a.owner like ''%''' +
    '  and  a.package_name like ''' + SynCommons.UpperCase(Package) + '''' +
    '  and  a.object_name like ''%''' +
    ' order by position';
  dMSSQL, dMySQL, dPostgreSQL: FMT :=
    'select PARAMETER_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE, PARAMETER_MODE ' +
    'from INFORMATION_SCHEMA.PARAMETERS ' +
    'where UPPER(SPECIFIC_SCHEMA) = ''%'' and UPPER(SPECIFIC_NAME) = ''%'''+
    ' order by ORDINAL_POSITION';
  dFirebird: begin
    if (Package = '') then
      result :=
        'select a.rdb$parameter_name, b.rdb$field_type || coalesce(b.rdb$field_sub_type, '''') as rdb$field_type,' +
        ' b.rdb$field_length, b.rdb$field_precision, b.rdb$field_scale,' +
        ' case a.rdb$parameter_type when 0 then ''IN'' else ''OUT'' end ' +
        'from rdb$procedure_parameters a, rdb$fields b ' +
        'where b.rdb$field_name = a.rdb$field_source and a.rdb$procedure_name = ''' + SynCommons.UpperCase(Proc) + ''' ' +
        'order by a.rdb$parameter_number'
    else
      result :=
        'select a.rdb$parameter_name, b.rdb$field_type || coalesce(b.rdb$field_sub_type, '''') as rdb$field_type,' +
        ' b.rdb$field_length, b.rdb$field_precision, b.rdb$field_scale,' +
        ' case a.rdb$parameter_type when 0 then ''IN'' else ''OUT'' end ' +
        'from rdb$procedure_parameters a, rdb$fields b ' +
        'where b.rdb$field_name = a.rdb$field_source and a.rdb$package_name = ''' + SynCommons.UpperCase(Package) + ''' ' +
        '  and a.rdb$procedure_name = ''' + SynCommons.UpperCase(Proc) + ''' ' +
        'order by a.rdb$parameter_number';
    exit;
  end;
  dNexusDB: begin // NOT TESTED !!!
    result := 'select PROCEDURE_ARGUMENT_NAME, PROCEDURE_ARGUMENT_TYPE, PROCEDURE_ARGUMENT_UNITS,'+
    ' PROCEDURE_ARGUMENT_UNITS, PROCEDURE_ARGUMENT_DECIMALS, PROCEDURE_ARGUMENT_KIND,'+
    ' from #procedure_arguments where PROCEDURE_NAME = '''+aProcName+'''' +
    ' order by PROCEDURE_ARGUMENT_INDEX';
    exit;
  end;
  else exit; // others (e.g. dDB2) will retrieve info from (ODBC) driver
  end;
  FormatUTF8(FMT,[SynCommons.UpperCase(Owner),SynCommons.UpperCase(Proc)],result);
end;

function TSQLDBConnectionProperties.SQLGetProcedure: RawUTF8;
var FMT,Owner: RawUTF8;
begin
  result := '';
  case DBMS of
  dOracle: FMT :=
    'select' +
    '  case P.OBJECT_TYPE' +
    '  when ''PACKAGE'' then P.OBJECT_NAME || ''.'' || P.PROCEDURE_NAME' +
    '  else P.OBJECT_NAME end NAME_ROUTINE ' +
    'from SYS.ALL_PROCEDURES P ' +
    'where P.OWNER = ''%'' and P.SUBPROGRAM_ID > 0 ' +
    'order by NAME_ROUTINE';
  dMSSQL, dMySQL, dPostgreSQL: FMT :=
    'select R.SPECIFIC_NAME NAME_ROUTINE ' +
    'from INFORMATION_SCHEMA.ROUTINES R ' +
    'where UPPER(R.SPECIFIC_SCHEMA) = ''%'' '+
    'order by NAME_ROUTINE';
  dFirebird: FMT :=
    'select P.RDB$PROCEDURE_NAME NAME_ROUTINE ' +
    'from RDB$PROCEDURES P ' +
    'where P.RDB$OWNER_NAME = ''%'' ' +
    'order by NAME_ROUTINE';
  dNexusDB: begin // NOT TESTED !!!
    result := 'select P.PROCEDURE_NAME NAME_ROUTINE '+
    'from #PROCEDURES P ' +
    'order by NAME_ROUTINE';
    exit;
  end;
  else exit; // others (e.g. dDB2) will retrieve info from (ODBC) driver
  end;
  SetSchemaNameToOwner(Owner);
  FormatUTF8(FMT,[SynCommons.UpperCase(Owner)],result);
end;

function TSQLDBConnectionProperties.SQLGetTableNames: RawUTF8;
begin
  case DBMS of
  dOracle: result := 'select owner||''.''||table_name name '+
    'from sys.all_tables order by owner, table_name';
  dMSSQL:
    result := 'select (TABLE_SCHEMA + ''.'' + TABLE_NAME) as name '+
      'from INFORMATION_SCHEMA.TABLES where TABLE_TYPE=''BASE TABLE'' order by name';
  dMySQL:
    result := 'select concat(TABLE_SCHEMA,''.'',TABLE_NAME) as name '+
      'from INFORMATION_SCHEMA.TABLES where TABLE_TYPE=''BASE TABLE'' order by name';
  dPostgreSQL:
    result := 'select (TABLE_SCHEMA||''.''||TABLE_NAME) as name '+
      'from INFORMATION_SCHEMA.TABLES where TABLE_TYPE=''BASE TABLE'' order by name';
  dSQLite: result := 'select name from sqlite_master where type=''table'' '+
     'and name not like ''sqlite_%''';
  dFirebird: result := 'select rdb$relation_name from rdb$relations '+
    'where rdb$view_blr is null and (rdb$system_flag is null or rdb$system_flag=0)';
  dNexusDB: result := 'select table_name name from #tables order by table_name';
  else result := ''; // others (e.g. dDB2) will retrieve info from (ODBC) driver
  end;
end;

function TSQLDBConnectionProperties.SQLGetViewNames: RawUTF8;
begin
  case DBMS of
  dOracle: result := 'select owner||''.''||view_name name '+
    'from sys.all_views order by owner, view_name';
  dMSSQL:
    result := 'select (TABLE_SCHEMA + ''.'' + TABLE_NAME) as name '+
      'from INFORMATION_SCHEMA.VIEWS order by name';
  dMySQL:
    result := 'select concat(TABLE_SCHEMA,''.'',TABLE_NAME) as name '+
      'from INFORMATION_SCHEMA.VIEWS order by name';
  dPostgreSQL:
    result := 'select (TABLE_SCHEMA||''.''||TABLE_NAME) as name '+
      'from INFORMATION_SCHEMA.VIEWS order by name';
  dSQLite: result := 'select name from sqlite_master where type=''view'' '+
     'and name not like ''sqlite_%''';
  dFirebird: result := 'select rdb$relation_name from rdb$relations '+
    'where rdb$view_blr is not null and (rdb$system_flag is null or rdb$system_flag=0)';
  dNexusDB: result := 'select view_name name from #views order by view_name'; // NOT TESTED !!!
  else result := ''; // others (e.g. dDB2) will retrieve info from (ODBC) driver
  end;
end;

function TSQLDBConnectionProperties.SQLCreateDatabase(const aDatabaseName: RawUTF8;
  aDefaultPageSize: integer): RawUTF8;
begin
  case DBMS of
  dFirebird: begin
    if (aDefaultPageSize<>8192) or (aDefaultPageSize<>16384) then
      aDefaultPageSize := 4096;
    FormatUTF8('create database ''%'' user ''sysdba'' password ''masterkey'''+
      ' page_size % default character set utf8;',[aDatabaseName,aDefaultPageSize],result);
  end;
  else result := '';
  end;
end;

function TSQLDBConnectionProperties.ColumnTypeNativeToDB(
  const aNativeType: RawUTF8; aScale: integer): TSQLDBFieldType;
function ColumnTypeNativeDefault: TSQLDBFieldType;
const
  DECIMAL=18; // change it if you update PCHARS[] below before 'DECIMAL'
  NUMERIC=DECIMAL+1;
  PCHARS: array[0..55] of PAnsiChar = (
    'TEXT COLLATE ISO8601', // should be before plain 'TEXT'
    'TEXT','CHAR','NCHAR','VARCHAR','NVARCHAR','CLOB','NCLOB','DBCLOB',
    'BIT','INT','BIGINT', 'DOUBLE','NUMBER','FLOAT','REAL','DECFLOAT',
    'CURR','DECIMAL','NUMERIC', 'BLOB SUB_TYPE 1',  'BLOB',
    'DATE','SMALLDATE','TIME',
    'TINYINT','BOOL','SMALLINT','MEDIUMINT','SERIAL','YEAR',
    'TINYTEXT','MEDIUMTEXT','NTEXT','XML','ENUM','SET','UNIQUEIDENTIFIER',
    'MONEY','SMALLMONEY','NUM',
    'VARRAW','RAW','LONG RAW','LONG VARRAW','TINYBLOB','MEDIUMBLOB',
    'BYTEA','VARBIN','IMAGE','LONGBLOB','BINARY','VARBINARY',
    'GRAPHIC','VARGRAPHIC', 'NULL');
  TYPES: array[-1..high(PCHARS)] of TSQLDBFieldType = (
    ftUnknown, ftDate,
    ftUTF8,ftUTF8,ftUTF8,ftUTF8,ftUTF8,ftUTF8,ftUTF8,ftUTF8,
    ftInt64,ftInt64,ftInt64, ftDouble,ftDouble,ftDouble,ftDouble,ftDouble,
    ftCurrency,ftCurrency,ftCurrency, ftUTF8, ftBlob,
    ftDate,ftDate,ftDate,
    ftInt64,ftInt64,ftInt64,ftInt64,ftInt64,ftInt64,
    ftUTF8,ftUTF8,ftUTF8,ftUTF8,ftUTF8,ftUTF8,ftUTF8,
    ftCurrency,ftCurrency,ftCurrency,
    ftBlob,ftBlob,ftBlob,ftBlob,ftBlob,ftBlob,ftBlob,ftBlob,
    ftBlob,ftBlob,ftBlob,ftBlob,ftBlob,ftBlob,
    ftNull);
var ndx: integer;
begin
  //assert(StrComp(PCHARS[DECIMAL],'DECIMAL')=0);
  ndx := IdemPCharArray(pointer(aNativeType),PCHARS);
  if (aScale=0) and (ndx in [DECIMAL,NUMERIC]) then
    result := ftInt64 else
    result := TYPES[ndx];
end;
function ColumnTypeNativeToDBOracle: TSQLDBFieldType;
begin
  if PosEx('CHAR',aNativeType)>0 then
    result := ftUTF8 else
  if IdemPropNameU(aNativeType,'NUMBER') then
    case aScale of
         0: result := ftInt64;
      1..4: result := ftCurrency;
    else    result := ftDouble;
    end else
  if (PosEx('RAW',aNativeType)>0) or
     IdemPropNameU(aNativeType,'BLOB') or
     IdemPropNameU(aNativeType,'BFILE') then
    result := ftBlob else
  if IdemPChar(pointer(aNativeType),'BINARY_') or
     IdemPropNameU(aNativeType,'FLOAT') then
    result := ftDouble else
  if IdemPropNameU(aNativeType,'DATE') or
     IdemPChar(pointer(aNativeType),'TIMESTAMP') then
    result := ftDate else
    // all other types will be converted to text
    result := ftUTF8;
end;
function ColumnTypeNativeToDBFirebird: TSQLDBFieldType;
var i,err: integer;
begin
  i := GetInteger(pointer(aNativeType),err);
  if err<>0 then
    result := ColumnTypeNativeDefault else
    case i of // see blr_* definitions
    10,11,27: result := ftDouble;
    12,13,35,120: result := ftDate;
    7,8,9,16,23,70,80,160: result := ftInt64;
    161..169: case abs(aScale) of
            0: result := ftInt64;
         1..4: result := ftCurrency;
         else  result := ftDouble;
         end;
    2610: result := ftBlob;
    else  result := ftUTF8;
    end;
end;
begin
  case DBMS of
  dOracle:   result := ColumnTypeNativeToDBOracle;
  dFireBird: result := ColumnTypeNativeToDBFirebird;
  else       result := ColumnTypeNativeDefault;
  end;
end;

function TSQLDBConnectionProperties.GetForeignKey(const aTableName,
  aColumnName: RawUTF8): RawUTF8;
begin
  if not fForeignKeys.Initialized then begin
    fForeignKeys.Init(false);
    GetForeignKeys;
  end;
  result := fForeignKeys.Value(aTableName+'.'+aColumnName);
end;

function TSQLDBConnectionProperties.GetForeignKeysData: RawByteString;
begin
  if not fForeignKeys.Initialized then begin
    fForeignKeys.Init(false);
    GetForeignKeys;
  end;
  result := fForeignKeys.BlobData;
end;

procedure TSQLDBConnectionProperties.SetForeignKeysData(const Value: RawByteString);
begin
  if not fForeignKeys.Initialized then
    fForeignKeys.Init(false);
  fForeignKeys.BlobData := Value;
end;

function TSQLDBConnectionProperties.SQLIso8601ToDate(const Iso8601: RawUTF8): RawUTF8;
  function TrimTInIso: RawUTF8;
  begin
    result := Iso8601;
    if (length(result)>10) and (result[11]='T') then
      result[11] := ' '; // 'T' -> ' '
  end;
begin
  case DBMS of
  dSQLite: result := TrimTInIso;
  dOracle: result := 'to_date('''+TrimTInIso+''',''YYYY-MM-DD HH24:MI:SS'')';
  dNexusDB: result := 'DATE '+Iso8601;
  dDB2: result := 'TIMESTAMP '''+TrimTInIso+'''';
  dPostgreSQL: result := ''''+TrimTInIso+'''';
  else  result := ''''+Iso8601+'''';
  end;
end;

function TSQLDBConnectionProperties.SQLDateToIso8601Quoted(DateTime: TDateTime): RawUTF8;
begin
  result := DateTimeToIso8601(DateTime,true,DateTimeFirstChar,false,'''');
end;

function TSQLDBConnectionProperties.SQLCreate(const aTableName: RawUTF8;
  const aFields: TSQLDBColumnCreateDynArray; aAddID: boolean): RawUTF8;
var i: integer;
    F: RawUTF8;
    FieldID: TSQLDBColumnCreate;
    AddPrimaryKey: RawUTF8;
begin // use 'ID' instead of 'RowID' here since some DB (e.g. Oracle) use it
  result := '';
  if high(aFields)<0 then
    exit; // nothing to create
  if aAddID then begin
    FieldID.DBType := ftInt64;
    FieldID.Name := 'ID';
    FieldID.Unique := true;
    FieldID.NonNullable := true;
    FieldID.PrimaryKey := true;
    result := SQLFieldCreate(FieldID,AddPrimaryKey)+',';
  end;
  for i := 0 to high(aFields) do begin
    F := SQLFieldCreate(aFields[i],AddPrimaryKey);
    if i<>high(aFields) then
      F := F+',';
    result := result+F;
  end;
  if AddPrimaryKey<>'' then
    result := result+', PRIMARY KEY('+AddPrimaryKey+')';
  result := 'CREATE TABLE '+aTableName+' ('+result+')';
  case DBMS of
  dDB2: result := result+' CCSID Unicode';
  end;
end;

function TSQLDBConnectionProperties.SQLFieldCreate(const aField: TSQLDBColumnCreate;
  var aAddPrimaryKey: RawUTF8): RawUTF8;
begin
  if (aField.DBType=ftUTF8) and (cardinal(aField.Width-1)<fSQLCreateFieldMax) then
    FormatUTF8(fSQLCreateField[ftNull],[aField.Width],result) else
    result := fSQLCreateField[aField.DBType];
  if aField.NonNullable or aField.Unique or aField.PrimaryKey then
    result := result+' NOT NULL';
  if aField.Unique and not aField.PrimaryKey then
    result := result+' UNIQUE'; // see http://www.w3schools.com/sql/sql_unique.asp
  if aField.PrimaryKey then
    case DBMS of
    dSQLite, dMSSQL, dOracle, dJet, dPostgreSQL, dFirebird, dNexusDB, dInformix:
      result := result+' PRIMARY KEY';
    dDB2, dMySQL:
      aAddPrimaryKey := aField.Name;
    end;
  result := aField.Name+result;
end;

function TSQLDBConnectionProperties.SQLAddColumn(const aTableName: RawUTF8;
  const aField: TSQLDBColumnCreate): RawUTF8;
var AddPrimaryKey: RawUTF8;
begin
  FormatUTF8('ALTER TABLE % ADD %',[aTableName,SQLFieldCreate(aField,AddPrimaryKey)],result);
end;

function TSQLDBConnectionProperties.SQLAddIndex(const aTableName: RawUTF8;
  const aFieldNames: array of RawUTF8; aUnique, aDescending: boolean;
  const aIndexName: RawUTF8): RawUTF8;
const CREATNDXIFNE: array[boolean] of RawUTF8 = ('','IF NOT EXISTS ');
var IndexName,FieldsCSV, ColsDesc, Owner,Table: RawUTF8;
begin
  result := '';
  if (self=nil) or (aTableName='') or (high(aFieldNames)<0) then
    exit;
  if aUnique then
    result := 'UNIQUE ';
  if aIndexName='' then begin
    SQLSplitTableName(aTableName,Owner,Table);
    if (Owner<>'') and
       not (fDBMS in [dMSSQL,dPostgreSQL,dMySQL,dFirebird,dDB2,dInformix]) then
      // some DB engines do not expect any schema in the index name
      IndexName := Owner+'.';
    FieldsCSV := RawUTF8ArrayToCSV(aFieldNames,'');
    if length(FieldsCSV)+length(Table)>27 then
      // sounds like if some DB limit the identifier length to 32 chars
      IndexName := IndexName+'INDEX'+
        crc32cUTF8ToHex(Table)+crc32cUTF8ToHex(FieldsCSV) else
      IndexName := IndexName+'NDX'+Table+FieldsCSV;
  end else
    IndexName := aIndexName;
  if aDescending then
    case DB_SQLDESENDINGINDEXPOS[DBMS] of
    posGlobalBefore:
      result := result+'DESC ';
    posWithColumn:
      ColsDesc := RawUTF8ArrayToCSV(aFieldNames,' DESC,')+' DESC';
    end;
  if ColsDesc='' then
    ColsDesc := RawUTF8ArrayToCSV(aFieldNames,',');
  result := FormatUTF8('CREATE %INDEX %% ON %(%)',
    [result,CREATNDXIFNE[DBMS in DB_HANDLECREATEINDEXIFNOTEXISTS],
     IndexName,aTableName,ColsDesc]);
end;

function TSQLDBConnectionProperties.SQLTableName(const aTableName: RawUTF8): RawUTF8;
var BeginQuoteChar, EndQuoteChar: RawUTF8;
    UseQuote: boolean;
begin
  BeginQuoteChar := '"';
  EndQuoteChar := '"';
  UseQuote := PosExChar(' ',aTableName)>0;
  case fDBMS of
    dPostgresql:
      if PosExChar('.',aTablename)=0 then
        UseQuote := true; // quote if not schema.identifier format
    dMySQL: begin
      BeginQuoteChar := '`';  // backtick/grave accent
      EndQuoteChar := '`';
    end;
    dJet: begin  // note: dMSSQL may SET IDENTIFIER ON to use doublequotes
      BeginQuotechar := '[';
      EndQuoteChar := ']';
    end;
    dSQLite: begin
      if PosExChar('.',aTableName)>0 then
        UseQuote := true;
      BeginQuoteChar := '`';  // backtick/grave accent
      EndQuoteChar := '`';
    end;
  end;
  if UseQuote and (PosEx(BeginQuoteChar,aTableName)=0) then
    result := BeginQuoteChar+aTableName+EndQuoteChar else
    result := aTableName;
end;

procedure TSQLDBConnectionProperties.GetIndexesAndSetFieldsColumnIndexed(
  const aTableName: RawUTF8; var Fields: TSQLDBColumnDefineDynArray);
var i,j: integer;
    ColName: RawUTF8;
    Indexes: TSQLDBIndexDefineDynArray;
begin
  if Fields=nil then
    exit;
  GetIndexes(aTableName,Indexes);
  for i := 0 to high(Indexes) do begin
    ColName := Trim(GetCSVItem(pointer(Indexes[i].KeyColumns),0));
    if ColName<>'' then
    for j := 0 to high(Fields) do
      if IdemPropNameU(Fields[j].ColumnName,ColName) then begin
        Fields[j].ColumnIndexed := true;
        break;
      end;
  end;
end;

function TSQLDBConnectionProperties.ExceptionIsAboutConnection(
  aClass: ExceptClass; const aMessage: RawUTF8): boolean;

  function PosErrorNumber(const aMessage: RawUTF8; const aSepChar: AnsiChar): PUTF8Char;
  begin // search aSepChar followed by a number
    result := pointer(aMessage);
    repeat
      result := SynCommons.PosChar(result,aSepChar);
      if result=nil then
        exit;
      inc(result);
    until result^ in ['0'..'9'];
  end;

begin // see more complete list in feature request [f024266c0839]
  case fDBMS of
  dOracle:
    result := IdemPCharArray(PosErrorNumber(aMessage,'-'),
      ['00028','01012','01017','01033','01089','02396','03113','03114','03135',
       '12152','12154','12157','12514','12520','12537','12545',
       '12560','12571'])>=0;
  dInformix: // error codes based on {IBM INFORMIX ODBC DRIVER} tested with wrong data connection
    result := IdemPCharArray(PosErrorNumber(aMessage,'-'),
      ['329','761','902','908','930','931','951','11017',
       '23101','23104','25567','25582','27002'])>=0;
  dMSSQL: // error codes based on {SQL Server Native Client 11.0} tested with wrong data connection
    // using general error codes because MS SQL SERVER has multiple error codes in the error message
    result := IdemPCharArray(PosErrorNumber(aMessage,'['),
      ['08001','08S01','08007','28000','42000'])>=0;
  dMySQL:
    result := (PosEx('Lost connection to MySQL server',aMessage)>0) or
              (PosEx('MySQL server has gone away',aMessage)>0);
  else
    result := PosI(' CONNE',aMessage)>0;
  end;
end;

procedure TSQLDBConnectionProperties.MultipleValuesInsert(
  Props: TSQLDBConnectionProperties; const TableName: RawUTF8;
  const FieldNames: TRawUTF8DynArray; const FieldTypes: TSQLDBFieldTypeArray;
  RowCount: integer; const FieldValues: TRawUTF8DynArrayDynArray);
var SQL: RawUTF8;
    SQLCached: boolean;
    prevrowcount: integer;
    maxf: integer;
procedure ComputeSQL(rowcount,offset: integer);
var f,r,p,len: integer;
    tmp: TTextWriterStackBuffer;
begin
  if (fDBMS<>dFireBird) and (rowcount=prevrowcount) then
    exit;
  prevrowcount := rowcount;
  with TTextWriter.CreateOwnedStream(tmp) do
  try
    case Props.fDBMS of
    dFirebird: begin
      AddShort('execute block('#10);
      p := 0;
      for r := offset to offset+rowcount-1 do begin
        for f := 0 to maxf do begin
          Add('i');
          inc(p);
          AddU(p);
          if FieldValues[f,r]='null' then
            AddShort(' CHAR(1)') else
            case FieldTypes[f] of
            ftNull: AddShort(' CHAR(1)');
            ftUTF8: begin
              len := length(FieldValues[f,r])-2; // unquoted UTF-8 text length
              if len<1 then
                len := 1;
              AddShort(' VARCHAR(');  // inlined Add(fmt...) for Delphi 5
              AddU(len);
              AddShort(') CHARACTER SET UTF8');
            end;
            else AddString(DB_FIELDS[dFirebird,FieldTypes[f]]);
            end;
          AddShort('=?,');
        end;
        CancelLastComma;
        Add(#10,',');
      end;
      CancelLastComma;
      AddShort(') as begin'#10);
      p := 0;
      for r := 1 to rowcount do begin
        AddShort('INSERT INTO ');
        AddString(TableName);
        Add(' ','(');
        for f := 0 to maxf do begin
          AddString(FieldNames[f]);
          Add(',');
        end;
        CancelLastComma;
        AddShort(') VALUES (');
        for f := 0 to maxf do begin
          inc(p);
          Add(':','i');
          AddU(p);
          Add(',');
        end;
        CancelLastComma;
        AddShort(');'#10);
      end;
      AddShort('end');
      if TextLength>32700 then
        raise ESQLDBException.CreateUTF8(
          '%.MultipleValuesInsert: Firebird Execute Block length=%',[self,TextLength]);
      SQLCached := false; // ftUTF8 values will have varying field length
    end;
    dOracle: begin // INSERT ALL INTO ... VALUES ... SELECT 1 FROM DUAL
      AddShort('insert all'#10); // see http://stackoverflow.com/a/93724
      for r := 1 to rowcount do begin
        AddShort('into ');
        AddString(TableName);
        Add(' ','(');
        for f := 0 to maxf do begin
          AddString(FieldNames[f]);
          Add(',');
        end;
        CancelLastComma;
        AddShort(') VALUES (');
        for f := 0 to maxf do
          Add('?',',');
        CancelLastComma;
        AddShort(')'#10);
      end;
      AddShort('select 1 from dual');
      SQLCached := true;
    end;
    else begin //  e.g. NexusDB/SQlite3/MySQL/PostgreSQL/MSSQL2008/DB2/INFORMIX
      AddShort('INSERT INTO '); // INSERT .. VALUES (..),(..),(..),..
      AddString(TableName);
      Add(' ','(');
      for f := 0 to maxf do begin
        AddString(FieldNames[f]);
        Add(',');
      end;
      CancelLastComma;
      AddShort(') VALUES');
      for r := 1 to rowcount do begin
        Add(' ','(');
        for f := 0 to maxf do
          Add('?',',');
        CancelLastComma;
        Add(')',',');
      end;
      CancelLastComma;
      SQLCached := true;
    end;
    end;
    SetText(SQL);
  finally
    Free;
  end;
end;
var batchRowCount,paramCountLimit: integer;
    currentRow,f,p,i,sqllen: integer;
    Stmt: TSQLDBStatement;
    Query: ISQLDBStatement;
begin
  maxf := length(FieldNames);     // e.g. 2 fields
  if (Props=nil) or (FieldNames=nil) or (TableName='') or (length(FieldValues)<>maxf) then
    raise ESQLDBException.CreateUTF8('Invalid %.MultipleValuesInsert(%) call',
      [self,TableName]);
  batchRowCount := 0;
  paramCountLimit := 0;
  case Props.fDBMS of
  // values below were done empirically, assuring < 667 (maximum :AA..:ZZ)
  // see http://stackoverflow.com/a/6582902 for theoritical high limits
  dSQlite:     paramCountLimit := 200;  // theoritical=999
  dMySQL:      paramCountLimit := 500;  // theoritical=60000
  dPostgreSQL: paramCountLimit := 500;  // theoritical=34000
  dOracle:     paramCountLimit := 500;  // empirical value (from ODBC)
  dMSSQL:      paramCountLimit := 500;  // theoritical=2100
  dDB2:        paramCountLimit := 500;  // empirical value (from ODBC)
  dNexusDB:    paramCountLimit := 100;  // empirical limit (above is slower)
  dFirebird: begin // compute from max SQL statement size of 32KB
    sqllen := maxf*48; // worse case (with BLOB param)
    for f := 0 to maxf-1 do
      inc(sqllen,Length(FieldNames[f]));
    batchRowCount := 32000 div sqllen;
    if batchRowCount>RowCount then
      batchRowCount := RowCount;
  end;
  end;
  if paramCountLimit<>0 then
    if RowCount*maxf>paramCountLimit then
      batchRowCount := paramCountLimit div maxf else
      batchRowCount := RowCount;
  if batchRowCount=0 then
    raise ESQLDBException.CreateUTF8('%.MultipleValuesInsert(%) with # params = %>%',
      [self,TableName,RowCount*maxf,paramCountLimit]);
  dec(maxf);
  prevrowcount := 0;
  SQLCached := false;
  currentRow := 0;
  repeat
    if RowCount-currentRow>batchRowCount then
      ComputeSQL(batchRowCount,currentRow) // max number of params -> try cache
    else begin
      ComputeSQL(RowCount-currentRow,currentRow);
      SQLCached := false; // truncate number of parameters should not be unique
    end;
    if SQLCached then
      Query := Props.NewThreadSafeStatementPrepared(SQL,false) else begin
      Stmt := Props.NewThreadSafeStatement;
      try
        Stmt.Prepare(SQL,false);
        Query := Stmt; // Stmt will be released by Query := nil below
      except
        on Exception do
          Stmt.Free; // avoid memory leak in case of invalid SQL statement
      end; // exception leaves Query=nil to raise exception
    end;
    if Query=nil then
      raise ESQLDBException.CreateUTF8('%.MultipleValuesInsert: Query=nil for [%]',[self,SQL]);
    try
      p := 1;
      for i := 1 to prevrowcount do begin
        for f := 0 to maxf do begin
          Query.Bind(p,FieldTypes[f],FieldValues[f,currentRow],false);
          inc(p);
        end;
        inc(currentRow);
      end;
      Query.ExecutePrepared;
    finally
      Query := nil; // will release the uncached local Stmt, if applying
    end;
  until currentRow=RowCount;
end;

procedure TSQLDBConnectionProperties.MultipleValuesInsertFirebird(
  Props: TSQLDBConnectionProperties; const TableName: RawUTF8;
  const FieldNames: TRawUTF8DynArray; const FieldTypes: TSQLDBFieldTypeArray;
  RowCount: integer; const FieldValues: TRawUTF8DynArrayDynArray);
var W: TTextWriter;
    maxf,sqllenwitoutvalues,sqllen,r,f,i: PtrInt;
    v: RawUTF8;
begin
  maxf := length(FieldNames);     // e.g. 2 fields
  if (Props=nil) or (FieldNames=nil) or (TableName='') or (length(FieldValues)<>maxf) or
     (Props.fDBMS<>dFirebird) then
    raise ESQLDBException.CreateUTF8('Invalid %.MultipleValuesInsertFirebird(%,%)',
      [self,Props,TableName]);
  sqllenwitoutvalues := 3*maxf+24;
  dec(maxf);
  for f := 0 to maxf do
    case FieldTypes[f] of
    ftBlob: begin // not possible to inline BLOBs -> fallback to regular
      MultipleValuesInsert(Props,TableName,FieldNames,FieldTypes,RowCount,FieldValues);
      exit;
    end;
    ftDate: inc(sqllenwitoutvalues,Length(FieldNames[f])+20); // 'timestamp '
    else
      inc(sqllenwitoutvalues,Length(FieldNames[f]));
    end;
  W := TTextWriter.CreateOwnedStream(49152);
  try
    r := 0;
    repeat
      W.AddShort('execute block as begin'#10);
      sqllen := sqllenwitoutvalues;
      repeat
        for f := 0 to maxf do
          inc(sqllen,length(FieldValues[f,r]));
        if sqllen+PtrInt(W.TextLength)>30000 then
          break;
        W.AddShort('INSERT INTO ');
        W.AddString(TableName);
        W.Add(' ','(');
        for f := 0 to maxf do begin
          W.AddString(FieldNames[f]);
          W.Add(',');
        end;
        W.CancelLastComma;
        W.AddShort(') VALUES (');
        for f := 0 to maxf do begin
          v := FieldValues[f,r]; // includes single quotes (#39)
          if (v='') or (v='null') then
            W.AddShort('null') else
          if FieldTypes[f]=ftDate then
            if v=#39#39 then
              W.AddShort('null') else begin
              W.AddShort('timestamp ');
              if length(v)>12 then begin // not 'CCYY-MM-DD' -> fix needed?
                if v[12]='T' then // handle 'CCYY-MM-DDTHH:MM:SS' common case
                  v[12] := ' ' else begin
                  i := PosExChar('T',v);
                  if i>0 then
                    v[i] := ' ';
                end; // see https://firebirdsql.org/en/firebird-date-literals
              end;
              W.AddString(v)
            end else
            W.AddString(v);
          W.Add(',');
        end;
        W.CancelLastComma;
        W.AddShort(');'#10);
        inc(r);
      until r=RowCount;
      W.AddShort('end');
      with Props.NewThreadSafeStatement do
      try
        Execute(W.Text,false);
      finally
        Free;
      end;
      if r=RowCount then
        break;
      W.CancelAll;
    until false;
  finally
    W.Free;
  end;
end;

function TSQLDBConnectionProperties.FieldsFromList(const aFields: TSQLDBColumnDefineDynArray;
  aExcludeTypes: TSQLDBFieldTypes): RawUTF8;
var i,n: integer;
begin
  result := '';
  if byte(aExcludeTypes)<>0 then begin
    n := length(aFields);
    for i := 0 to n-1 do
    with aFields[i] do
    if not (ColumnType in aExcludeTypes) then begin
      dec(n);
      if result='' then
        result := ColumnName else
        result := result+','+ColumnName;
    end;
    if n=0 then
      result := '*';
  end else
    result := '*';
end;

function TSQLDBConnectionProperties.SQLSelectAll(const aTableName: RawUTF8;
  const aFields: TSQLDBColumnDefineDynArray; aExcludeTypes: TSQLDBFieldTypes): RawUTF8;
begin
  if (self=nil) or (aTableName='') then
    result := '' else
    result := 'select '+FieldsFromList(aFields,aExcludeTypes)+' from '+
      SQLTableName(aTableName);
end;

class function TSQLDBConnectionProperties.EngineName: RawUTF8;
var L: integer;
begin
  if self=nil then
    result := '' else begin
    result := RawUTF8(ClassName);
    if IdemPChar(pointer(result),'TSQLDB') then
      Delete(result,1,6) else
    if result[1]='T' then
      Delete(result,1,1);
    L := length(result);
    if (L>20) and IdemPropName('ConnectionProperties',@result[L-19],20) then
      SetLength(result,L-20);
    if (L>5) and IdemPropName('OleDB',pointer(result),5) then
      Delete(result,1,5);
  end;
end;

function TSQLDBConnectionProperties.GetDBMS: TSQLDBDefinition;
begin
  if fDBMS=dUnknown then
    result := dDefault else
    result := fDBMS;
end;

function TSQLDBConnectionProperties.GetDBMSName: RawUTF8;
var PS: PShortString;
begin
  PS := ToText(DBMS);
  FastSetString(result,@PS^[2],ord(PS^[0])-1);
end;

function TSQLDBConnectionProperties.GetDatabaseNameSafe: RawUTF8;
begin
  result := StringReplaceAll(fDatabaseName,PassWord,'***');
end;

function TSQLDBConnectionProperties.SQLLimitClause(AStmt: TSynTableStatement): TSQLDBDefinitionLimitClause;
begin
  result := DB_SQLLIMITCLAUSE[DBMS];
end;

var
  GlobalDefinitions: array of TSQLDBConnectionPropertiesClass;

class procedure TSQLDBConnectionProperties.RegisterClassNameForDefinition;
begin
  ObjArrayAddOnce(GlobalDefinitions,TObject(self)); // TClass stored as TObject
end;

procedure TSQLDBConnectionProperties.DefinitionTo(Definition: TSynConnectionDefinition);
begin
  if Definition=nil then
    exit;
  Definition.Kind := ClassName;
  Definition.ServerName := ServerName;
  Definition.DatabaseName := DatabaseName;
  Definition.User := UserID;
  Definition.PassWordPlain := PassWord;
end;

function TSQLDBConnectionProperties.DefinitionToJSON(Key: cardinal): RawUTF8;
var Definition: TSynConnectionDefinition;
begin
  Definition := TSynConnectionDefinition.Create;
  try
    Definition.Key := Key;
    DefinitionTo(Definition);
    result := Definition.SaveToJSON;
  finally
    Definition.Free;
  end;
end;

procedure TSQLDBConnectionProperties.DefinitionToFile(const aJSONFile: TFileName;
  Key: cardinal);
begin
  FileFromString(JSONReformat(DefinitionToJSON(Key)),aJSONFile);
end;

class function TSQLDBConnectionProperties.ClassFrom(
  aDefinition: TSynConnectionDefinition): TSQLDBConnectionPropertiesClass;
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;

class function TSQLDBConnectionProperties.CreateFrom(
  aDefinition: TSynConnectionDefinition): TSQLDBConnectionProperties;
var C: TSQLDBConnectionPropertiesClass;
begin
  C := ClassFrom(aDefinition);
  if C=nil then
    raise ESQLDBException.CreateUTF8('%.CreateFrom: unknown % class - please '+
      'add a reference to its implementation unit',[self,aDefinition.Kind]);
  result := C.Create(aDefinition.ServerName,aDefinition.DatabaseName,
    aDefinition.User,aDefinition.PassWordPlain);
end;

class function TSQLDBConnectionProperties.CreateFromJSON(
  const aJSONDefinition: RawUTF8; aKey: cardinal): TSQLDBConnectionProperties;
var Definition: TSynConnectionDefinition;
begin
  Definition := TSynConnectionDefinition.CreateFromJSON(aJSONDefinition,aKey);
  try
    result := CreateFrom(Definition);
  finally
    Definition.Free;
  end;
end;

class function TSQLDBConnectionProperties.CreateFromFile(const aJSONFile: TFileName;
  aKey: cardinal): TSQLDBConnectionProperties;
begin
  result := CreateFromJSON(AnyTextFileToRawUTF8(aJSONFile,true),aKey);
end;


{ TSQLDBConnectionPropertiesThreadSafe }

procedure TSQLDBConnectionPropertiesThreadSafe.ClearConnectionPool;
var i: PtrInt;
begin
  fConnectionPool.Safe.Lock;
  try
    if fMainConnection<>nil then
      fMainConnection.fLastAccessTicks := -1; // force IsOutdated to return true
    for i := 0 to fConnectionPool.Count-1 do
      TSQLDBConnectionThreadSafe(fConnectionPool.List[i]).fLastAccessTicks := -1;
    fLatestConnectionRetrievedInPool := -1;
  finally
    fConnectionPool.Safe.UnLock;
  end;
end;

constructor TSQLDBConnectionPropertiesThreadSafe.Create(const aServerName,
  aDatabaseName, aUserID, aPassWord: RawUTF8);
begin
  fConnectionPool := TSynObjectListLocked.Create;
  fLatestConnectionRetrievedInPool := -1;
  inherited Create(aServerName,aDatabaseName,aUserID,aPassWord);
end;

function TSQLDBConnectionPropertiesThreadSafe.CurrentThreadConnectionIndex: Integer;
var id: TThreadID;
    tix: Int64;
    conn: TSQLDBConnectionThreadSafe;
begin // caller made EnterCriticalSection(fConnectionCS)
  if self<>nil then begin
    id := GetCurrentThreadId;
    tix := GetTickCount64;
    result := fLatestConnectionRetrievedInPool;
    if result>=0 then begin
      conn := fConnectionPool.List[result];
      if (conn.fThreadID=id) and not conn.IsOutdated(tix) then
        exit;
    end;
    result := 0;
    while result<fConnectionPool.Count do begin
      conn := fConnectionPool.List[result];
      if conn.IsOutdated(tix) then // to guarantee reconnection
        fConnectionPool.Delete(result) else begin
        if conn.fThreadID=id then begin
          fLatestConnectionRetrievedInPool := result;
          exit;
        end;
        inc(result);
      end;
    end;
  end;
  result := -1;
end;

destructor TSQLDBConnectionPropertiesThreadSafe.Destroy;
begin
  inherited Destroy; // do MainConnection.Free
  fConnectionPool.Free;
end;

procedure TSQLDBConnectionPropertiesThreadSafe.EndCurrentThread;
var i: integer;
begin
  fConnectionPool.Safe.Lock;
  try
    i := CurrentThreadConnectionIndex;
    if i>=0 then begin // do nothing if this thread has no active connection
      fConnectionPool.Delete(i); // release thread's TSQLDBConnection instance
      if i=fLatestConnectionRetrievedInPool then
        fLatestConnectionRetrievedInPool := -1;
    end;
  finally
    fConnectionPool.Safe.UnLock;
  end;
end;

function TSQLDBConnectionPropertiesThreadSafe.GetMainConnection: TSQLDBConnection;
begin
  result := ThreadSafeConnection;
end;

function TSQLDBConnectionPropertiesThreadSafe.ThreadSafeConnection: TSQLDBConnection;
var i: integer;
begin
  case fThreadingMode of
  tmThreadPool: begin
    fConnectionPool.Safe.Lock;
    try
      i := CurrentThreadConnectionIndex;
      if i>=0 then begin
        result := fConnectionPool.List[i];
        exit;
      end;
      result := NewConnection; // no need to release the lock (fast method)
      (result as TSQLDBConnectionThreadSafe).fThreadID := GetCurrentThreadId;
      fLatestConnectionRetrievedInPool := fConnectionPool.Add(result)
    finally
      fConnectionPool.Safe.UnLock;
     end;
  end;
  tmMainConnection:
    result := inherited GetMainConnection;
  else
    result := nil;
  end;
end;

{
  tmBackgroundThread should handle TSQLRestStorageExternal methods:
  Create: ServerTimestamp+GetFields
  BeginTransaction
  Commit
  Rollback
  InternalBatchStop: PrepareSQL+BindArray+ExecutePrepared
  EngineUpdateBlob:  PrepareSQL+Bind/BindNull+ExecutePrepared
  ExecuteDirect:     PrepareSQL+Bind+ExecutePrepared
  ExecuteInlined:    ExecuteInlined

  Handling only TSQLDBStatementWithParams will allow all binding to be
  set in the calling thread, but the actual process to take place in the
  background thread
}

{ TSQLDBStatement }

procedure TSQLDBStatement.Bind(Param: Integer; const Data: TSQLVar;
  IO: TSQLDBParamInOutType);
begin
  with Data do
  case VType of
    ftNull:     BindNull(Param,IO);
    ftInt64:    Bind(Param,VInt64,IO);
    ftDate:     BindDateTime(Param,VDateTime,IO);
    ftDouble:   Bind(Param,VDouble,IO);
    ftCurrency: Bind(Param,VCurrency,IO);
    ftUTF8:     BindTextP(Param,VText,IO);
    ftBlob:     BindBlob(Param,VBlob,VBlobLen,IO);
    else raise ESQLDBException.CreateUTF8('%.Bind(Param=%,VType=%)',
      [self,Param,ord(VType)]);
  end;
end;

procedure TSQLDBStatement.Bind(Param: Integer; ParamType: TSQLDBFieldType;
  const Value: RawUTF8; ValueAlreadyUnquoted: boolean; IO: TSQLDBParamInOutType=paramIn);
var tmp: RawUTF8;
begin
  if not ValueAlreadyUnquoted and (Value='null') then
    // bind null (ftUTF8 should be '"null"')
    BindNull(Param,IO) else
    case ParamType of
      ftNull:     BindNull(Param,IO);
      ftInt64:    Bind(Param,GetInt64(pointer(Value)),IO);
      ftDouble:   Bind(Param,GetExtended(pointer(Value)),IO);
      ftCurrency: BindCurrency(Param,StrToCurrency(pointer(Value)),IO);
      ftBlob:     BindBlob(Param,Value,IO); // already decoded
      ftDate: begin
        if ValueAlreadyUnquoted then
          tmp := Value else
          UnQuoteSQLStringVar(pointer(Value),tmp);
        BindDateTime(Param,Iso8601ToDateTime(tmp),IO);
      end;
      ftUTF8:
        if (fConnection<>nil) and fConnection.fProperties.StoreVoidStringAsNull and
           ((Value='') or // check if '' or '""' should be stored as null
           ((PInteger(Value)^ and $ffffff=$2727) and not ValueAlreadyUnquoted)) then
          BindNull(Param,IO,ftUTF8) else begin
          if ValueAlreadyUnquoted then
            tmp := Value else
            UnQuoteSQLStringVar(pointer(Value),tmp);
          BindTextU(Param,tmp,IO);
        end;
      else raise ESQLDBException.CreateUTF8('Invalid %.Bind(%,TSQLDBFieldType(%),%)',
        [self,Param,ord(ParamType),Value]);
    end;
end;

function VariantIsBlob(const V: variant): boolean;
begin
  with TVarData(V) do
    result := (VType=varNull) or
      ((VType=varString) and (VString<>nil) and
       (PCardinal(VString)^ and $ffffff=JSON_BASE64_MAGIC));
end;

procedure TSQLDBStatement.Bind(const Params: array of const;
  IO: TSQLDBParamInOutType);
var i,c: integer;
begin
  for i := 1 to high(Params)+1 do
  with Params[i-1] do // bind parameter index starts at 1
  case VType of
    vtString:     // expect WinAnsi String for ShortString
      BindTextU(i,WinAnsiToUtf8(@VString^[1],ord(VString^[0])),IO);
    vtAnsiString:
      if VAnsiString=nil then
        BindTextU(i,'',IO) else begin
        c := PInteger(VAnsiString)^ and $00ffffff;
        if c=JSON_BASE64_MAGIC then
          BindBlob(i,Base64ToBin(PAnsiChar(VAnsiString)+3,length(RawUTF8(VAnsiString))-3)) else
        if c=JSON_SQLDATE_MAGIC then
          BindDateTime(i,Iso8601ToDateTimePUTF8Char(PUTF8Char(VAnsiString)+3,length(RawUTF8(VAnsiString))-3)) else
          // expect UTF-8 content only for AnsiString, i.e. RawUTF8 variables
          {$ifdef HASCODEPAGE}
          BindTextU(i,AnyAnsiToUTF8(RawByteString(VAnsiString)),IO);
          {$else}
          BindTextU(i,RawUTF8(VAnsiString),IO);
          {$endif}
      end;
    vtPChar:      BindTextP(i,PUTF8Char(VPChar),IO);
    vtChar:       BindTextU(i,RawUTF8(VChar),IO);
    vtWideChar:   BindTextU(i,RawUnicodeToUtf8(@VWideChar,1),IO);
    vtPWideChar:  BindTextU(i,RawUnicodeToUtf8(VPWideChar,StrLenW(VPWideChar)),IO);
    vtWideString: BindTextW(i,WideString(VWideString),IO);
    {$ifdef HASVARUSTRING}
    {$ifdef UNICODE}
    vtUnicodeString: BindTextS(i,string(VUnicodeString),IO);
    {$else}
    vtUnicodeString: BindTextU(i,UnicodeStringToUtf8(UnicodeString(VUnicodeString)),IO);
    {$endif}
    {$endif}
    vtBoolean:    Bind(i,integer(VBoolean),IO);
    vtInteger:    Bind(i,VInteger,IO);
    vtInt64:      Bind(i,VInt64^,IO);
    {$ifdef FPC}
    vtQWord:      Bind(i,VQWord^,IO);
    {$endif}
    vtCurrency:   BindCurrency(i,VCurrency^,IO);
    vtExtended:   Bind(i,VExtended^,IO);
    vtPointer:
      if VPointer=nil then
        BindNull(i,IO) else
        raise ESQLDBException.CreateUTF8('Unexpected %.Bind() pointer',[self]);
    vtVariant:
      BindVariant(i,VVariant^,VariantIsBlob(VVariant^),IO);
    else
      raise ESQLDBException.CreateUTF8('%.BindArrayOfConst(Param=%,Type=%)',
        [self,i,VType]);
  end;
end;

procedure TSQLDBStatement.BindVariant(Param: Integer; const Data: Variant;
  DataIsBlob: boolean; IO: TSQLDBParamInOutType);
{$ifndef DELPHI5OROLDER}
var I64: Int64Rec;
{$endif}
begin
  with TVarData(Data) do
  case VType of
    varNull:
      BindNull(Param,IO);
    varBoolean:
      if VBoolean then
        Bind(Param,1,IO) else
        Bind(Param,0,IO);
    varByte:
      Bind(Param,VInteger,IO);
    varSmallint:
      Bind(Param,VSmallInt,IO);
    {$ifndef DELPHI5OROLDER}
    varShortInt:
      Bind(Param,VShortInt,IO);
    varWord:
      Bind(Param,VWord,IO);
    varLongWord: begin
      I64.Lo := VLongWord;
      I64.Hi := 0;
      Bind(Param,Int64(I64),IO);
    end;
    {$endif}
    varInteger:
      Bind(Param,VInteger,IO);
    varInt64, varWord64:
      Bind(Param,VInt64,IO);
    varSingle:
      Bind(Param,VSingle,IO);
    varDouble:
      Bind(Param,VDouble,IO);
    varDate:
      BindDateTime(Param,VDate,IO);
    varCurrency:
      BindCurrency(Param,VCurrency,IO);
    varOleStr: // handle special case if was bound explicitely as WideString
      BindTextW(Param,WideString(VAny),IO);
    {$ifdef HASVARUSTRING}
    varUString:
      if DataIsBlob then
        raise ESQLDBException.CreateUTF8(
          '%.BindVariant: BLOB should not be UnicodeString',[self]) else
        BindTextU(Param,UnicodeStringToUtf8(UnicodeString(VAny)),IO);
    {$endif}
    varString:
      if DataIsBlob then
        if (VAny<>nil) and (PInteger(VAny)^ and $00ffffff=JSON_BASE64_MAGIC) then
          // recognized as Base64 encoded text
          BindBlob(Param,Base64ToBin(PAnsiChar(VAny)+3,length(RawByteString(VAny))-3)) else
          // no conversion if was set via TQuery.AsBlob property e.g.
          BindBlob(Param,RawByteString(VAny),IO) else
        // direct bind of AnsiString as UTF-8 value
        {$ifdef HASCODEPAGE}
        BindTextU(Param,AnyAnsiToUTF8(RawByteString(VAny)),IO);
        {$else} // on older Delphi, we assume AnsiString = RawUTF8
        BindTextU(Param,RawUTF8(VAny),IO);
        {$endif}
    else
    if VType=varByRef or varVariant then
      BindVariant(Param,PVariant(VPointer)^,DataIsBlob,IO) else
    if VType=varByRef or varOleStr then
      BindTextW(Param,PWideString(VAny)^,IO) else
    {$ifdef LVCL}
      raise ESQLDBException.CreateUTF8(
        '%.BindVariant: Unhandled variant type %',[self,VType]);
    {$else}
      // also use TEXT for any non native VType parameter
      {$ifdef NOVARIANTS}
      BindTextU(Param,StringToUTF8(string(Data)),IO);
      {$else}
      BindTextU(Param,VariantToUTF8(Data),IO);
      {$endif}
    {$endif}
  end;
end;

procedure TSQLDBStatement.BindArray(Param: Integer; ParamType: TSQLDBFieldType;
  const Values: TRawUTF8DynArray; ValuesCount: integer);
begin
  if (Param<=0) or (ParamType in [ftUnknown,ftNull]) or (ValuesCount<=0) or
     (length(Values)<ValuesCount) or (fConnection=nil) or
     (fConnection.fProperties.BatchSendingAbilities*[cCreate,cUpdate,cDelete]=[]) then
    raise ESQLDBException.CreateUTF8('Invalid call to %.BindArray(Param=%,Type=%)',
      [self,Param,ToText(ParamType)^]);
end;

procedure TSQLDBStatement.BindArray(Param: Integer; const Values: array of Int64);
begin
  BindArray(Param,ftInt64,nil,0); // will raise an exception (Values=nil)
end;

procedure TSQLDBStatement.BindArray(Param: Integer; const Values: array of RawUTF8);
begin
  BindArray(Param,ftUTF8,nil,0); // will raise an exception (Values=nil)
end;

procedure TSQLDBStatement.BindArray(Param: Integer; const Values: array of double);
begin
  BindArray(Param,ftDouble,nil,0); // will raise an exception (Values=nil)
end;

procedure TSQLDBStatement.BindArrayCurrency(Param: Integer;
  const Values: array of currency);
begin
  BindArray(Param,ftCurrency,nil,0); // will raise an exception (Values=nil)
end;

procedure TSQLDBStatement.BindArrayDateTime(Param: Integer; const Values: array of TDateTime);
begin
  BindArray(Param,ftDate,nil,0); // will raise an exception (Values=nil)
end;

procedure TSQLDBStatement.CheckCol(Col: integer);
begin
  if (self=nil) or (cardinal(Col)>=cardinal(fColumnCount)) then
    raise ESQLDBException.CreateUTF8('Invalid call to %.Column*(Col=%)',[self,Col]);
end;

function TSQLDBStatement.GetForceBlobAsNull: boolean;
begin
  result := fForceBlobAsNull;
end;

procedure TSQLDBStatement.SetForceBlobAsNull(value: boolean);
begin
  fForceBlobAsNull := value;
end;

function TSQLDBStatement.GetForceDateWithMS: boolean;
begin
  result := fForceDateWithMS;
end;

procedure TSQLDBStatement.SetForceDateWithMS(value: boolean);
begin
  fForceDateWithMS := value;
end;

constructor TSQLDBStatement.Create(aConnection: TSQLDBConnection);
begin
  inherited Create;
  fConnection := aConnection;
  fStripSemicolon := true;
  fCacheIndex := -1;
  if aConnection<>nil then
    fDBMS := aConnection.fProperties.DBMS;
end;

function TSQLDBStatement.ColumnCount: integer;
begin
  if self=nil then
    result := 0 else
    result := fColumnCount;
end;

function TSQLDBStatement.ColumnBlobBytes(Col: integer): TBytes;
begin
  RawByteStringToBytes(ColumnBlob(Col),result);
end;

procedure TSQLDBStatement.ColumnBlobToStream(Col: integer; Stream: TStream);
var tmp: RawByteString;
begin
  tmp := ColumnBlob(Col); // default implementation
  Stream.WriteBuffer(pointer(tmp)^,Length(tmp));
end;

procedure TSQLDBStatement.ColumnBlobFromStream(Col: integer; Stream: TStream);
begin
  raise ESQLDBException.CreateUTF8('%.ColumnBlobFromStream not implemented',[self]);
end;

{$ifndef LVCL}
function TSQLDBStatement.ColumnVariant(Col: integer): Variant;
begin
  ColumnToVariant(Col,result);
end;

function TSQLDBStatement.ColumnToVariant(Col: integer; var Value: Variant): TSQLDBFieldType;
var tmp: RawByteString;
    V: TSQLVar;
begin
  ColumnToSQLVar(Col,V,tmp);
  result := V.VType;
  VarClear(Value);
  with TVarData(Value) do begin
    VType := MAP_FIELDTYPE2VARTYPE[V.VType];
    case result of
      ftNull: ; // do nothing
      ftInt64:    VInt64    := V.VInt64;
      ftDouble:   VDouble   := V.VDouble;
      ftDate:     VDate     := V.VDateTime;
      ftCurrency: VCurrency := V.VCurrency;
      ftBlob: begin
        VAny := nil;
        if V.VBlob<>nil then
          if V.VBlob=pointer(tmp) then
            RawByteString(VAny) := tmp else
            SetString(RawByteString(VAny),PAnsiChar(V.VBlob),V.VBlobLen);
      end;
      ftUTF8: begin
        VAny := nil; // avoid GPF below
        if V.VText<>nil then begin
          if V.VText=pointer(tmp) then
            V.VBlobLen := length(tmp) else
            V.VBlobLen := StrLen(V.VText);
          {$ifndef UNICODE}
          if (fConnection<>nil) and not fConnection.Properties.VariantStringAsWideString then begin
            VType := varString;
            if (CurrentAnsiConvert.CodePage=CP_UTF8) and (V.VText=pointer(tmp)) then
              RawByteString(VAny) := tmp else
              CurrentAnsiConvert.UTF8BufferToAnsi(V.VText,V.VBlobLen,RawByteString(VAny));
          end else
          {$endif UNICODE}
            UTF8ToSynUnicode(V.VText,V.VBlobLen,SynUnicode(VAny));
        end else
          VType := varString; // avoid obscure "Invalid variant type" in FPC
      end;
      else raise ESQLDBException.CreateUTF8(
        '%.ColumnToVariant: Invalid ColumnType(%)=%',[self,Col,ord(result)]);
    end;
  end;
end;
{$endif LVCL}

function TSQLDBStatement.ColumnTimestamp(Col: integer): TTimeLog;
begin
  case ColumnType(Col) of // will call GetCol() to check Col
    ftNull:  result := 0;
    ftInt64: result := ColumnInt(Col);
    ftDate:  PTimeLogBits(@result)^.From(ColumnDateTime(Col));
    else     PTimeLogBits(@result)^.From(Trim(ColumnUTF8(Col)));
  end;
end;

function TSQLDBStatement.ColumnTimestamp(const ColName: RawUTF8): TTimeLog;
begin
  result := ColumnTimestamp(ColumnIndex(ColName));
end;

procedure TSQLDBStatement.ColumnsToJSON(WR: TJSONWriter);
var col: integer;
    blob: RawByteString;
begin
  if WR.Expand then
    WR.Add('{');
  for col := 0 to fColumnCount-1 do begin
    if WR.Expand then
      WR.AddFieldName(ColumnName(col)); // add '"ColumnName":'
    if ColumnNull(col) then
      WR.AddShort('null') else
    case ColumnType(col) of
      ftNull:     WR.AddShort('null');
      ftInt64:    WR.Add(ColumnInt(col));
      ftDouble:   WR.AddDouble(ColumnDouble(col));
      ftCurrency: WR.AddCurr64(ColumnCurrency(col));
      ftDate: begin
        WR.Add('"');
        WR.AddDateTime(ColumnDateTime(col),fForceDateWithMS);
        WR.Add('"');
      end;
      ftUTF8: begin
        WR.Add('"');
        WR.AddJSONEscape(pointer(ColumnUTF8(col)));
        WR.Add('"');
      end;
      ftBlob:
        if fForceBlobAsNull then
          WR.AddShort('null') else begin
          blob := ColumnBlob(col);
          WR.WrBase64(pointer(blob),length(blob),{withMagic=}true);
        end;
      else raise ESQLDBException.CreateUTF8(
        '%.ColumnsToJSON: invalid ColumnType(%)=%',[self,col,ord(ColumnType(col))]);
    end;
    WR.Add(',');
  end;
  WR.CancelLastComma; // cancel last ','
  if WR.Expand then
    WR.Add('}');
end;

procedure TSQLDBStatement.ColumnToSQLVar(Col: Integer; var Value: TSQLVar;
  var Temp: RawByteString);
begin
  Value.Options := [];
  if ColumnNull(Col) then // will call GetCol() to check Col
    Value.VType := ftNull else
    Value.VType := ColumnType(Col);
  case Value.VType of
    ftInt64:    Value.VInt64  := ColumnInt(Col);
    ftDouble:   Value.VDouble := ColumnDouble(Col);
    ftDate:     Value.VDateTime := ColumnDateTime(Col);
    ftCurrency: Value.VCurrency := ColumnCurrency(Col);
    ftUTF8: begin
      Temp := ColumnUTF8(Col);
      Value.VText := pointer(Temp);
    end;
    ftBlob:
    if fForceBlobAsNull then begin
      Value.VBlob := nil;
      Value.VBlobLen := 0;
      Value.VType := ftNull;
    end else begin
      Temp := ColumnBlob(Col);
      Value.VBlob := pointer(Temp);
      Value.VBlobLen := length(Temp);
    end;
  end;
end;

function TSQLDBStatement.ColumnToTypedValue(Col: integer;
  DestType: TSQLDBFieldType; var Dest): TSQLDBFieldType;
{$ifdef LVCL}
begin
  raise ESQLDBException.CreateUTF8('%.ColumnToTypedValue non implemented in LVCL',[self]);
end;
{$else}
var Temp: Variant; // rely on a temporary variant value for the conversion
begin
  result := ColumnToVariant(Col,Temp);
  case DestType of
  ftInt64:    {$ifdef DELPHI5OROLDER}integer{$else}Int64{$endif}(Dest) := Temp;
  ftDouble:   Double(Dest) := Temp;
  ftCurrency: Currency(Dest) := Temp;
  ftDate:     TDateTime(Dest) := Temp;
  {$ifdef NOVARIANTS}
  ftUTF8:     RawUTF8(Dest) := StringToUTF8(string(Temp));
  {$else}
  ftUTF8:     RawUTF8(Dest) := VariantToUTF8(Temp);
  {$endif}
  ftBlob:     VariantToRawByteString(Temp,RawByteString(Dest));
  else raise ESQLDBException.CreateUTF8('%.ColumnToTypedValue: Invalid Type [%]',
    [self,ToText(result)^]);
  end;
end;
{$endif}

{$ifndef LVCL}
function TSQLDBStatement.ParamToVariant(Param: Integer; var Value: Variant;
  CheckIsOutParameter: boolean=true): TSQLDBFieldType;
begin
  dec(Param); // start at #1
  if (self=nil) or (cardinal(Param)>=cardinal(fParamCount)) then
    raise ESQLDBException.CreateUTF8('%.ParamToVariant(%)',[self,Param]);
  // overridden method should fill Value with proper data
  result := ftUnknown;
end;
{$endif}

procedure TSQLDBStatement.Execute(const aSQL: RawUTF8;
  ExpectResults: Boolean);
begin
  Connection.InternalProcess(speActive);
  try
    Prepare(aSQL,ExpectResults);
    SetForceBlobAsNull(true);
    ExecutePrepared;
  finally
    Connection.InternalProcess(speNonActive);
  end;
end;

function TSQLDBStatement.FetchAllToJSON(JSON: TStream; Expanded: boolean): PtrInt;
var W: TJSONWriter;
    col: integer;
    maxmem: PtrUInt;
    tmp: TTextWriterStackBuffer;
begin
  result := 0;
  W := TJSONWriter.Create(JSON,Expanded,false,nil,0,@tmp);
  try
    Connection.InternalProcess(speActive);
    maxmem := Connection.Properties.StatementMaxMemory;
    // get col names and types
    SetLength(W.ColNames,ColumnCount);
    for col := 0 to ColumnCount-1 do
      W.ColNames[col] := ColumnName(col);
    W.AddColumns; // write or init field names for appropriate JSON Expand
    if Expanded then
      W.Add('[');
    // write rows data
    {$ifdef SYNDB_SILENCE}
    fSQLLogTimer.Resume; // log fetch duration
    {$endif}
    while Step do begin
      ColumnsToJSON(W);
      W.Add(',');
      inc(result);
      if (maxmem>0) and (W.WrittenBytes>maxmem) then // TextLength is slower
        raise ESQLDBException.CreateUTF8('%.FetchAllToJSON: overflow %',
          [self, KB(maxmem)]);
    end;
    {$ifdef SYNDB_SILENCE}
    fSQLLogTimer.Pause;
    {$endif}
    ReleaseRows;
    if (result=0) and W.Expand then begin
      // we want the field names at least, even with no data (RowCount=0)
      W.Expand := false; //  {"FieldCount":2,"Values":["col1","col2"]}
      W.CancelAll;
      for col := 0 to ColumnCount-1 do
        W.ColNames[col] := ColumnName(col); // previous W.AddColumns did add ""
      W.AddColumns;
    end;
    W.EndJSONObject(0,result);
  finally
    W.Free;
    Connection.InternalProcess(speNonActive);
  end;
end;

function TSQLDBStatement.FetchAllToCSVValues(Dest: TStream; Tab: boolean;
  CommaSep: AnsiChar; AddBOM: boolean): PtrInt;
const NULL: array[boolean] of string[7] = ('"null"','null');
      BLOB: array[boolean] of string[7] = ('"blob"','blob');
var F, FMax: integer;
    maxmem: PtrUInt;
    W: TTextWriter;
    tmp: RawByteString;
    V: TSQLVar;
begin
  result := 0;
  if (Dest=nil) or (self=nil) or (ColumnCount=0) then
    exit;
  fForceBlobAsNull := true;
  if Tab then
    CommaSep := #9;
  FMax := ColumnCount-1;
  maxmem := Connection.Properties.StatementMaxMemory;
  W := TTextWriter.Create(Dest,65536);
  try
    if AddBOM then
      W.AddShort(#$ef#$bb#$bf); // add UTF-8 Byte Order Mark
    // add CSV header
    for F := 0 to FMax do begin
      if not Tab then
        W.Add('"');
      W.AddString(ColumnName(F));
      if Tab then
        W.Add(#9) else
        W.Add('"',CommaSep);
    end;
    W.CancelLastChar;
    W.AddCR;
    // add CSV rows
    {$ifdef SYNDB_SILENCE}
    fSQLLogTimer.Resume;
    {$endif}
    while Step do begin
      for F := 0 to FMax do begin
        ColumnToSQLVar(F,V,tmp);
        case V.VType of
          ftNull:     W.AddShort(NULL[tab]);
          ftInt64:    W.Add(V.VInt64);
          ftDouble:   W.AddDouble(V.VDouble);
          ftCurrency: W.AddCurr64(V.VCurrency);
          ftDate: begin
            if not Tab then
              W.Add('"');
            W.AddDateTime(V.VDateTime,svoDateWithMS in V.Options);
            if not Tab then
              W.Add('"');
          end;
          ftUTF8: begin
            if not Tab then begin
              W.Add('"');
              W.AddJSONEscape(V.VText);
              W.Add('"');
            end else
              W.AddNoJSONEscape(V.VText);
          end;
          ftBlob: W.AddShort(BLOB[Tab]);  // ForceBlobAsNull should be true
          else raise ESQLDBException.CreateUTF8(
            '%.FetchAllToCSVValues: Invalid ColumnType(%) %',
            [self,F,ToText(ColumnType(F))^]);
        end;
        if F=FMax then
          W.AddCR else
          W.Add(CommaSep);
      end;
      inc(result);
      if (maxmem>0) and (W.WrittenBytes>maxmem) then // TextLength is slower
        raise ESQLDBException.CreateUTF8('%.FetchAllToCSVValues: overflow %',
          [self, KB(maxmem)]);
    end;
    {$ifdef SYNDB_SILENCE}
    fSQLLogTimer.Pause;
    {$endif}
    ReleaseRows;
    W.FlushFinal;
  finally
    W.Free;
  end;
end;

function TSQLDBStatement.FetchAllAsJSON(Expanded: boolean;
  ReturnedRowCount: PPtrInt): RawUTF8;
var Stream: TRawByteStringStream;
    RowCount: PtrInt;
begin
  Stream := TRawByteStringStream.Create;
  try
    RowCount := FetchAllToJSON(Stream,Expanded);
    if ReturnedRowCount<>nil then
      ReturnedRowCount^ := RowCount;
    result := Stream.DataString;
  finally
    Stream.Free;
  end;
end;

procedure TSQLDBStatement.ColumnsToBinary(W: TFileBufferWriter;
  Null: pointer; const ColTypes: TSQLDBFieldTypeDynArray);
var F: integer;
    VDouble: double;
    VCurrency: currency absolute VDouble;
    VDateTime: TDateTime absolute VDouble;
    ft: TSQLDBFieldType;
begin
  for F := 0 to length(ColTypes)-1 do
    if not GetBitPtr(Null, F) then begin
      ft := ColTypes[F];
      if ft<ftInt64 then begin // ftUnknown,ftNull
        ft := ColumnType(F); // per-row column type (SQLite3 only)
        W.Write1(ord(ft));
      end;
      case ft of
      ftInt64:
        W.WriteVarInt64(ColumnInt(F));
      ftDouble: begin
        VDouble := ColumnDouble(F);
        W.Write(@VDouble,sizeof(VDouble));
      end;
      ftCurrency: begin
        VCurrency := ColumnCurrency(F);
        W.Write(@VCurrency,sizeof(VCurrency));
      end;
      ftDate: begin
        VDateTime := ColumnDateTime(F);
        W.Write(@VDateTime,sizeof(VDateTime));
      end;
      ftUTF8:
        W.Write(ColumnUTF8(F));
      ftBlob:
        W.Write(ColumnBlob(F));
      else
      raise ESQLDBException.CreateUTF8('%.ColumnsToBinary: Invalid ColumnType(%)=%',
        [self,ColumnName(F),ord(ft)]);
    end;
  end;
end;

const
  FETCHALLTOBINARY_MAGIC = 1;

function TSQLDBStatement.FetchAllToBinary(Dest: TStream; MaxRowCount: cardinal;
  DataRowPosition: PCardinalDynArray): cardinal;
var F, FMax, FieldSize, NullRowSize: integer;
    StartPos, MaxMem: Int64;
    W: TFileBufferWriter;
    ft: TSQLDBFieldType;
    ColTypes: TSQLDBFieldTypeDynArray;
    Null: TByteDynArray;
begin
  result := 0;
  MaxMem := Connection.Properties.StatementMaxMemory;
  W := TFileBufferWriter.Create(Dest);
  try
    W.WriteVarUInt32(FETCHALLTOBINARY_MAGIC);
    FMax := ColumnCount;
    W.WriteVarUInt32(FMax);
    if FMax>0 then begin
      // write column description
      SetLength(ColTypes,FMax);
      dec(FMax);
      for F := 0 to FMax do begin
        W.Write(ColumnName(F));
        ft := ColumnType(F,@FieldSize);
        if (ft=ftUnknown) and (CurrentRow=0) and Step then
          ft := ColumnType(F,@FieldSize); // e.g. SQLite3 -> fetch and guess
        ColTypes[F] := ft;
        W.Write1(ord(ft));
        W.WriteVarUInt32(FieldSize);
      end;
      // initialize null handling
      SetLength(Null,(FMax shr 3)+1);
      NullRowSize := 0;
      // save all data rows
      StartPos := W.TotalWritten;
      if (CurrentRow=1) or Step then // Step may already be done (e.g. TQuery.Open)
      repeat
        // save row position in DataRowPosition[] (if any)
        if DataRowPosition<>nil then begin
          if Length(DataRowPosition^)<=integer(result) then
            SetLength(DataRowPosition^,NextGrow(result));
          DataRowPosition^[result] := W.TotalWritten-StartPos;
        end;
        // first write null columns flags
        if NullRowSize>0 then begin
          FillCharFast(Null[0],NullRowSize,0);
          NullRowSize := 0;
        end;
        for F := 0 to FMax do
          if ColumnNull(F) then begin
            SetBitPtr(pointer(Null),F);
            NullRowSize := (F shr 3)+1;
          end;
        if NullRowSize>0 then begin
          W.WriteVarUInt32(NullRowSize);
          W.Write(pointer(Null),NullRowSize);
        end else
          W.Write1(0); // = W.WriteVarUInt32(0)
        // then write data values
        ColumnsToBinary(W,pointer(Null),ColTypes);
        inc(result);
        if (MaxMem>0) and (W.TotalWritten>MaxMem) then // Stream.Position is slower
          raise ESQLDBException.CreateUTF8('%.FetchAllToBinary: overflow %',
            [self, KB(MaxMem)]);
        if (MaxRowCount>0) and (result>=MaxRowCount) then
          break;
      until not Step;
      ReleaseRows;
    end;
    W.Write(@result,SizeOf(result)); // fixed size at the end for row count
    W.Flush;
  finally
    W.Free;
  end;
end;

procedure TSQLDBStatement.Execute(const aSQL: RawUTF8;
  ExpectResults: Boolean; const Params: array of const);
begin
  Connection.InternalProcess(speActive);
  try
    Prepare(aSQL,ExpectResults);
    Bind(Params);
    ExecutePrepared;
  finally
    Connection.InternalProcess(speNonActive);
  end;
end;

procedure TSQLDBStatement.Execute(const SQLFormat: RawUTF8;
  ExpectResults: Boolean; const Args, Params: array of const);
begin
  Execute(FormatUTF8(SQLFormat,Args),ExpectResults,Params);
end;

function TSQLDBStatement.UpdateCount: integer;
begin
  result := 0;
end;

procedure TSQLDBStatement.ExecutePreparedAndFetchAllAsJSON(Expanded: boolean; out JSON: RawUTF8);
begin
  ExecutePrepared;
  JSON := FetchAllAsJSON(Expanded);
end;

function TSQLDBStatement.ColumnString(Col: integer): string;
begin
  Result := UTF8ToString(ColumnUTF8(Col));
end;

function TSQLDBStatement.ColumnString(const ColName: RawUTF8): string;
begin
  result := ColumnString(ColumnIndex(ColName));
end;

function TSQLDBStatement.ColumnBlob(const ColName: RawUTF8): RawByteString;
begin
  result := ColumnBlob(ColumnIndex(ColName));
end;

function TSQLDBStatement.ColumnBlobBytes(const ColName: RawUTF8): TBytes;
begin
  result := ColumnBlobBytes(ColumnIndex(ColName));
end;

procedure TSQLDBStatement.ColumnBlobToStream(const ColName: RawUTF8; Stream: TStream);
begin
  ColumnBlobToStream(ColumnIndex(ColName),Stream);
end;

procedure TSQLDBStatement.ColumnBlobFromStream(const ColName: RawUTF8; Stream: TStream);
begin
  ColumnBlobFromStream(ColumnIndex(ColName),Stream);
end;

function TSQLDBStatement.ColumnCurrency(const ColName: RawUTF8): currency;
begin
  result := ColumnCurrency(ColumnIndex(ColName));
end;

function TSQLDBStatement.ColumnDateTime(const ColName: RawUTF8): TDateTime;
begin
  result := ColumnDateTime(ColumnIndex(ColName));
end;

function TSQLDBStatement.ColumnDouble(const ColName: RawUTF8): double;
begin
  result := ColumnDouble(ColumnIndex(ColName));
end;

function TSQLDBStatement.ColumnInt(const ColName: RawUTF8): Int64;
begin
  result := ColumnInt(ColumnIndex(ColName));
end;

function TSQLDBStatement.ColumnUTF8(const ColName: RawUTF8): RawUTF8;
begin
  result := ColumnUTF8(ColumnIndex(ColName));
end;

{$ifndef LVCL}
function TSQLDBStatement.ColumnVariant(const ColName: RawUTF8): Variant;
begin
  ColumnToVariant(ColumnIndex(ColName),result);
end;

function TSQLDBStatement.GetColumnVariant(const ColName: RawUTF8): Variant;
begin
  ColumnToVariant(ColumnIndex(ColName),result);
end;
{$endif LVCL}

function TSQLDBStatement.ColumnCursor(const ColName: RawUTF8): ISQLDBRows;
begin
  result := ColumnCursor(ColumnIndex(ColName));
end;

function TSQLDBStatement.ColumnCursor(Col: integer): ISQLDBRows;
begin
  raise ESQLDBException.CreateUTF8('% does not support CURSOR columns',[self]);
end;

function TSQLDBStatement.Instance: TSQLDBStatement;
begin
  Result := Self;
end;

function TSQLDBStatement.SQLLogBegin(level: TSynLogInfo): TSynLog;
begin
  if level = sllDB then // prepare
    fSQLLogTimer.Start else
    fSQLLogTimer.Resume;
  {$ifdef SYNDB_SILENCE}
  result := nil;
  {$else}
  result := SynDBLog.Add;
  if result <> nil then
    if level in result.Family.Level then
    begin
      fSQLLogLevel := level;
      if level = sllSQL then
        ComputeSQLWithInlinedParams;
    end
    else
      result := nil;
  fSQLLogLog := result;
  {$endif}
end;

function TSQLDBStatement.SQLLogEnd(msg: PShortString): Int64;
{$ifndef SYNDB_SILENCE}
var tmp: TShort16;
{$endif}
begin
  fSQLLogTimer.Pause;
  {$ifdef SYNDB_SILENCE}
  result := fSQLLogTimer.LastTimeInMicroSec;
  {$else}
  result := 0;
  if fSQLLogLog=nil then
    exit;
  tmp[0] := #0;
  if fSQLLogLevel=sllSQL then begin
    if msg=nil then begin
      if not fExpectResults then
        FormatShort16(' wr=%',[UpdateCount],tmp);
      msg := @tmp;
    end;
    fSQLLogLog.Log(fSQLLogLevel, 'ExecutePrepared %% %',
      [fSQLLogTimer.Time, msg^, fSQLWithInlinedParams], self)
  end
  else begin
    if msg=nil then
      msg := @tmp;
    fSQLLogLog.Log(fSQLLogLevel, 'Prepare %% %', [fSQLLogTimer.Stop, msg^, fSQL], self);
  end;
  result := fSQLLogTimer.LastTimeInMicroSec;
  fSQLLogLog := nil;
  {$endif}
end;

function TSQLDBStatement.SQLLogEnd(const Fmt: RawUTF8; const Args: array of const): Int64;
var tmp: shortstring;
begin
  tmp[0] := #0;
  {$ifndef SYNDB_SILENCE}
  result := 0;
  if fSQLLogLog=nil then
    exit;
  if Fmt<>'' then
    FormatShort(Fmt,Args,tmp);
  {$endif}
  result := SQLLogEnd(@tmp);
end;

function TSQLDBStatement.GetSQLCurrent: RawUTF8;
begin
  if fSQLPrepared <> '' then
    Result := fSQLPrepared else
    Result := fSQL;
end;

function TSQLDBStatement.GetSQLWithInlinedParams: RawUTF8;
begin
  if fSQL='' then
    result := '' else begin
    if fSQLWithInlinedParams='' then
      ComputeSQLWithInlinedParams;
    result := fSQLWithInlinedParams;
  end;
end;

function GotoNextParam(P: PUTF8Char): PUTF8Char;
  {$ifdef HASINLINE} inline; {$endif}
var c: AnsiChar;
begin
  repeat
    c := P^;
    if (c=#0) or (c='?') then
      break;
    if (c='''') and (P[1]<>'''') then begin
      repeat // ignore ? inside ' quotes
        inc(P);
        c := P^;
      until (c=#0) or ((c='''') and (P[1]<>''''));
      if c=#0 then
        break;
    end;
    inc(P);
  until false;
  result := P;
end;

procedure TSQLDBStatement.ComputeSQLWithInlinedParams;
var P,B: PUTF8Char;
    num: integer;
    maxSize,maxAllowed: cardinal;
    W: TTextWriter;
    tmp: TTextWriterStackBuffer;
begin
  fSQLWithInlinedParams := fSQL;
  if fConnection=nil then
    maxSize := 0 else
    maxSize := fConnection.fProperties.fLoggedSQLMaxSize;
  if (integer(maxSize)<0) or (PosExChar('?',fSQL)=0) then
    // maxsize=-1 -> log statement without any parameter value (just ?)
    exit;
  P := pointer(fSQL);
  num := 1;
  W := nil;
  try
    repeat
      B := P;
      P := GotoNextParam(P);
      if W=nil then
        if P^=#0 then
          exit else
          W := TTextWriter.CreateOwnedStream(tmp);
      W.AddNoJSONEscape(B,P-B);
      if P^=#0 then
        break;
      inc(P); // jump P^='?'
      if maxSize>0 then
        maxAllowed := W.TextLength-maxSize else
        maxAllowed := maxInt;
      AddParamValueAsText(num,W,maxAllowed);
      inc(num);
    until (P^=#0) or ((maxSize>0) and (W.TextLength>=maxSize));
    W.SetText(fSQLWithInlinedParams);
  finally
    W.Free;
  end;
end;

procedure TSQLDBStatement.AddParamValueAsText(Param: integer; Dest: TTextWriter;
  MaxCharCount: integer);
  procedure AppendUnicode(W: PWideChar; WLen: integer);
  var tmp: TSynTempBuffer;
  begin
    if MaxCharCount<WLen then
      WLen := MaxCharCount;
    tmp.Init(WLen);
    try
      RawUnicodeToUtf8(tmp.buf,tmp.Len,W,WLen,[ccfNoTrailingZero]);
      Dest.AddQuotedStr(tmp.buf,'''',MaxCharCount);
    finally
      tmp.Done;
    end;
  end;
var v: variant;
    ft: TSQLDBFieldType;
begin
  ft := ParamToVariant(Param,v,false);
  with TVarData(v) do
    case cardinal(VType) of
      varString:
        if ft=ftBlob then
          Dest.AddU(length(RawByteString(VString))) else
          Dest.AddQuotedStr(VString,'''',MaxCharCount);
      varOleStr:
        AppendUnicode(VString, length(WideString(VString)));
      {$ifdef HASVARUSTRING}
      varUString:
        AppendUnicode(VString, length(UnicodeString(VString)));
      {$endif}
      else if (ft=ftDate) and (cardinal(VType) in [varDouble,varDate]) then
        Dest.AddDateTime(vdate) else
        Dest.AddVariant(v);
    end;
end;

{$ifndef DELPHI5OROLDER}
{$ifndef LVCL}
var
  SQLDBRowVariantType: TCustomVariantType = nil;

function TSQLDBStatement.RowData: Variant;
begin
  if SQLDBRowVariantType=nil then
    SQLDBRowVariantType := SynRegisterCustomVariantType(TSQLDBRowVariantType);
  VarClear(result);
  with TVarData(result) do begin
    VType := SQLDBRowVariantType.VarType;
    VPointer := self;
  end;
end;

procedure TSQLDBStatement.RowDocVariant(out aDocument: variant;
  aOptions: TDocVariantOptions);
var n,F: integer;
    names: TRawUTF8DynArray;
    values: TVariantDynArray;
begin
  n := ColumnCount;
  SetLength(names,n); // faster to assign internal arrays per reference
  SetLength(values,n);
  for F := 0 to n-1 do begin
    names[F] := ColumnName(F);
    ColumnToVariant(F,values[F]);
  end;
  TDocVariantData(aDocument).InitObjectFromVariants(names,values,aOptions);
end;

{$endif}
{$endif}

procedure TSQLDBStatement.Prepare(const aSQL: RawUTF8; ExpectResults: Boolean);
var L: integer;
begin
  Connection.InternalProcess(speActive);
  try
    L := length(aSQL);
    if StripSemicolon then
      if (L>5) and (aSQL[L]=';') and // avoid syntax error for some drivers
         not IdemPChar(@aSQL[L-4],' END') then
        fSQL := copy(aSQL,1,L-1) else
        fSQL := aSQL else
      fSQL := aSQL;
    fExpectResults := ExpectResults;
    if (fConnection<>nil) and not fConnection.IsConnected then
      fConnection.Connect;
  finally
    Connection.InternalProcess(speNonActive);
  end;
end;

procedure TSQLDBStatement.ExecutePrepared;
begin
  if fConnection<>nil then
    fConnection.fLastAccessTicks := GetTickCount64;
  // a do-nothing default method
end;

procedure TSQLDBStatement.Reset;
begin
  fSQLWithInlinedParams := '';
  fSQLLogTimer.Init; // reset timer (for cached statement for example)
end;

procedure TSQLDBStatement.ReleaseRows;
begin
  fSQLWithInlinedParams := '';
end;

function TSQLDBStatement.ColumnsToSQLInsert(const TableName: RawUTF8;
  var Fields: TSQLDBColumnCreateDynArray): RawUTF8;
var F,size: integer;
begin
  Result := '';
  if (self=nil) or (TableName='') then
    exit;
  SetLength(Fields,ColumnCount);
  if Fields=nil then
    exit;
  Result := 'insert into '+TableName+' (';
  for F := 0 to high(Fields) do begin
    Fields[F].Name := ColumnName(F);
    Fields[F].DBType := ColumnType(F,@size);
    Fields[F].Width := size;
    case Fields[F].DBType of
    ftNull:
      Fields[F].DBType := ftBlob; // if not identified, assume it is a BLOB
    ftUnknown:
      raise ESQLDBException.CreateUTF8(
        '%.ColumnsToSQLInsert: Invalid column %',[self,Fields[F].Name]);
    end;
    Result := Result+Fields[F].Name+',';
  end;
  Result[length(Result)] := ')';
  Result := Result+' values (';
  for F := 0 to high(Fields) do
    Result := Result+'?,'; // MUCH faster with a prepared statement
  Result[length(Result)] := ')';
end;

procedure TSQLDBStatement.BindFromRows(
  const Fields: TSQLDBFieldTypeDynArray; Rows: TSQLDBStatement);
var F: integer;
begin
  if (self<>nil) and (Fields<>nil) and (Rows<>nil) then
    for F := 0 to high(Fields) do
      if Rows.ColumnNull(F) then
        BindNull(F+1) else
      case Fields[F] of
        ftNull:     BindNull(F+1);
        ftInt64:    Bind(F+1,Rows.ColumnInt(F));
        ftDouble:   Bind(F+1,Rows.ColumnDouble(F));
        ftCurrency: BindCurrency(F+1,Rows.ColumnCurrency(F));
        ftDate:     BindDateTime(F+1,Rows.ColumnDateTime(F));
        ftUTF8:     BindTextU(F+1,Rows.ColumnUTF8(F));
        ftBlob:     BindBlob(F+1,Rows.ColumnBlob(F));
      end;
end;

procedure TSQLDBStatement.BindCursor(Param: integer);
begin
  raise ESQLDBException.CreateUTF8('% does not support CURSOR parameter',[self]);
end;

function TSQLDBStatement.BoundCursor(Param: Integer): ISQLDBRows;
begin
  raise ESQLDBException.CreateUTF8('% does not support CURSOR parameter',[self]);
end;


{$ifndef DELPHI5OROLDER}
{$ifndef LVCL}

{ TSQLDBRowVariantType }

function TSQLDBRowVariantType.IntGet(var Dest: TVarData;
  const Instance: TVarData; Name: PAnsiChar; NameLen: PtrInt): boolean;
var Rows: TSQLDBStatement;
    col: RawUTF8;
    ndx: integer;
begin
  Rows := TSQLDBStatement(Instance.VPointer);
  if Rows=nil then
    raise ESQLDBException.CreateUTF8('Invalid % call',[self]);
  FastSetString(col,Name,NameLen);
  ndx := Rows.ColumnIndex(col);
  result := ndx>=0;
  if ndx>=0 then
    Rows.ColumnToVariant(ndx,Variant(Dest));
end;

{$endif LVCL}
{$endif DELPHI5OROLDER}


{ TSQLDBStatementWithParams }

function TSQLDBStatementWithParams.CheckParam(Param: Integer;
  NewType: TSQLDBFieldType; IO: TSQLDBParamInOutType): PSQLDBParam;
begin
  if self=nil then
    raise ESQLDBException.Create('self=nil for TSQLDBStatement.Bind*()');
  if Param>fParamCount then
    fParam.Count := Param; // resize fParams[] dynamic array if necessary
  result := @fParams[Param-1];
  result^.VType := NewType;
  result^.VInOut := IO;
end;

function TSQLDBStatementWithParams.CheckParam(Param: Integer;
  NewType: TSQLDBFieldType; IO: TSQLDBParamInOutType; ArrayCount: integer): PSQLDBParam;
begin
  result := CheckParam(Param,NewType,IO);
  if (NewType in [ftUnknown,ftNull]) or (fConnection=nil) or
     (fConnection.fProperties.BatchSendingAbilities*[cCreate,cUpdate,cDelete]=[]) then
    raise ESQLDBException.CreateUTF8('Invalid call to %.BindArray(Param=%,Type=%)',
      [self,Param,ToText(NewType)^]);
  SetLength(result^.VArray,ArrayCount);
  result^.VInt64 := ArrayCount;
  fParamsArrayCount := ArrayCount;
end;

constructor TSQLDBStatementWithParams.Create(aConnection: TSQLDBConnection);
begin
  inherited Create(aConnection);
  fParam.Init(TypeInfo(TSQLDBParamDynArray),fParams,@fParamCount);
end;

procedure TSQLDBStatementWithParams.Bind(Param: Integer; Value: double;
  IO: TSQLDBParamInOutType);
begin
  CheckParam(Param,ftDouble,IO)^.VInt64 := PInt64(@Value)^;
end;

procedure TSQLDBStatementWithParams.Bind(Param: Integer; Value: Int64;
  IO: TSQLDBParamInOutType);
begin
  CheckParam(Param,ftInt64,IO)^.VInt64 := Value;
end;

procedure TSQLDBStatementWithParams.BindBlob(Param: Integer;
  const Data: RawByteString; IO: TSQLDBParamInOutType);
begin
  CheckParam(Param,ftBlob,IO)^.VData := Data;
end;

procedure TSQLDBStatementWithParams.BindBlob(Param: Integer; Data: pointer;
  Size: integer; IO: TSQLDBParamInOutType);
begin
  SetString(CheckParam(Param,ftBlob,IO)^.VData,PAnsiChar(Data),Size);
end;

procedure TSQLDBStatementWithParams.BindCurrency(Param: Integer;
  Value: currency; IO: TSQLDBParamInOutType);
begin
  CheckParam(Param,ftCurrency,IO)^.VInt64 := PInt64(@Value)^;
end;

procedure TSQLDBStatementWithParams.BindDateTime(Param: Integer;
  Value: TDateTime; IO: TSQLDBParamInOutType);
begin
  CheckParam(Param,ftDate,IO)^.VInt64 := PInt64(@Value)^;
end;

procedure TSQLDBStatementWithParams.BindNull(Param: Integer;
  IO: TSQLDBParamInOutType; BoundType: TSQLDBFieldType);
begin
  CheckParam(Param,ftNull,IO);
end;

procedure TSQLDBStatementWithParams.BindTextS(Param: Integer;
  const Value: string; IO: TSQLDBParamInOutType);
begin
  if (Value='') and (fConnection<>nil) and fConnection.fProperties.StoreVoidStringAsNull then
    CheckParam(Param,ftNull,IO) else
    CheckParam(Param,ftUTF8,IO)^.VData := StringToUTF8(Value);
end;

procedure TSQLDBStatementWithParams.BindTextU(Param: Integer;
  const Value: RawUTF8; IO: TSQLDBParamInOutType);
begin
  if (Value='') and (fConnection<>nil) and fConnection.fProperties.StoreVoidStringAsNull then
    CheckParam(Param,ftNull,IO) else
    CheckParam(Param,ftUTF8,IO)^.VData := Value;
end;

procedure TSQLDBStatementWithParams.BindTextP(Param: Integer;
  Value: PUTF8Char; IO: TSQLDBParamInOutType);
begin
  if (Value=nil) and (fConnection<>nil) and fConnection.fProperties.StoreVoidStringAsNull then
    CheckParam(Param,ftNull,IO) else
    FastSetString(RawUTF8(CheckParam(Param,ftUTF8,IO)^.VData),Value,StrLen(Value));
end;

procedure TSQLDBStatementWithParams.BindTextW(Param: Integer;
  const Value: WideString; IO: TSQLDBParamInOutType);
begin
  if (Value='') and (fConnection<>nil) and fConnection.fProperties.StoreVoidStringAsNull then
    CheckParam(Param,ftNull,IO) else
    CheckParam(Param,ftUTF8,IO)^.VData := RawUnicodeToUtf8(pointer(Value),length(Value));
end;

{$ifndef LVCL}
function TSQLDBStatementWithParams.ParamToVariant(Param: Integer;
  var Value: Variant; CheckIsOutParameter: boolean): TSQLDBFieldType;
begin
  inherited ParamToVariant(Param,Value); // raise exception if Param incorrect
  dec(Param); // start at #1
  if CheckIsOutParameter and (fParams[Param].VInOut=paramIn) then
    raise ESQLDBException.CreateUTF8('%.ParamToVariant expects an [In]Out parameter',[self]);
  // OleDB provider should have already modified the parameter in-place, i.e.
  // in our fParams[] buffer, especialy for TEXT parameters (OleStr/WideString)
  // -> we have nothing to do but return the current value! :)
  with fParams[Param] do begin
    result := VType;
    if VArray=nil then
      case VType of
        ftInt64:     Value := {$ifdef DELPHI5OROLDER}integer{$endif}(VInt64);
        ftDouble:    Value := unaligned(PDouble(@VInt64)^);
        ftCurrency:  Value := PCurrency(@VInt64)^;
        ftDate:      Value := PDateTime(@VInt64)^;
        ftUTF8:      RawUTF8ToVariant(RawUTF8(VData),Value);
        ftBlob:      RawByteStringToVariant(VData,Value);
        else         SetVariantNull(Value)
      end else SetVariantNull(Value);
  end;
end;
{$endif}

procedure TSQLDBStatementWithParams.AddParamValueAsText(Param: integer; Dest: TTextWriter;
  MaxCharCount: integer);
begin
  dec(Param);
  if cardinal(Param)>=cardinal(fParamCount) then
    Dest.AddShort('null') else
    with fParams[Param] do
    if VArray=nil then
      case VType of
        ftInt64:    Dest.Add({$ifdef DELPHI5OROLDER}integer{$endif}(VInt64));
        ftDouble:   Dest.AddDouble(unaligned(PDouble(@VInt64)^));
        ftCurrency: Dest.AddCurr64(VInt64);
        ftDate:     Dest.AddDateTime(PDateTime(@VInt64),' ','''');
        ftUTF8:     Dest.AddQuotedStr(pointer(VData),'''',MaxCharCount);
        ftBlob:     Dest.AddU(length(VData));
        else        Dest.AddShort('null');
      end
      else Dest.AddString(VArray[0]); // first item is enough in the logs
end;

procedure TSQLDBStatementWithParams.BindArray(Param: Integer;
  const Values: array of double);
var i: PtrInt;
begin
  with CheckParam(Param,ftDouble,paramIn,length(Values))^ do
    for i := 0 to high(Values) do
      VArray[i] := DoubleToStr(Values[i]);
end;

procedure TSQLDBStatementWithParams.BindArray(Param: Integer;
  const Values: array of Int64);
var i: PtrInt;
begin
  with CheckParam(Param,ftInt64,paramIn,length(Values))^ do
    for i := 0 to high(Values) do
      VArray[i] := Int64ToUtf8(Values[i]);
end;

procedure TSQLDBStatementWithParams.BindArray(Param: Integer;
  ParamType: TSQLDBFieldType; const Values: TRawUTF8DynArray; ValuesCount: integer);
var i: PtrInt;
    ChangeFirstChar: AnsiChar;
    p: PSQLDBParam;
begin
  inherited; // raise an exception in case of invalid parameter
  if fConnection=nil then
    ChangeFirstChar := 'T' else
    ChangeFirstChar := Connection.Properties.DateTimeFirstChar;
  p := CheckParam(Param,ParamType,paramIn);
  p^.VInt64 := ValuesCount;
  p^.VArray := Values; // immediate COW reference-counted assignment
  if (ParamType=ftDate) and (ChangeFirstChar<>'T') then
    for i := 0 to ValuesCount-1 do // fix e.g. for PostgreSQL
      if (p^.VArray[i]<>'') and (p^.VArray[i][1]='''') then
        // not only replace 'T'->ChangeFirstChar, but force expanded format
        DateTimeToIso8601(Iso8601ToDateTime(p^.VArray[i]),
          {expanded=}true, ChangeFirstChar, {ms=}fForceDateWithMS, '''');
  fParamsArrayCount := ValuesCount;
end;

procedure TSQLDBStatementWithParams.BindArray(Param: Integer;
  const Values: array of RawUTF8);
var i: PtrInt;
    StoreVoidStringAsNull: boolean;
begin
  StoreVoidStringAsNull := (fConnection<>nil) and
    fConnection.Properties.StoreVoidStringAsNull;
  with CheckParam(Param,ftUTF8,paramIn,length(Values))^ do
    for i := 0 to high(Values) do
      if StoreVoidStringAsNull and (Values[i]='') then
        VArray[i] := 'null' else
        QuotedStr(Values[i],'''',VArray[i]);
end;

procedure TSQLDBStatementWithParams.BindArrayCurrency(Param: Integer;
  const Values: array of currency);
var i: PtrInt;
begin
  with CheckParam(Param,ftCurrency,paramIn,length(Values))^ do
    for i := 0 to high(Values) do
      VArray[i] := Curr64ToStr(PInt64(@Values[i])^);
end;

procedure TSQLDBStatementWithParams.BindArrayDateTime(Param: Integer;
  const Values: array of TDateTime);
var i: PtrInt;
begin
  with CheckParam(Param,ftDate,paramIn,length(Values))^ do
    for i := 0 to high(Values) do
      VArray[i] := Connection.Properties.SQLDateToIso8601Quoted(Values[i]);
end;

procedure TSQLDBStatementWithParams.BindArrayRowPrepare(
  const aParamTypes: array of TSQLDBFieldType; aExpectedMinimalRowCount: integer);
var i: PtrInt;
begin
  fParam.Count := 0;
  for i := 0 to high(aParamTypes) do
    CheckParam(i+1,aParamTypes[i],paramIn,aExpectedMinimalRowCount);
  fParamsArrayCount := 0;
end;

procedure TSQLDBStatementWithParams.BindArrayRow(const aValues: array of const);
var i: PtrInt;
begin
  if length(aValues)<>fParamCount then
    raise ESQLDBException.CreateFmt('Invalid %.BindArrayRow call',[self]);
  for i := 0 to high(aValues) do
    with fParams[i] do begin
      if length(VArray)<=fParamsArrayCount then
        SetLength(VArray,NextGrow(fParamsArrayCount));
      VInt64 := fParamsArrayCount;
      if (VType=ftDate) and (aValues[i].VType=vtExtended) then
        VArray[fParamsArrayCount] := // direct binding of TDateTime value
          Connection.Properties.SQLDateToIso8601Quoted(aValues[i].VExtended^) else begin
        VarRecToUTF8(aValues[i],VArray[fParamsArrayCount]);
        case VType of
        ftUTF8:
          if (VArray[fParamsArrayCount]='') and (fConnection<>nil) and
             fConnection.Properties.StoreVoidStringAsNull then
          VArray[fParamsArrayCount] := 'null' else
          VArray[fParamsArrayCount] := QuotedStr(VArray[fParamsArrayCount]);
        ftDate:
          VArray[fParamsArrayCount] := QuotedStr(VArray[fParamsArrayCount]);
        end;
      end;
    end;
  inc(fParamsArrayCount);
end;

procedure TSQLDBStatementWithParams.BindFromRows(Rows: TSQLDBStatement);
var F: PtrInt;
    U: RawUTF8;
begin
  if Rows<>nil then
    if Rows.ColumnCount<>fParamCount then
      raise ESQLDBException.CreateUTF8('Invalid %.BindFromRows call',[self]) else
    for F := 0 to fParamCount-1 do
    with fParams[F] do begin
      if length(VArray)<=fParamsArrayCount then
        SetLength(VArray,NextGrow(fParamsArrayCount));
      if Rows.ColumnNull(F) then
        VArray[fParamsArrayCount] := 'null' else
      case Rows.ColumnType(F) of
        ftNull:
          VArray[fParamsArrayCount] := 'null';
        ftInt64:
          VArray[fParamsArrayCount] := Int64ToUtf8(Rows.ColumnInt(F));
        ftDouble:
          VArray[fParamsArrayCount] := DoubleToStr(Rows.ColumnDouble(F));
        ftCurrency:
          VArray[fParamsArrayCount] := CurrencyToStr(Rows.ColumnCurrency(F));
        ftDate:
          VArray[fParamsArrayCount] := ''''+DateTimeToSQL(Rows.ColumnDateTime(F))+'''';
        ftUTF8: begin
          U := Rows.ColumnUTF8(F);
          if (U='') and (fConnection<>nil) and fConnection.Properties.StoreVoidStringAsNull then
            VArray[fParamsArrayCount] := 'null' else
            VArray[fParamsArrayCount] := QuotedStr(U,'''');
        end;
        ftBlob:
          VArray[fParamsArrayCount] := Rows.ColumnBlob(F);
      end;
    end;
  inc(fParamsArrayCount);
end;

procedure TSQLDBStatementWithParams.Reset;
begin
  fParam.Clear;
  fParamsArrayCount := 0;
  inherited Reset;
end;

procedure TSQLDBStatementWithParams.ReleaseRows;
var i: PtrInt;
    p: PSQLDBParam;
begin
  p := pointer(fParams);
  if p<>nil then
    for i := 1 to fParamCount do begin
      if p^.VData<>'' then
        p^.VData := ''; // release bound value, but keep fParams[] reusable
      if p^.VArray<>nil then
        RawUTF8DynArrayClear(p^.VArray);
      inc(p);
    end;
  inherited ReleaseRows;
end;


{ TSQLDBStatementWithParamsAndColumns }

function TSQLDBStatementWithParamsAndColumns.ColumnIndex(const aColumnName: RawUTF8): integer;
begin
  result := fColumn.FindHashed(aColumnName);
end;

function TSQLDBStatementWithParamsAndColumns.ColumnName(Col: integer): RawUTF8;
begin
  CheckCol(Col);
  result := fColumns[Col].ColumnName;
end;

function TSQLDBStatementWithParamsAndColumns.ColumnType(Col: integer; FieldSize: PInteger=nil): TSQLDBFieldType;
begin
  with fColumns[Col] do begin
    result := ColumnType;
    if FieldSize<>nil then
      if ColumnValueInlined then
        FieldSize^ := ColumnValueDBSize else
        FieldSize^ := 0;
  end;
end;

constructor TSQLDBStatementWithParamsAndColumns.Create(aConnection: TSQLDBConnection);
begin
  inherited Create(aConnection);
  fColumn.InitSpecific(TypeInfo(TSQLDBColumnPropertyDynArray),
    fColumns,djRawUTF8,@fColumnCount,True);
end;


procedure LogTruncatedColumn(const Col: TSQLDBColumnProperty);
begin
  SynDBLog.Add.Log(sllDB,'Truncated column %',Col.ColumnName);
end;

function TrimLeftSchema(const TableName: RawUTF8): RawUTF8;
var i,j: integer;
begin
  j := 1;
  repeat
    i := PosEx('.',TableName,j);
    if i=0 then break;
    j := i+1;
  until false;
  if j=1 then
    result := TableName else
    result := copy(TableName,j,maxInt);
end;

function ReplaceParamsByNames(const aSQL: RawUTF8; var aNewSQL: RawUTF8;
  aStripSemicolon: boolean): integer;
var i,j,B,L: PtrInt;
    P: PAnsiChar;
    c: array[0..3] of AnsiChar;
    tmp: RawUTF8;
const SQL_KEYWORDS: array[0..19] of AnsiChar = 'ASATBYIFINISOFONORTO';
begin
  result := 0;
  L := Length(aSQL);
  if aStripSemicolon then
    while (L>0) and (aSQL[L] in [#1..' ',';']) do
      if (aSQL[L]=';') and (L>5) and IdemPChar(@aSQL[L-3],'END') then
        break else // allows 'END;' at the end of a statement
        dec(L);    // trim ' ' or ';' right (last ';' could be found incorrect)
  if PosExChar('?',aSQL)>0 then begin
    aNewSQL:= '';
    // change ? into :AA :BA ..
    c := ':AA';
    i := 0;
    P := pointer(aSQL);
    if P<>nil then
    repeat
      B := i;
      while (i<L) and (P[i]<>'?') do begin
        if P[i]='''' then begin
          repeat // ignore chars inside ' quotes
            inc(i);
          until (i=L) or ((P[i]='''')and(P[i+1]<>''''));
          if i=L then break;
        end;
        inc(i);
      end;
      FastSetString(tmp,P+B,i-B);
      aNewSQL := aNewSQL+tmp;
      if i=L then break;
      // store :AA :BA ..
      j := length(aNewSQL);
      SetLength(aNewSQL,j+3);
      PCardinal(PtrInt(aNewSQL)+j)^ := PCardinal(@c)^;
      repeat
        if c[1]='Z' then begin
          if c[2]='Z' then
            raise ESQLDBException.Create('Parameters :AA to :ZZ');
          c[1] := 'A';
          inc(c[2]);
        end else
          inc(c[1]);
      until WordScanIndex(@SQL_KEYWORDS,length(SQL_KEYWORDS)shr 1,PWord(@c[1])^)<0;
      inc(result);
      inc(i); // jump '?'
    until i=L;
  end else
    aNewSQL := copy(aSQL,1,L); // trim right ';' if any
end;

function ReplaceParamsByNumbers(const aSQL: RawUTF8; var aNewSQL: RawUTF8;
  IndexChar: AnsiChar; AllowSemicolon: boolean): integer;
var
  ndx, L: PtrInt;
  s, d: PUTF8Char;
  c: AnsiChar;
begin
  aNewSQL := aSQL;
  result := 0;
  ndx := 0;
  L := Length(aSQL);
  s := pointer(aSQL);
  if (s = nil) or (PosExChar('?', aSQL) = 0) then
    exit;
  // calculate ? parameters count, check for ;
  while s^ <> #0 do
  begin
    c := s^;
    if c = '?' then
    begin
      inc(ndx);
      if ndx > 9 then  // ? will be replaced by $n $nn $nnn
        if ndx > 99 then
          if ndx > 999 then
            exit
          else
            inc(L, 3)
        else
          inc(L, 2)
        else
          inc(L);
    end
    else if c = '''' then
    begin
      repeat
        inc(s);
        c := s^;
        if c = #0 then
          exit; // quote without proper ending -> reject
        if c = '''' then
          if s[1] = c then
            inc(s) // ignore double quotes between single quotes
          else
            break;
      until false;
    end else if (c = ';') and not AllowSemicolon then
      exit; // complex expression can not be prepared
    inc(s);
  end;
  if ndx = 0 then // no ? parameter
    exit;
  result := ndx;
  // parse SQL and replace ? into $n $nn $nnn
  FastSetString(aNewSQL, nil, L);
  s := pointer(aSQL);
  d := pointer(aNewSQL);
  ndx := 0;
  repeat
    c := s^;
    if c = '?' then
    begin
      d^ := IndexChar; // e.g. '$'
      inc(d);
      inc(ndx);
      d := Append999ToBuffer(d, ndx);
    end
    else if c = '''' then
    begin
      repeat // ignore double quotes between single quotes
        d^ := c;
        inc(d);
        inc(s);
        c := s^;
        if c = '''' then
          if s[1] = c then
          begin
            d^ := c;
            inc(d);
            inc(s) // ignore double quotes between single quotes
          end
          else
            break;
      until false;
      d^ := c; // store last '''
      inc(d);
    end
    else
    begin
      d^ := c;
      inc(d);
    end;
    inc(s);
  until s^ = #0;
  //assert(d - pointer(aNewSQL) = length(aNewSQL)); // until stabilized
end;

function BoundArrayToJSONArray(const Values: TRawUTF8DynArray): RawUTF8;
//  'one', 't"wo' -> '{"one","t\"wo"}'  and  1,2,3 -> '{1,2,3}'
var
  V: ^RawUTF8;
  s, d: PUTF8Char;
  L, vl, n: PtrInt;
  c: AnsiChar;
label
  _dq;
begin
  result := '';
  n := length(Values);
  if n = 0 then
    exit;
  L := 1; // trailing '{'
  inc(L, n); // ',' after each element - and ending '}'
  v := pointer(Values);
  repeat
    vl := length(v^);
    if vl <> 0 then
    begin
      inc(L, vl);
      s := pointer(v^);
      if s^ = '''' then
      begin // quoted ftUTF8
        dec(vl, 2);
        if vl > 0 then
          repeat
            inc(s);
            c := s^;
            if c = '''' then
            begin
              if s[1] = '''' then
                dec(L); // double ' into single '
            end
            else if (c = '"') or (c = '\') then
              inc(L); // escape \ before "
            dec(vl);
          until vl = 0;
      end;
    end;
    inc(v);
    dec(n);
  until n = 0;
  FastSetString(result, nil, L);
  d := pointer(result);
  d^ := '{';
  inc(d);
  v := pointer(Values);
  n := length(Values);
  repeat
    vl := length(v^);
    if vl <> 0 then
    begin
      s := pointer(v^);
      if s^ = '''' then // quoted ftUTF8
      begin
        d^ := '"';
        inc(d);
        dec(vl, 2);
        if vl > 0 then
          repeat
            inc(s);
            c := s^;
            if c = '''' then
            begin
              if s[1] = '''' then
                goto _dq; // double ' into single '
            end
            else if (c = '"') or (c = '\') then
            begin
              d^ := '\'; // escape \ before "
              inc(d);
            end;
            d^ := c;
            inc(d);
_dq:        dec(vl);
          until vl = 0;
        d^ := '"';
        inc(d);
      end
      else
        repeat // regular content
          d^ := s^;
          inc(d);
          inc(s);
          dec(vl);
        until vl = 0;
    end;
    d^ := ',';
    inc(d);
    inc(v);
    dec(n);
  until n = 0;
  d[-1] := '}'; // replace last ',' by '}'
  //assert(d - pointer(result) = length(result)); // until stabilized
end;


{ TSQLDBLib }

function TSQLDBLib.TryLoadLibrary(const aLibrary: array of TFileName;
  aRaiseExceptionOnFailure: ESynExceptionClass): boolean;
var i: integer;
    lib, libs {$ifdef MSWINDOWS} , nwd, cwd {$endif}: TFileName;
begin
  for i := 0 to high(aLibrary) do begin
    lib := aLibrary[i];
    if lib = '' then
      continue;
    {$ifdef MSWINDOWS}
    nwd := ExtractFilePath(lib);
    if nwd <> '' then begin
      cwd := GetCurrentDir;
      SetCurrentDir(nwd); // search for dll dependencies in the same folder
    end;
    fHandle := SafeLoadLibrary(lib);
    if nwd <> '' then
      SetCurrentDir(cwd);
    {$else}
    fHandle := SafeLoadLibrary(lib);
    {$endif MSWINDOWS}
    if fHandle <> 0 then begin
      fLibraryPath := lib;
      result := true;
      exit;
    end;
    if libs = '' then
      libs := lib else
      libs := libs + ', ' + lib;
  end;
  result := false;
  if aRaiseExceptionOnFailure <> nil then
    raise aRaiseExceptionOnFailure.CreateUTF8(
      '%.LoadLibray failed - searched in %', [self, libs]);
end;

destructor TSQLDBLib.Destroy;
begin
  if Handle<>0 then
    FreeLibrary(Handle);
  inherited;
end;


{$ifdef WITH_PROXY}

{ TSQLDBProxyConnectionPropertiesAbstract }

procedure TSQLDBProxyConnectionPropertiesAbstract.SetInternalProperties;
var InputCredential: RawUTF8;
    token: Int64;
begin
  if fStartTransactionTimeOut=0 then
    fStartTransactionTimeOut := 2000;
  if fProtocol=nil then
    // override this method and set fProtocol before calling inherited
    fProtocol := TSQLDBProxyConnectionProtocol.Create(nil);
  Process(cGetToken,self,token);
  SetLength(InputCredential,4);
  PCardinal(InputCredential)^ := fProtocol.Authenticate.ComputeHash(token,UserID,PassWord);
  InputCredential := UserID+#1+InputCredential;
  fCurrentSession := Process(cGetDBMS,InputCredential,fDBMS);
end;

destructor TSQLDBProxyConnectionPropertiesAbstract.Destroy;
begin
  try
    inherited Destroy;
    Process(cQuit,self,self);
  finally
    fProtocol.Free;
  end;
end;

procedure TSQLDBProxyConnectionPropertiesAbstract.GetForeignKeys;
begin
  Process(cGetForeignKeys,self,fForeignKeys);
end;

function TSQLDBProxyConnectionPropertiesAbstract.NewConnection: TSQLDBConnection;
begin
  result := TSQLDBProxyConnection.Create(self);
end;

procedure TSQLDBProxyConnectionPropertiesAbstract.GetFields(const aTableName: RawUTF8;
  out Fields: TSQLDBColumnDefineDynArray);
begin
  Process(cGetFields,aTableName,Fields);
end;

procedure TSQLDBProxyConnectionPropertiesAbstract.GetIndexes(const aTableName: RawUTF8;
  out Indexes: TSQLDBIndexDefineDynArray);
begin
  Process(cGetIndexes,aTableName,Indexes);
end;

procedure TSQLDBProxyConnectionPropertiesAbstract.GetTableNames(out Tables: TRawUTF8DynArray);
begin
  Process(cGetTableNames,self,Tables);
end;

function TSQLDBProxyConnectionPropertiesAbstract.IsCachable(P: PUTF8Char): boolean;
begin
  result := False;
end;


{ TSQLDBRemoteConnectionPropertiesAbstract }

function TSQLDBRemoteConnectionPropertiesAbstract.Process(
  Command: TSQLDBProxyConnectionCommand; const Input; var Output): integer;
var msgInput,msgOutput,msgRaw: RawByteString;
    header: TRemoteMessageHeader;
    outheader: PRemoteMessageHeader;
    InputText: RawUTF8 absolute Input;
    InputExecute: TSQLDBProxyConnectionCommandExecute absolute Input;
    O: PAnsiChar;
    OutputSQLDBDefinition: TSQLDBDefinition absolute Output;
    OutputInt64: Int64 absolute Output;
    OutputBoolean: boolean absolute Output;
    OutputSQLDBColumnDefineDynArray: TSQLDBColumnDefineDynArray absolute Output;
    OutputSQLDBIndexDefineDynArray: TSQLDBIndexDefineDynArray absolute Output;
    OutputRawUTF8DynArray: TRawUTF8DynArray absolute Output;
    OutputRawUTF8: RawUTF8 absolute Output;
    OutputSynNameValue: TSynNameValue absolute Output;
begin // use our optimized RecordLoadSave/DynArrayLoadSave binary serialization
  header.Magic := REMOTE_MAGIC;
  header.SessionID := fCurrentSession;
  header.Command := Command;
  SetString(msgInput,PAnsiChar(@header),sizeof(header));
  case Command of
  cGetToken, cConnect, cDisconnect, cTryStartTransaction, cCommit, cRollback,
  cServerTimestamp, cGetTableNames, cGetForeignKeys, cQuit:
    ; // no input parameters here, just the command
  cGetDBMS, cGetFields, cGetIndexes:
    msgInput := msgInput+InputText;
  cExecute, cExecuteToBinary, cExecuteToJSON, cExecuteToExpandedJSON:
    msgInput := msgInput+
      RecordSave(InputExecute,TypeInfo(TSQLDBProxyConnectionCommandExecute));
  else raise ESQLDBRemote.CreateUTF8('Unknown %.Process() input command % (%)',
        [self,ToText(Command)^,ord(Command)]);
  end;
  ProcessMessage(fProtocol.HandleOutput(msgInput),msgRaw);
  msgOutput := fProtocol.HandleInput(msgRaw);
  outheader := pointer(msgOutput);
  if (outheader=nil) or (outheader.Magic<>REMOTE_MAGIC) then
    raise ESQLDBRemote.CreateUTF8('Wrong %.Process() returned content',[self]);
  O := pointer(msgOutput);
  inc(O,sizeof(header));
  case outheader.Command of
  cGetToken, cServerTimestamp:
    OutputInt64 := PInt64(O)^;
  cGetDBMS:
    OutputSQLDBDefinition := TSQLDBDefinition(O^);
  cConnect, cDisconnect, cCommit, cRollback, cQuit:
    ; // no output parameters here
  cTryStartTransaction:
    OutputBoolean := boolean(O^);
  cGetFields:
    DynArrayLoad(OutputSQLDBColumnDefineDynArray,O,TypeInfo(TSQLDBColumnDefineDynArray));
  cGetIndexes:
    DynArrayLoad(OutputSQLDBIndexDefineDynArray,O,TypeInfo(TSQLDBIndexDefineDynArray));
  cGetTableNames:
    DynArrayLoad(OutputRawUTF8DynArray,O,TypeInfo(TRawUTF8DynArray));
  cGetForeignKeys:
    OutputSynNameValue.SetBlobDataPtr(O);
  cExecute, cExecuteToBinary, cExecuteToJSON, cExecuteToExpandedJSON:
    FastSetString(OutputRawUTF8,O,length(msgOutput)-sizeof(header));
  cExceptionRaised: // msgOutput is ExceptionClassName+#0+ExceptionMessage
    raise ESQLDBRemote.CreateUTF8('%.Process(%): server raised % with ''%''',
      [self,ToText(Command)^,O,O+StrLen(O)+1]);
  else raise ESQLDBRemote.CreateUTF8('Unknown %.Process() output command % (%)',
        [self,ToText(outheader.Command)^,ord(outheader.Command)]);
  end;
  result := outHeader.SessionID;
end;


{ TSQLDBRemoteConnectionPropertiesTest }

constructor TSQLDBRemoteConnectionPropertiesTest.Create(
  aProps: TSQLDBConnectionProperties; const aUserID,aPassword: RawUTF8;
  aProtocol: TSQLDBProxyConnectionProtocolClass);
begin
  fProps := aProps;
  fProtocol := aProtocol.Create(TSynAuthentication.Create(aUserID,aPassword));
  inherited Create('','',aUserID,aPassword);
end;

procedure TSQLDBRemoteConnectionPropertiesTest.ProcessMessage(const Input: RawByteString;
  out Output: RawByteString);
begin
  fProps.ThreadSafeConnection.RemoteProcessMessage(Input,Output,fProtocol);
end;


{ TSQLDBProxyConnection }

constructor TSQLDBProxyConnection.Create(aProperties: TSQLDBConnectionProperties);
begin
  fProxy := aProperties as TSQLDBProxyConnectionPropertiesAbstract;
  inherited Create(aProperties);
end;

procedure TSQLDBProxyConnection.Commit;
begin
  inherited Commit; // dec(fTransactionCount)
  try
    fProxy.Process(cCommit,self,self);
  except
    inc(fTransactionCount); // the transaction is still active
    raise;
  end;
end;

procedure TSQLDBProxyConnection.Connect;
begin
  inherited Connect;
  if fProxy.HandleConnection then
    fProxy.Process(cConnect,self,self);
  fConnected := true;
end;

procedure TSQLDBProxyConnection.Disconnect;
begin
  inherited Disconnect;
  if fProxy.HandleConnection then
    fProxy.Process(cDisconnect,self,self);
  fConnected := false;
end;

function TSQLDBProxyConnection.GetServerDateTime: TDateTime;
var timestamp: TTimeLogBits;
begin
  fProxy.Process(cServerTimestamp,self,timestamp);
  result := timestamp.ToDateTime;
end;

function TSQLDBProxyConnection.IsConnected: boolean;
begin
  result := fConnected;
end;

function TSQLDBProxyConnection.NewStatement: TSQLDBStatement;
begin // always create a new proxy statement instance (cached on remote side)
  result := TSQLDBProxyStatement.Create(self);
end;

procedure TSQLDBProxyConnection.Rollback;
begin
  inherited Rollback;
  fProxy.Process(cRollback,self,self);
end;

procedure TSQLDBProxyConnection.StartTransaction;
var started: boolean;
    endTrial: Int64;
begin
  inherited StartTransaction;
  started := false;
  endTrial := GetTickCount64+fProxy.StartTransactionTimeOut;
  repeat
    fProxy.Process(cTryStartTransaction,self,started);
    if started or (GetTickCount64>endTrial) then
      break;
    SleepHiRes(10); // retry every 10 ms
  until false;
  if not started then begin
    inherited Rollback; // dec(fTransactionCount)
    raise ESQLDBRemote.CreateUTF8('Reached %("%/%").StartTransactionTimeOut=% ms',
      [self,fProxy.ServerName,fProxy.DatabaseName,fProxy.StartTransactionTimeOut]);
  end;
end;


{ TSQLDBProxyStatementAbstract }

procedure TSQLDBProxyStatementAbstract.IntHeaderProcess(Data: PByte; DataLen: integer);
var Magic,F,colCount: integer;
    p: PSQLDBColumnProperty;
begin
  fDataCurrentRowValuesStart := nil;
  fDataCurrentRowValuesSize := 0;
  fDataCurrentRowIndex := -1;
  fDataCurrentRowNull := nil;
  fDataCurrentRowNullLen := 0;
  repeat
    if DataLen<=5 then
      break; // to raise ESQLDBException
    fDataRowCount := PInteger(PtrUInt(Data)+PtrUInt(DataLen)-sizeof(Integer))^;
    Magic := FromVarUInt32(Data);
    if Magic<>FETCHALLTOBINARY_MAGIC then
      break; // corrupted
    colCount := FromVarUInt32(Data);
    SetLength(fDataCurrentRowColTypes,colCount);
    SetLength(fDataCurrentRowValues,colCount);
    fColumn.Capacity := colCount;
    for F := 0 to colCount-1 do begin
      p := fColumn.AddAndMakeUniqueName(FromVarString(Data));
      p^.ColumnType := TSQLDBFieldType(Data^);
      inc(Data);
      p^.ColumnValueDBSize := FromVarUInt32(Data);
      fDataCurrentRowColTypes[F] := p^.ColumnType;
    end;
    if fColumnCount=0 then
      exit; // no data returned
    if cardinal(fDataRowCount)>=cardinal(DataLen) then
      break; // obviously truncated
    fDataRowReaderOrigin := Data;
    fDataRowReader := Data;
    fDataRowNullSize := ((fColumnCount-1) shr 3)+1;
    SetLength(fDataCurrentRowNull,fDataRowNullSize);
    exit;
  until false;
  fDataRowCount := 0;
  fColumnCount := 0;
  raise ESQLDBException.CreateUTF8('Invalid %.IntHeaderProcess',[self]);
end;

procedure TSQLDBProxyStatementAbstract.IntFillDataCurrent(var Reader: PByte;
  IgnoreColumnDataSize: boolean);
var F,Len: Integer;
    ft: TSQLDBFieldType;
begin // format match TSQLDBStatement.FetchAllToBinary()
  if fDataCurrentRowNullLen>0 then
    FillCharFast(fDataCurrentRowNull[0],fDataCurrentRowNullLen,0);
  fDataCurrentRowNullLen := FromVarUInt32(Reader);
  if fDataCurrentRowNullLen>fDataRowNullSize then
    raise ESQLDBException.CreateUTF8('Invalid %.IntFillDataCurrent %>%',
      [self,fDataCurrentRowNullLen,fDataRowNullSize]);
  if fDataCurrentRowNullLen>0 then begin
    MoveFast(Reader^,fDataCurrentRowNull[0],fDataCurrentRowNullLen);
    inc(Reader,fDataCurrentRowNullLen);
  end;
  fDataCurrentRowValuesStart := Reader;
  for F := 0 to fColumnCount-1 do
    if GetBitPtr(pointer(fDataCurrentRowNull),F) then
      fDataCurrentRowValues[F] := nil else begin
      ft := fColumns[F].ColumnType;
      if ft<ftInt64 then begin // per-row column type (SQLite3 only)
        ft := TSQLDBFieldType(Reader^);
        inc(Reader);
      end;
      fDataCurrentRowColTypes[F] := ft;
      fDataCurrentRowValues[F] := Reader;
      case ft of
      ftInt64:
        Reader := GotoNextVarInt(Reader);
      ftDouble, ftCurrency, ftDate:
        inc(Reader,SizeOf(Int64));
      ftUTF8, ftBlob: begin
        Len := FromVarUInt32(Reader);
        if not IgnoreColumnDataSize then
          if Len>fColumns[F].ColumnDataSize then
            fColumns[F].ColumnDataSize := Len;
        inc(Reader,Len); // jump string/blob content
      end;
      else raise ESQLDBException.CreateUTF8('%.IntStep: Invalid ColumnType(%)=%',
        [self,fColumns[F].ColumnName,ord(ft)]);
      end;
    end;
  fDataCurrentRowValuesSize := PtrUInt(Reader)-PtrUInt(fDataCurrentRowValuesStart);
end;

procedure TSQLDBProxyStatementAbstract.ColumnsToJSON(WR: TJSONWriter);
var col, DataLen: integer;
    Data: PByte;
begin
  if WR.Expand then
    WR.Add('{');
  for col := 0 to fColumnCount-1 do begin
    if WR.Expand then
      WR.AddFieldName(fColumns[col].ColumnName); // add '"ColumnName":'
    Data := fDataCurrentRowValues[col];
    if Data=nil then
      WR.AddShort('null') else
    case fDataCurrentRowColTypes[col] of
      ftInt64:
        WR.Add(FromVarInt64Value(Data));
      ftDouble:
        WR.AddDouble(unaligned(PDouble(Data)^));
      ftCurrency:
        WR.AddCurr64(PInt64(Data)^);
      ftDate: begin
        WR.Add('"');
        WR.AddDateTime(PDateTime(Data)^);
        WR.Add('"');
      end;
      ftUTF8: begin
        WR.Add('"');
        DataLen := FromVarUInt32(Data);
        WR.AddJSONEscape(Data,DataLen);
        WR.Add('"');
      end;
      ftBlob:
      if fForceBlobAsNull then
        WR.AddShort('null') else begin
        DataLen := FromVarUInt32(Data);
        WR.WrBase64(PAnsiChar(Data),DataLen,{withMagic=}true);
      end;
    end;
    WR.Add(',');
  end;
  WR.CancelLastComma; // cancel last ','
  if WR.Expand then
    WR.Add('}');
end;

procedure TSQLDBProxyStatementAbstract.ColumnsToBinary(W: TFileBufferWriter;
  Null: pointer; const ColTypes: TSQLDBFieldTypeDynArray);
begin
  W.Write(fDataCurrentRowValuesStart,fDataCurrentRowValuesSize);
end;

function TSQLDBProxyStatementAbstract.ColumnData(Col: integer): pointer;
begin
  if (fDataCurrentRowValues<>nil) and (cardinal(Col)<cardinal(fColumnCount)) then
    result := fDataCurrentRowValues[col] else
    result := nil;
end;

function TSQLDBProxyStatementAbstract.ColumnType(Col: integer; FieldSize: PInteger): TSQLDBFieldType;
begin
  if (fDataRowCount>0) and (cardinal(Col)<cardinal(fColumnCount)) then
    if GetBitPtr(pointer(fDataCurrentRowNull),Col) then
      result := ftNull else
      with fColumns[Col] do begin
        if FieldSize<>nil then
          FieldSize^ := ColumnDataSize; // true max size as computed at loading
        result := fDataCurrentRowColTypes[Col]; // per-row column type (SQLite3)
      end else
    raise ESQLDBException.CreateUTF8('Invalid %.ColumnType()',[self]);
end;

function TSQLDBProxyStatementAbstract.IntColumnType(Col: integer; out Data: PByte): TSQLDBFieldType;
begin
  if (cardinal(Col)>=cardinal(fColumnCount)) or (fDataCurrentRowValues=nil) then
    result := ftUnknown else begin
    Data := fDataCurrentRowValues[Col];
    if Data=nil then
      result := ftNull else
      result := fDataCurrentRowColTypes[Col]; // per-row column type (SQLite3)
  end;
end;

function TSQLDBProxyStatementAbstract.ColumnCurrency(Col: integer): currency;
var Data: PByte;
begin
  case IntColumnType(Col,Data) of
  ftNull: result := 0;
  ftInt64: result := FromVarInt64Value(Data);
  ftDouble, ftDate: result := unaligned(PDouble(Data)^);
  ftCurrency: result := PCurrency(Data)^;
  else raise ESQLDBException.CreateUTF8('%.ColumnCurrency()',[self]);
  end;
end;

function TSQLDBProxyStatementAbstract.ColumnDateTime(Col: integer): TDateTime;
var Data: PByte;
begin
  case IntColumnType(Col,Data) of
  ftNull: result := 0;
  ftInt64: result := FromVarInt64Value(Data);
  ftDouble, ftDate: result := unaligned(PDouble(Data)^);
  ftUTF8: with FromVarBlob(Data) do
            result := Iso8601ToDateTimePUTF8Char(PUTF8Char(Ptr),Len);
  else raise ESQLDBException.CreateUTF8('%.ColumnDateTime()',[self]);
  end;
end;

function TSQLDBProxyStatementAbstract.ColumnDouble(Col: integer): double;
var Data: PByte;
begin
  case IntColumnType(Col,Data) of
  ftNull: result := 0;
  ftInt64: result := FromVarInt64Value(Data);
  ftDouble, ftDate: result := unaligned(PDouble(Data)^);
  ftCurrency: result := PCurrency(Data)^;
  else raise ESQLDBException.CreateUTF8('%.ColumnDouble()',[self]);
  end;
end;

function TSQLDBProxyStatementAbstract.ColumnInt(Col: integer): Int64;
var Data: PByte;
begin
  case IntColumnType(Col,Data) of
  ftNull: result := 0;
  ftInt64: result := FromVarInt64Value(Data);
  ftDouble, ftDate: result := Trunc(unaligned(PDouble(Data)^));
  ftCurrency: result := PInt64(Data)^ div 10000;
  else raise ESQLDBException.CreateUTF8('%.ColumnInt()',[self]);
  end;
end;

function TSQLDBProxyStatementAbstract.ColumnNull(Col: integer): boolean;
begin
  result := (cardinal(Col)>=cardinal(fColumnCount)) or
            GetBitPtr(pointer(fDataCurrentRowNull),Col);
end;

function TSQLDBProxyStatementAbstract.ColumnBlob(Col: integer): RawByteString;
var Data: PByte;
begin
  case IntColumnType(Col,Data) of
  ftNull: result := '';
  ftDouble, ftCurrency, ftDate: SetString(result,PAnsiChar(Data),sizeof(Int64));
  ftBlob, ftUTF8: with FromVarBlob(Data) do SetString(result,Ptr,Len);
  else raise ESQLDBException.CreateUTF8('%.ColumnBlob()',[self]);
  end;
end;

function TSQLDBProxyStatementAbstract.ColumnUTF8(Col: integer): RawUTF8;
var Data: PByte;
begin
  case IntColumnType(Col,Data) of
  ftNull: result := '';
  ftInt64: result := Int64ToUtf8(FromVarInt64Value(Data));
  ftDouble: result := DoubleToStr(unaligned(PDouble(Data)^));
  ftCurrency: result := Curr64ToStr(PInt64(Data)^);
  ftDate: DateTimeToIso8601TextVar(PDateTime(Data)^,'T',result);
  ftBlob, ftUTF8: with FromVarBlob(Data) do FastSetString(result,Ptr,Len);
  else raise ESQLDBException.CreateUTF8('%.ColumnUTF8()',[self]);
  end;
end;

function TSQLDBProxyStatementAbstract.ColumnString(Col: integer): string;
var Data: PByte;
begin
  case IntColumnType(Col,Data) of
  ftNull: result := '';
  ftInt64: result := IntToString(FromVarInt64Value(Data));
  ftDouble: result := DoubleToString(unaligned(PDouble(Data)^));
  ftCurrency: result := Curr64ToString(PInt64(Data)^);
  ftDate: DateTimeToIso8601StringVar(PDateTime(Data)^,'T',result);
  ftUTF8: with FromVarBlob(Data) do UTF8DecodeToString(PUTF8Char(Ptr),Len,result);
  ftBlob: with FromVarBlob(Data) do SetString(result,Ptr,Len shr 1);
  else raise ESQLDBException.CreateUTF8('%.ColumnString()',[self]);
  end;
end;


{ TSQLDBProxyStatement }

procedure TSQLDBProxyStatement.ParamsToCommand(var Input: TSQLDBProxyConnectionCommandExecute);
begin
  if (fColumnCount>0) or (fDataInternalCopy<>'') then
    raise ESQLDBException.CreateUTF8('Invalid %.ExecutePrepared* call',[self]);
  Input.SQL := fSQL;
  if length(fParams)<>fParamCount then // strip to only needed memory
    SetLength(fParams,fParamCount);
  Input.Params := fParams;
  Input.ArrayCount := fParamsArrayCount;
  if fForceBlobAsNull then
    Input.Force := [fBlobAsNull] else
    Input.Force := [];
  if fForceDateWithMS then
    include(Input.Force,fDateWithMS);
  if fForceNoUpdateCount then
    include(Input.Force,fNoUpdateCount);
end;

procedure TSQLDBProxyStatement.ExecutePrepared;
var Input: TSQLDBProxyConnectionCommandExecute;
const CMD: array[boolean] of TSQLDBProxyConnectionCommand = (
  cExecute, cExecuteToBinary);
begin
  inherited ExecutePrepared; // set fConnection.fLastAccessTicks
  // execute the statement
  ParamsToCommand(Input);
  TSQLDBProxyConnectionPropertiesAbstract(fConnection.fProperties).Process(
    CMD[fExpectResults],Input,fDataInternalCopy);
  if fExpectResults then
    // retrieve columns information from TSQLDBStatement.FetchAllToBinary() format
    IntHeaderProcess(pointer(fDataInternalCopy),Length(fDataInternalCopy)) else
    // retrieve UpdateCount value for plain cExecute command
    fUpdateCount := GetInteger(pointer(fDataInternalCopy));
end;

function TSQLDBProxyStatement.UpdateCount: integer;
begin
  result := fUpdateCount;
end;

procedure TSQLDBProxyStatement.ExecutePreparedAndFetchAllAsJSON(Expanded: boolean;
  out JSON: RawUTF8);
var Input: TSQLDBProxyConnectionCommandExecute;
const CMD: array[boolean] of TSQLDBProxyConnectionCommand = (
  cExecuteToJSON, cExecuteToExpandedJSON);
begin
  ParamsToCommand(Input);
  TSQLDBProxyConnectionPropertiesAbstract(fConnection.fProperties).Process(
    CMD[Expanded],Input,JSON);
end;

function TSQLDBProxyStatement.FetchAllToBinary(Dest: TStream; MaxRowCount: cardinal;
  DataRowPosition: PCardinalDynArray): cardinal;
begin
  if (MaxRowCount>0) and (MaxRowCount<cardinal(fDataRowCount)) then begin
    result := inherited FetchAllToBinary(Dest,MaxRowCount,DataRowPosition);
    exit;
  end;
  Dest.WriteBuffer(pointer(fDataInternalCopy)^,Length(fDataInternalCopy));
  if DataRowPosition<>nil then
    // TSQLDBProxyStatementRandomAccess.Create() will recompute it fast enough
    DataRowPosition^ := nil;
  result := fDataRowCount;
end;

function TSQLDBProxyStatement.Step(SeekFirst: boolean): boolean;
begin // retrieve one row of data from TSQLDBStatement.FetchAllToBinary() format
  if SeekFirst then
    fCurrentRow := 0;
  if (cardinal(fCurrentRow)>=cardinal(fDataRowCount)) then begin
    result := false; // no data was retrieved
    exit;
  end;
  if fCurrentRow=0 then begin
    fDataRowReader := fDataRowReaderOrigin;     // rewind TFileBufferReader
    fDataCurrentRowNullLen := fDataRowNullSize; // reset null
  end;
  IntFillDataCurrent(fDataRowReader,false);
  inc(fCurrentRow);
  result := true;
end;


{ TSQLDBProxyStatementRandomAccess }

constructor TSQLDBProxyStatementRandomAccess.Create(Data: PByte; DataLen: integer;
  DataRowPosition: PCardinalDynArray; IgnoreColumnDataSize: boolean);
var i,f: integer;
    Reader: PByte;
begin
  inherited Create(nil);
  IntHeaderProcess(Data,DataLen);
  Reader := fDataRowReaderOrigin;
  if (DataRowPosition<>nil) and (DataRowPosition^<>nil) then begin
    fRowData := DataRowPosition^; // fast copy-on-write
    if not IgnoreColumnDataSize then
    for f := 0 to fColumnCount-1 do
      with fColumns[f] do
      if ColumnType in [ftUTF8,ftBlob] then
        if ColumnValueDBSize=0 then begin // unknown size -> compute
          for i := 0 to DataRowCount-1 do
            IntFillDataCurrent(Reader,false); // will compute ColumnDataSize
          break;
        end else
          ColumnDataSize := ColumnValueDBSize; // use declared maximum size
  end else begin
    SetLength(fRowData,DataRowCount);
    for i := 0 to DataRowCount-1 do begin
      fRowData[i] := PtrUInt(Reader)-PtrUInt(fDataRowReaderOrigin);
      IntFillDataCurrent(Reader,IgnoreColumnDataSize); // will also compute ColumnDataSize
    end;
  end;
end;

function TSQLDBProxyStatementRandomAccess.GotoRow(Index: integer;
  RaiseExceptionOnWrongIndex: Boolean): boolean;
var Reader: PByte;
begin
  result := (cardinal(Index)<cardinal(fDataRowCount)) and (fColumnCount>0);
  if not result then
    if RaiseExceptionOnWrongIndex then
      raise ESQLDBException.CreateUTF8('Invalid %.GotoRow(%)',[self,Index]) else
      exit;
  if fDataCurrentRowIndex<>Index then begin // compute only if changed :)
    Reader := @PAnsiChar(fDataRowReaderOrigin)[fRowData[Index]];
    IntFillDataCurrent(Reader,false);
    fDataCurrentRowIndex := Index;
  end;
end;

procedure TSQLDBProxyStatementRandomAccess.ExecutePrepared;
begin
  raise ESQLDBException.CreateUTF8('Unexpected %.ExecutePrepared',[self]);
end;

function TSQLDBProxyStatementRandomAccess.Step(SeekFirst: boolean=false): boolean;
begin
  raise ESQLDBException.CreateUTF8('Unexpected %.Step',[self]);
end;

{$endif WITH_PROXY}


{ ESQLDBException }

constructor ESQLDBException.CreateUTF8(const Format: RawUTF8; const Args: array of const);
var msg {$ifndef SYNDB_SILENCE}, sql{$endif}: RawUTF8;
begin
  msg := FormatUTF8(Format,Args);
  {$ifndef SYNDB_SILENCE}
  if (length(Args)>0) and (Args[0].VType=vtObject) and (Args[0].VObject<>nil) then
    if Args[0].VObject.InheritsFrom(TSQLDBStatement) then begin
      fStatement := TSQLDBStatement(Args[0].VObject);
      if fStatement.Connection.Properties.LogSQLStatementOnException then begin
        try
          sql := fStatement.GetSQLWithInlinedParams;
        except
          sql := fStatement.SQL; // if parameter access failed -> append with ?
        end;
        msg := msg+' - '+sql;
      end;
    end;
  {$endif}
  inherited Create(UTF8ToString(msg));
end;


const
  __TSQLDBColumnDefine = 'ColumnName,ColumnTypeNative RawUTF8 '+
    'ColumnLength,ColumnPrecision,ColumnScale PtrInt '+
    'ColumnType TSQLDBFieldType ColumnIndexed boolean';

initialization
  assert(SizeOf(TSQLDBColumnProperty)=sizeof(PTrUInt)*2+20);
  TTextWriter.RegisterCustomJSONSerializerFromTextSimpleType(TypeInfo(TSQLDBFieldType));
  TTextWriter.RegisterCustomJSONSerializerFromText(
    TypeInfo(TSQLDBColumnDefine),__TSQLDBColumnDefine);
end.

