xtool/contrib/mORMot/SynDB.pas

9058 lines
365 KiB
ObjectPascal

/// 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.