1009 lines
34 KiB
ObjectPascal
1009 lines
34 KiB
ObjectPascal
/// PostgreSQL direct access classes for SynDB units (not DB.pas based)
|
|
// - this unit is a part of the freeware Synopse framework,
|
|
// licensed under a MPL/GPL/LGPL tri-license - see LICENSE.md
|
|
unit SynDBPostgres;
|
|
|
|
{
|
|
*****************************************************************************
|
|
Implementation of TSQLDB* for PostgreSQL using libpg
|
|
|
|
Features:
|
|
- fast, minimum memory allocation
|
|
- includes its own simple wrapper to the libpq native client
|
|
- perfect fit for our ORM (JSON results, bulk insert/update/delete)
|
|
- array binding for select statements (caller should use =ANY(?) etc.)
|
|
|
|
Limitations:
|
|
- works with PostgreSQL>=7.4 and (v3 protocol)
|
|
- consider creating the database with UTF8 collation
|
|
- notifications are not implemented
|
|
- Postgres level prepared cached statements works only for SQLs what starts
|
|
exactly with SELECT INSERT UPDATE DELETE VALUES and not contains ";"
|
|
- parameters parser will fails in case SQL contains comments with ? inside
|
|
(TODO - will be fixed)
|
|
- all query rows are returned at once, caller should care about pagination
|
|
(TODO - implement singleRowMode?)
|
|
|
|
Aim of this unit is to provide simple alternative to SynDBZeos for PostgreSQL
|
|
*****************************************************************************
|
|
}
|
|
|
|
interface
|
|
|
|
{$I Synopse.inc} // define HASINLINE CPU32 CPU64 OWNNORMTOUPPER
|
|
|
|
uses
|
|
{$ifdef MSWINDOWS}
|
|
Windows,
|
|
{$endif}
|
|
SysUtils,
|
|
SynCommons,
|
|
SynTable,
|
|
SynDB;
|
|
|
|
type
|
|
/// exception type associated to the native libpg Interface
|
|
ESQLDBPostgres = class(ESQLDBException);
|
|
|
|
/// connection properties which will implement an internal Thread-Safe
|
|
// connection pool
|
|
TSQLDBPostgresConnectionProperties = class(TSQLDBConnectionPropertiesThreadSafe)
|
|
private
|
|
fOids: TWordDynArray; // O(n) search in L1 cache - use SSE2 on FPC x86_64
|
|
fOidsFieldTypes: TSQLDBFieldTypeDynArray;
|
|
fOidsCount: integer;
|
|
protected
|
|
procedure GetForeignKeys; override;
|
|
/// fill mapping of standard OID
|
|
// - at runtime mapping can be defined using Oid2FieldType() method
|
|
// - OIDs defined in DB can be retrieved using query
|
|
// "select oid, typname from pg_type where typtype = 'b' order by oid"
|
|
procedure FillOidMapping; virtual;
|
|
public
|
|
/// initialize the properties
|
|
// - raise an exception in case libpg is not thead-safe
|
|
// - aDatabaseName can be a Connection URI - see
|
|
// https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING
|
|
// - if aDatabaseName contains connection URI with password we recommend to repeat password
|
|
// in aPassword parameter to prevent logging it (see TSQLDBConnectionProperties.DatabaseNameSafe)
|
|
// - better to use environment variables and postgres config file for connection parameters
|
|
constructor Create(const aServerName, aDatabaseName, aUserID, aPassword: RawUTF8); override;
|
|
/// create a new connection
|
|
// - caller is responsible of freeing this instance
|
|
// - this overridden method will create an TSQLDBPostgresConnection instance
|
|
function NewConnection: TSQLDBConnection; override;
|
|
/// add or replace mapping of OID into TSQLDBFieldType
|
|
// - in case mapping for OID is not defined, returns ftUTF8
|
|
function Oid2FieldType(cOID: cardinal): TSQLDBFieldType; {$ifdef HASINLINE} inline; {$endif}
|
|
// add new (or override existed) OID to FieldType mapping
|
|
procedure MapOid(cOid: cardinal; fieldType: TSQLDBFieldType);
|
|
end;
|
|
|
|
|
|
/// implements a connection via the libpq access layer
|
|
TSQLDBPostgresConnection = class(TSQLDBConnectionThreadSafe)
|
|
protected
|
|
// prepared statement names = SHA-256 of its SQL
|
|
fPrepared: THash256DynArray; // O(n) fast search in L1 cache
|
|
fPreparedCount: integer;
|
|
// the associated low-level provider connection
|
|
fPGConn: pointer;
|
|
// fServerSettings: set of (ssByteAasHex);
|
|
// maintain fPrepared[] hash list to identify already cached
|
|
// - returns statement index in prepared cache array
|
|
function PrepareCached(const aSQL: RawUTF8; aParamCount: integer;
|
|
out aName: RawUTF8): integer;
|
|
/// direct execution of SQL statement what do not returns a result
|
|
// - statement should not contains parameters
|
|
// - raise an ESQLDBPostgres on error
|
|
procedure DirectExecSQL(const SQL: RawUTF8); overload;
|
|
/// direct execution of SQL statement what do not returns a result
|
|
// - overloaded method to return a single value e.g. from a SELECT
|
|
procedure DirectExecSQL(const SQL: RawUTF8; out Value: RawUTF8); overload;
|
|
/// query the pg_settings table for a given setting
|
|
function GetServerSetting(const Name: RawUTF8): RawUTF8;
|
|
public
|
|
/// connect to the specified server
|
|
// - should raise an ESQLDBPostgres on error
|
|
procedure Connect; override;
|
|
/// stop connection to the specified PostgreSQL database server
|
|
// - should raise an ESQLDBPostgres on error
|
|
procedure Disconnect; override;
|
|
/// return TRUE if Connect has been already successfully called
|
|
function IsConnected: boolean; override;
|
|
/// create a new statement instance
|
|
function NewStatement: TSQLDBStatement; override;
|
|
/// begin a Transaction for this connection
|
|
procedure StartTransaction; override;
|
|
/// commit changes of a Transaction for this connection
|
|
// - StartTransaction method must have been called before
|
|
procedure Commit; override;
|
|
/// discard changes of a Transaction for this connection
|
|
// - StartTransaction method must have been called before
|
|
procedure Rollback; override;
|
|
/// direct access to the associated PPGconn connection
|
|
property Direct: pointer read fPGConn;
|
|
/// how many prepared statements are currently cached for this connection
|
|
property PreparedCount: integer read fPreparedCount;
|
|
end;
|
|
|
|
|
|
/// implements a statement via a Postgres database connection
|
|
TSQLDBPostgresStatement = class(TSQLDBStatementWithParamsAndColumns)
|
|
protected
|
|
fPreparedStmtName: RawUTF8; // = SHA-256 of the SQL
|
|
fPreparedParamsCount: integer;
|
|
fRes: pointer;
|
|
fResStatus: integer;
|
|
// pointers to query parameters; initialized by Prepare, filled in Executeprepared
|
|
fPGParams: TPointerDynArray;
|
|
// 0 - text, 1 - binary; initialized by Prepare, filled in Executeprepared
|
|
fPGParamFormats: TIntegerDynArray;
|
|
// non zero for binary params
|
|
fPGparamLengths: TIntegerDynArray;
|
|
/// define the result columns name and content
|
|
procedure BindColumns;
|
|
/// raise an exception if Col is out of range according to fColumnCount
|
|
// or rowset is not initialized
|
|
procedure CheckColAndRowset(const Col: integer);
|
|
public
|
|
/// finalize the statement for a given connection
|
|
destructor Destroy; override;
|
|
/// 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
|
|
// - raise an ESQLDBPostgres on any error
|
|
procedure Prepare(const aSQL: RawUTF8; ExpectResults: boolean = False); overload; override;
|
|
/// Execute a prepared SQL statement
|
|
// - parameters marked as ? should have been already bound with Bind*() functions
|
|
// - this implementation will also handle bound array of values (if any)
|
|
// - this overridden method will log the SQL statement if sllSQL has been
|
|
// enabled in SynDBLog.Family.Level
|
|
// - raise an ESQLDBPostgres on any error
|
|
procedure ExecutePrepared; override;
|
|
/// gets a number of updates made by latest executed statement
|
|
function UpdateCount: integer; override;
|
|
/// Reset the previous prepared statement
|
|
// - this overridden implementation will reset all bindings and the cursor state
|
|
// - raise an ESQLDBPostgres on any error
|
|
procedure Reset; override;
|
|
|
|
/// Access the next or first row of data from the SQL Statement result
|
|
// - return true on success, with data ready to be retrieved by Column*() methods
|
|
// - return false if no more row is available (e.g. if the SQL statement
|
|
// is not a SELECT but an UPDATE or INSERT command)
|
|
// - if SeekFirst is TRUE, will put the cursor on the first row of results
|
|
// - raise an ESQLDBPostgres on any error
|
|
function Step(SeekFirst: boolean = False): boolean; override;
|
|
/// clear(fRes) when ISQLDBStatement is back in cache
|
|
procedure ReleaseRows; override;
|
|
/// return a Column integer value of the current Row, first Col is 0
|
|
function ColumnInt(Col: integer): int64; override;
|
|
/// returns TRUE if the column contains NULL
|
|
function ColumnNull(Col: integer): boolean; override;
|
|
/// return a Column floating point value of the current Row, first Col is 0
|
|
function ColumnDouble(Col: integer): double; override;
|
|
/// return a Column date and time 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
|
|
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 as a blob value of the current Row, first Col is 0
|
|
function ColumnBlob(Col: integer): RawByteString; override;
|
|
/// append all columns values of the current Row to a JSON stream
|
|
// - overriden method to avoid temporary memory allocation or conversion
|
|
procedure ColumnsToJSON(WR: TJSONWriter); override;
|
|
/// how many parameters founded during prepare stage
|
|
property PreparedParamsCount: integer read fPreparedParamsCount;
|
|
end;
|
|
|
|
|
|
var
|
|
/// allow to specify a libpq library file name to use
|
|
SynDBPostgresLibrary: TFileName;
|
|
|
|
|
|
implementation
|
|
|
|
uses
|
|
SynLog,
|
|
SynCrypto; // libpq requires named prepared statements = use SHA-256
|
|
|
|
|
|
{ *********** minimal access to libpq native Postgres client library }
|
|
|
|
const // see pg_type.h
|
|
BOOLOID = 16;
|
|
BYTEAOID = 17;
|
|
INT8OID = 20;
|
|
INT2OID = 21;
|
|
INT4OID = 23;
|
|
REGPROCOID = 24;
|
|
TEXTOID = 25;
|
|
OIDOID = 26;
|
|
FLOAT4OID = 700;
|
|
FLOAT8OID = 701;
|
|
ABSTIMEOID = 702;
|
|
CASHOID = 790;
|
|
DATEOID = 1082;
|
|
TIMEOID = 1083;
|
|
TIMESTAMPOID = 1114;
|
|
TIMESTAMPTZOID = 1184;
|
|
TIMETZOID = 1266;
|
|
NUMERICOID = 1700;
|
|
|
|
CHAROID = 18;
|
|
NAMEOID = 19;
|
|
INT2VECTOROID = 22;
|
|
TIDOID = 27;
|
|
XIDOID = 28;
|
|
CIDOID = 29;
|
|
OIDVECTOROID = 30;
|
|
JSONOID = 114;
|
|
XMLOID = 142;
|
|
PGNODETREEOID = 194;
|
|
PGDDLCOMMANDOID = 32;
|
|
POINTOID= 600;
|
|
LSEGOID = 601;
|
|
PATHOID = 602;
|
|
BOXOID = 603;
|
|
POLYGONOID = 604;
|
|
LINEOID = 628;
|
|
RELTIMEOID = 703;
|
|
TINTERVALOID = 704;
|
|
UNKNOWNOID = 705;
|
|
CIRCLEOID = 718;
|
|
MACADDROID = 829;
|
|
INETOID = 869;
|
|
CIDROID = 650;
|
|
INT2ARRAYOID = 1005;
|
|
INT4ARRAYOID = 1007;
|
|
TEXTARRAYOID= 1009;
|
|
OIDARRAYOID = 1028;
|
|
FLOAT4ARRAYOID = 1021;
|
|
ACLITEMOID = 1033;
|
|
CSTRINGARRAYOID = 1263;
|
|
BPCHAROID = 1042;
|
|
VARCHAROID = 1043;
|
|
INTERVALOID = 1186;
|
|
BITOID = 1560;
|
|
VARBITOID = 1562;
|
|
REFCURSOROID= 1790;
|
|
REGPROCEDUREOID = 2202;
|
|
REGOPEROID = 2203;
|
|
REGOPERATOROID = 2204;
|
|
REGCLASSOID = 2205;
|
|
REGTYPEOID = 2206;
|
|
REGROLEOID = 4096;
|
|
REGNAMESPACEOID = 4089;
|
|
REGTYPEARRAYOID = 2211;
|
|
UUIDOID = 2950;
|
|
LSNOID = 3220;
|
|
TSVECTOROID = 3614;
|
|
GTSVECTOROID = 3642;
|
|
TSQUERYOID = 3615;
|
|
REGCONFIGOID = 3734;
|
|
REGDICTIONARYOID = 3769;
|
|
JSONBOID = 3802;
|
|
INT4RANGEOID = 3904;
|
|
|
|
const
|
|
PGRES_EMPTY_QUERY = 0;
|
|
PGRES_COMMAND_OK = 1;
|
|
PGRES_TUPLES_OK = 2;
|
|
PGRES_COPY_OUT = 3;
|
|
PGRES_COPY_IN = 4;
|
|
PGRES_BAD_RESPONSE = 5;
|
|
PGRES_NONFATAL_ERROR = 6;
|
|
PGRES_FATAL_ERROR = 7;
|
|
|
|
CONNECTION_OK = 0;
|
|
CONNECTION_BAD = 1;
|
|
CONNECTION_STARTED = 2;
|
|
CONNECTION_MADE = 3;
|
|
CONNECTION_AWAITING_RESPONSE = 4;
|
|
CONNECTION_AUTH_OK = 5;
|
|
CONNECTION_SETENV = 6;
|
|
CONNECTION_SSL_STARTUP = 7;
|
|
CONNECTION_NEEDED = 8;
|
|
|
|
PGFMT_TEXT = 0;
|
|
PGFMT_BIN = 1;
|
|
|
|
type
|
|
PPGconn = type pointer;
|
|
PPGresult = type pointer;
|
|
PPPGresult = ^PPGresult;
|
|
|
|
PQnoticeProcessor = procedure(arg: pointer; message: PUTF8Char); cdecl;
|
|
|
|
/// direct access to the libpq native Postgres protocol 3 library
|
|
// - only the endpoints needed by this unit are imported
|
|
TSQLDBPostgresLib = class(TSQLDBLib)
|
|
public
|
|
LibVersion: function: integer; cdecl;
|
|
IsThreadSafe: function: integer; cdecl;
|
|
SetDBLogin: function(pghost, pgport, pgoptions, pgtty, dbName,
|
|
login, pwd: PUTF8Char): PPGconn; cdecl;
|
|
Status: function(conn: PPGconn): integer; cdecl;
|
|
Finish: procedure(conn: PPGconn); cdecl;
|
|
ResultStatus: function(res: PPGresult): integer; cdecl;
|
|
ResultErrorField: function(res: PPGresult; fieldcode: integer): PUTF8Char; cdecl;
|
|
ErrorMessage: function(conn: PPGconn): PUTF8Char; cdecl;
|
|
SetNoticeProcessor: function(conn: PPGconn; proc: PQnoticeProcessor;
|
|
arg: pointer): PQnoticeProcessor; cdecl;
|
|
Clear: procedure(res: PPGresult); cdecl;
|
|
Freemem: procedure(ptr: pointer); cdecl;
|
|
Exec: function(conn: PPGconn; query: PUTF8Char): PPGresult; cdecl;
|
|
Prepare: function(conn: PPGconn; stmtName, query: PUTF8Char; nParams: integer;
|
|
paramTypes: PCardinal): PPGresult; cdecl;
|
|
ExecPrepared: function(conn: PPGconn; stmtName: PUTF8Char; nParams: integer;
|
|
paramValues: PPchar; paramLengths, paramFormats: PInteger;
|
|
resultFormat: integer): PPGresult; cdecl;
|
|
ExecParams: function(conn: PPGconn; command: PUTF8Char; nParams: integer;
|
|
paramTypes: PCardinal; paramValues: PPchar; paramLengths, paramFormats: PInteger;
|
|
resultFormat: integer):PPGresult; cdecl;
|
|
nfields: function(res: PPGresult): integer; cdecl;
|
|
ntuples: function(res: PPGresult): integer; cdecl;
|
|
cmdTuples: function(res: PPGresult): PUTF8Char; cdecl;
|
|
fname: function(res: PPGresult; field_num: integer): PUTF8Char; cdecl;
|
|
ftype: function(res: PPGresult; field_num: integer): cardinal; cdecl;
|
|
GetValue: function(res: PPGresult; tup_num, field_num: integer): PUTF8Char; cdecl;
|
|
GetLength: function(res: PPGresult; tup_num, field_num: integer): integer; cdecl;
|
|
GetIsNull: function(res: PPGresult; tup_num, field_num: integer): integer; cdecl;
|
|
/// try to dynamically load the libpq library
|
|
// - raise ESQLDBPostgres if the expected library is not found
|
|
constructor Create;
|
|
/// just a wrapper around FastSetString + GetValue/GetLength
|
|
procedure GetRawUTF8(res: PPGresult; tup_num, field_num: integer;
|
|
var result: RawUTF8);
|
|
/// raise an exception on error and clean result
|
|
// - will set pRes to nil if passed
|
|
// - if andClear is true - will call always PQ.Clear(res)
|
|
procedure Check(conn: PPGconn; res: PPGresult;
|
|
pRes: PPPGresult = nil; andClear: boolean = true);
|
|
end;
|
|
|
|
const
|
|
PQ_ENTRIES: array[0..22] of PChar = (
|
|
'PQlibVersion', 'PQisthreadsafe', 'PQsetdbLogin', 'PQstatus', 'PQfinish',
|
|
'PQresultStatus', 'PQresultErrorField', 'PQerrorMessage', 'PQsetNoticeProcessor',
|
|
'PQclear', 'PQfreemem', 'PQexec', 'PQprepare', 'PQexecPrepared', 'PQexecParams',
|
|
'PQnfields', 'PQntuples', 'PQcmdTuples', 'PQfname', 'PQftype', 'PQgetvalue',
|
|
'PQgetlength', 'PQgetisnull');
|
|
|
|
var
|
|
PQ: TSQLDBPostgresLib = nil;
|
|
|
|
{ TSQLDBPostgresLib }
|
|
|
|
const
|
|
LIBNAME = {$ifdef MSWINDOWS}'libpq.dll'{$else}
|
|
{$ifdef darwin}'libpq.dylib'{$else}'libpq.so.5'{$endif}{$endif};
|
|
LIBNAME2 = {$ifdef MSWINDOWS}''{$else}
|
|
{$ifdef darwin}''{$else}'libpq.so.4'{$endif}{$endif};
|
|
|
|
constructor TSQLDBPostgresLib.Create;
|
|
var
|
|
P: PPointer;
|
|
i: PtrInt;
|
|
l2: TFileName;
|
|
begin
|
|
if LIBNAME2 <> '' then
|
|
l2 := ExeVersion.ProgramFilePath + LIBNAME2;
|
|
TryLoadLibrary([SynDBPostgresLibrary, ExeVersion.ProgramFilePath + LIBNAME,
|
|
l2, LIBNAME, LIBNAME2], ESQLDBPostgres);
|
|
P := @@LibVersion;
|
|
for i := 0 to High(PQ_ENTRIES) do
|
|
begin
|
|
P^ := GetProcAddress(fHandle, PQ_ENTRIES[i]);
|
|
if P^ = nil then
|
|
begin
|
|
FreeLibrary(fHandle);
|
|
fHandle := 0;
|
|
raise ESQLDBPostgres.CreateUTF8('Invalid %: missing %', [LIBNAME, PQ_ENTRIES[i]]);
|
|
end;
|
|
inc(P);
|
|
end;
|
|
end;
|
|
|
|
procedure TSQLDBPostgresLib.GetRawUTF8(res: PPGresult;
|
|
tup_num, field_num: integer; var result: RawUTF8);
|
|
begin
|
|
FastSetString(result, GetValue(res, tup_num, field_num),
|
|
GetLength(res, tup_num, field_num));
|
|
end;
|
|
|
|
procedure TSQLDBPostgresLib.Check(conn: PPGconn; res: PPGresult; pRes: PPPGresult;
|
|
andClear: boolean);
|
|
var
|
|
errMsg, errCode: PUTF8Char;
|
|
begin
|
|
if (res = nil) or // nil in case of very fatal error, out of emory for example
|
|
(ResultStatus(res) in [PGRES_BAD_RESPONSE, PGRES_NONFATAL_ERROR,
|
|
PGRES_FATAL_ERROR]) then
|
|
begin
|
|
errMsg := ErrorMessage(conn);
|
|
if res <> nil then
|
|
errCode := ResultErrorField(res, Ord('C'){PG_DIAG_SQLSTATE})
|
|
else
|
|
errCode := nil;
|
|
Clear(res);
|
|
if pRes <> nil then
|
|
pRes^ := nil;
|
|
raise ESQLDBPostgres.CreateUTF8('% PGERRCODE: %, %', [self, errCode, errMsg]);
|
|
end
|
|
else if andClear then
|
|
Clear(res);
|
|
end;
|
|
|
|
|
|
{ TSQLDBPostgresConnection }
|
|
|
|
function TSQLDBPostgresConnection.PrepareCached(const aSQL: RawUTF8; aParamCount: integer;
|
|
out aName: RawUTF8): integer;
|
|
var
|
|
dig: TSHA256Digest;
|
|
begin
|
|
dig := SHA256Digest(aSQL);
|
|
aName := SHA256DigestToString(dig);
|
|
result := Hash256Index(pointer(fPrepared), fPreparedCount, @dig);
|
|
if result >= 0 then
|
|
exit; // already prepared
|
|
PQ.Check(fPGConn, PQ.Prepare(fPGConn, pointer(aName), pointer(aSQL), aParamCount, nil));
|
|
result := fPreparedCount;
|
|
inc(fPreparedCount);
|
|
if result = length(fPrepared) then
|
|
SetLength(fPrepared, result + 32);
|
|
fPrepared[result] := dig;
|
|
end;
|
|
|
|
procedure TSQLDBPostgresConnection.DirectExecSQL(const SQL: RawUTF8);
|
|
begin
|
|
PQ.Check(fPGConn, PQ.Exec(fPGConn, pointer(SQL)));
|
|
end;
|
|
|
|
procedure TSQLDBPostgresConnection.DirectExecSQL(const SQL: RawUTF8; out Value: RawUTF8);
|
|
var
|
|
res: PPGresult;
|
|
begin
|
|
res := PQ.Exec(fPGConn, pointer(SQL));
|
|
PQ.Check(fPGConn, res, nil, {andclear=}false);
|
|
PQ.GetRawUTF8(res, 0, 0, Value);
|
|
PQ.Clear(res);
|
|
end;
|
|
|
|
function TSQLDBPostgresConnection.GetServerSetting(const Name: RawUTF8): RawUTF8;
|
|
var
|
|
sql: RawUTF8;
|
|
begin
|
|
FormatUTF8('select setting from pg_settings where name=''%''', [Name], sql);
|
|
DirectExecSQL(sql, result);
|
|
end;
|
|
|
|
// our conversion is faster than PQUnescapeByteA - which requires libpq 8.3+
|
|
// and calls malloc()
|
|
// https://github.com/postgres/postgres/blob/master/src/interfaces/libpq/fe-exec.c
|
|
|
|
// checking \x for hexadecimal encoding is what UnescapeByteA() does
|
|
// -> no need to ask server settings
|
|
// note: bytea_output is HEX by default (at least since PostgreSQL 9.0)
|
|
|
|
function BlobInPlaceDecode(P: PAnsiChar; PLen: integer): integer;
|
|
begin
|
|
if (P = nil) or (PLen <= 0) then
|
|
result := 0
|
|
else
|
|
if PWord(P)^ = ord('\') + ord('x') shl 8 then {ssByteAasHex in fServerSettings}
|
|
begin
|
|
result := (PLen - 2) shr 1; // skip trailing \x and compute number of bytes
|
|
if result > 0 then
|
|
HexToBinFast(P + 2, PByte(P), result); // in-place conversion
|
|
end
|
|
else
|
|
result := OctToBin(P, pointer(P)); // in-place conversion
|
|
end;
|
|
|
|
procedure SynLogNoticeProcessor({%H-}arg: Pointer; message: PUTF8Char); cdecl;
|
|
begin
|
|
SynDBLog.Add.Log(sllTrace, 'PGINFO: %', [message], TObject(arg));
|
|
end;
|
|
|
|
procedure DummyNoticeProcessor({%H-}arg: Pointer; message: PUTF8Char); cdecl;
|
|
begin
|
|
end;
|
|
|
|
procedure TSQLDBPostgresConnection.Connect;
|
|
var
|
|
log: ISynLog;
|
|
host, port: RawUtf8;
|
|
begin
|
|
log := SynDBLog.Enter(self, 'Connect');
|
|
Disconnect; // force fTrans=fError=fServer=fContext=nil
|
|
try
|
|
Split(Properties.ServerName, ':', host, port);
|
|
fPGConn := PQ.SetDBLogin(pointer(host), pointer(port), nil, nil,
|
|
pointer(Properties.DatabaseName), pointer(Properties.UserID),
|
|
pointer(Properties.PassWord));
|
|
if PQ.Status(fPGConn) = CONNECTION_BAD then
|
|
raise ESQLDBPostgres.CreateUTF8('Connection to database % failed [%]',
|
|
[Properties.DatabaseNameSafe, PQ.ErrorMessage(fPGConn)]);
|
|
// if GetServerSetting('bytea_output') = 'HEX' then
|
|
// include(fServerSettings, ssByteAasHex);
|
|
if log <> nil then
|
|
begin
|
|
PQ.SetNoticeProcessor(fPGConn, SynLogNoticeProcessor, pointer(self));
|
|
log.Log(sllDB, 'Connected to % % using % v%', [fProperties.ServerName,
|
|
fProperties.DatabaseNameSafe, PQ.fLibraryPath, PQ.LibVersion], self);
|
|
end
|
|
else
|
|
// to ensure no performance drop due to notice to console
|
|
PQ.SetNoticeProcessor(fPGConn, DummyNoticeProcessor, nil);
|
|
inherited Connect; // notify any re-connection
|
|
except
|
|
on E: Exception do
|
|
begin
|
|
if log <> nil then
|
|
log.Log(sllError, 'Connect: % on %', [E, Properties.DatabaseNameSafe], self);
|
|
Disconnect; // clean up on fail
|
|
raise;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TSQLDBPostgresConnection.Disconnect;
|
|
begin
|
|
try
|
|
inherited Disconnect;
|
|
finally
|
|
if fPGConn <> nil then
|
|
begin
|
|
PQ.Finish(fPGConn);
|
|
fPGConn := nil;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function TSQLDBPostgresConnection.IsConnected: boolean;
|
|
begin
|
|
result := (fPGConn <> nil);
|
|
end;
|
|
|
|
function TSQLDBPostgresConnection.NewStatement: TSQLDBStatement;
|
|
begin
|
|
result := TSQLDBPostgresStatement.Create(self);
|
|
end;
|
|
|
|
procedure TSQLDBPostgresConnection.StartTransaction;
|
|
var
|
|
log: ISynLog;
|
|
begin
|
|
log := SynDBLog.Enter(self, 'StartTransaction');
|
|
if TransactionCount > 0 then
|
|
raise ESQLDBPostgres.CreateUTF8('Invalid %.StartTransaction: nested ' +
|
|
'transactions are not supported by the Postgres - use SAVEPOINT instead', [self]);
|
|
try
|
|
inherited StartTransaction;
|
|
DirectExecSQL('START TRANSACTION');
|
|
except
|
|
on E: Exception do
|
|
begin
|
|
if log <> nil then
|
|
log.Log(sllError, 'StartTransaction: % on %', [E, Properties.DatabaseNameSafe], self);
|
|
if fTransactionCount > 0 then
|
|
Dec(fTransactionCount);
|
|
raise;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TSQLDBPostgresConnection.Commit;
|
|
begin
|
|
inherited Commit;
|
|
try
|
|
DirectExecSQL('COMMIT');
|
|
except
|
|
inc(fTransactionCount); // the transaction is still active
|
|
raise;
|
|
end;
|
|
end;
|
|
|
|
procedure TSQLDBPostgresConnection.Rollback;
|
|
begin
|
|
inherited;
|
|
DirectExecSQL('ROLLBACK');
|
|
end;
|
|
|
|
{ TSQLDBPostgresConnectionProperties }
|
|
procedure TSQLDBPostgresConnectionProperties.GetForeignKeys;
|
|
begin
|
|
// TODO - how to get field we reference to? (currently consider this is "ID")
|
|
with Execute('SELECT' + ' ct.conname as foreign_key_name, ' +
|
|
' case when ct.condeferred then 1 else 0 end AS is_disabled, ' +
|
|
' (SELECT tc.relname from pg_class tc where tc.oid = ct.conrelid) || ''.'' || ' +
|
|
' (SELECT a.attname FROM pg_attribute a WHERE a.attnum = ct.conkey[1] AND a.attrelid = ct.conrelid) as from_ref, ' +
|
|
' (SELECT tc.relname from pg_class tc where tc.oid = ct.confrelid) || ''.id'' as referenced_object ' +
|
|
'FROM pg_constraint ct WHERE contype = ''f''', []) do
|
|
while Step do
|
|
fForeignKeys.Add(ColumnUTF8(2), ColumnUTF8(3));
|
|
end;
|
|
|
|
procedure TSQLDBPostgresConnectionProperties.FillOidMapping;
|
|
begin // see pg_type.h (most used first)
|
|
mapOid(INT4OID, ftInt64);
|
|
mapOid(INT8OID, ftInt64);
|
|
mapOid(TEXTOID, ftUTF8);
|
|
mapOid(FLOAT8OID, ftDouble);
|
|
mapOid(TIMESTAMPOID, ftDate);
|
|
mapOid(BYTEAOID, ftBlob);
|
|
mapOid(NUMERICOID, ftCurrency);// our ORM uses NUMERIC(19,4) for currency
|
|
mapOid(BOOLOID, ftInt64);
|
|
mapOid(INT2OID, ftInt64);
|
|
mapOid(CASHOID, ftCurrency);
|
|
mapOid(TIMESTAMPTZOID, ftDate);
|
|
mapOid(ABSTIMEOID, ftDate);
|
|
mapOid(DATEOID, ftDate);
|
|
mapOid(TIMEOID, ftDate);
|
|
mapOid(TIMETZOID, ftDate);
|
|
mapOid(REGPROCOID, ftInt64);
|
|
mapOid(OIDOID, ftInt64);
|
|
mapOid(FLOAT4OID, ftDouble);
|
|
end; // any unregistered OID will be handled as ftUTF8
|
|
|
|
constructor TSQLDBPostgresConnectionProperties.Create(
|
|
const aServerName, aDatabaseName, aUserID, aPassword: RawUTF8);
|
|
begin
|
|
GlobalLock;
|
|
try
|
|
if PQ = nil then
|
|
GarbageCollectorFreeAndNil(PQ, TSQLDBPostgresLib.Create);
|
|
finally
|
|
GlobalUnLock;
|
|
end;
|
|
if PQ.IsThreadSafe <> 1 then
|
|
raise ESQLDBPostgres.Create('libpq should be compiled in threadsafe mode');
|
|
fDBMS := dPostgreSQL;
|
|
FillOidMapping;
|
|
inherited Create(aServerName, aDatabaseName, aUserID, aPassWord);
|
|
// JSONDecodedPrepareToSQL will detect cPostgreBulkArray and set
|
|
// DecodedFieldTypesToUnnest -> fast bulk insert/delete/update
|
|
fBatchSendingAbilities := [cCreate, cDelete, cUpdate, cPostgreBulkArray];
|
|
// disable MultiInsert SQL and rely on cPostgreBulkArray process for cCreate
|
|
fOnBatchInsert := nil; // see TSQLRestStorageExternal.InternalBatchStop
|
|
end;
|
|
|
|
function TSQLDBPostgresConnectionProperties.NewConnection: TSQLDBConnection;
|
|
begin
|
|
result := TSQLDBPostgresConnection.Create(self);
|
|
end;
|
|
|
|
function TSQLDBPostgresConnectionProperties.Oid2FieldType(cOID: cardinal): TSQLDBFieldType;
|
|
var
|
|
i: PtrInt;
|
|
begin
|
|
if cOID <= 65535 then
|
|
begin
|
|
i := WordScanIndex(pointer(fOids), fOidsCount, cOID);
|
|
if i >= 0 then
|
|
result := fOidsFieldTypes[i]
|
|
else
|
|
result := ftUTF8;
|
|
end
|
|
else
|
|
result := ftUTF8;
|
|
end;
|
|
|
|
procedure TSQLDBPostgresConnectionProperties.MapOid(cOid: cardinal;
|
|
fieldType: TSQLDBFieldType);
|
|
var
|
|
i: PtrInt;
|
|
begin
|
|
if cOID > 65535 then
|
|
raise ESQLDBPostgres.CreateUTF8('Out of range %.MapOid(%)', [self, cOID]);
|
|
i := WordScanIndex(pointer(fOids), fOidsCount, cOID);
|
|
if i < 0 then
|
|
begin
|
|
i := FOidsCount;
|
|
inc(FOidsCount);
|
|
if i = length(FOids) then
|
|
begin
|
|
SetLength(fOids, i + 32);
|
|
SetLength(fOidsFieldTypes, i + 32);
|
|
end;
|
|
fOids[i] := cOid;
|
|
end;
|
|
fOidsFieldTypes[i] := fieldType // set or replace
|
|
end;
|
|
|
|
procedure TSQLDBPostgresStatement.BindColumns;
|
|
var
|
|
nCols, c: integer;
|
|
cName: RawUTF8;
|
|
begin
|
|
fColumn.Clear;
|
|
fColumn.ReHash;
|
|
nCols := PQ.nfields(fRes);
|
|
fColumn.Capacity := nCols;
|
|
for c := 0 to nCols - 1 do
|
|
begin
|
|
cName := PQ.fname(fRes, c);
|
|
with PSQLDBColumnProperty(fColumn.AddAndMakeUniqueName(cName))^ do
|
|
begin
|
|
ColumnAttr := PQ.ftype(fRes, c);
|
|
ColumnType := TSQLDBPostgresConnectionProperties(Connection.
|
|
Properties).Oid2FieldType(ColumnAttr);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TSQLDBPostgresStatement.CheckColAndRowset(const Col: integer);
|
|
begin
|
|
CheckCol(Col);
|
|
if (fRes = nil) or (fResStatus <> PGRES_TUPLES_OK) then
|
|
raise ESQLDBPostgres.CreateUTF8('%.Execute not called before Column*', [self]);
|
|
end;
|
|
|
|
destructor TSQLDBPostgresStatement.Destroy;
|
|
begin
|
|
try
|
|
Reset; // close result if any
|
|
finally
|
|
inherited;
|
|
end;
|
|
end;
|
|
|
|
// see https://www.postgresql.org/docs/9.3/libpq-exec.html
|
|
|
|
procedure TSQLDBPostgresStatement.Prepare(const aSQL: RawUTF8; ExpectResults: boolean);
|
|
begin
|
|
SQLLogBegin(sllDB);
|
|
if aSQL = '' then
|
|
raise ESQLDBPostgres.CreateUTF8('%.Prepare: empty statement', [self]);
|
|
inherited Prepare(aSQL, ExpectResults); // will strip last ;
|
|
fPreparedParamsCount := ReplaceParamsByNumbers(fSQL, fSQLPrepared, '$');
|
|
if (fPreparedParamsCount > 0) and (IdemPCharArray(pointer(fSQLPrepared),
|
|
['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'VALUES']) >= 0) then
|
|
begin // preparable
|
|
fCacheIndex := TSQLDBPostgresConnection(fConnection).PrepareCached(
|
|
fSQLPrepared, fPreparedParamsCount, fPreparedStmtName);
|
|
SQLLogEnd(' name=% cache=%', [fPreparedStmtName, fCacheIndex]);
|
|
end
|
|
else
|
|
SQLLogEnd;
|
|
SetLength(fPGParams, fPreparedParamsCount);
|
|
SetLength(fPGParamFormats, fPreparedParamsCount);
|
|
SetLength(fPGparamLengths, fPreparedParamsCount);
|
|
end;
|
|
|
|
procedure TSQLDBPostgresStatement.ExecutePrepared;
|
|
var
|
|
i: PtrInt;
|
|
p: PSQLDBParam;
|
|
c: TSQLDBPostgresConnection;
|
|
begin
|
|
SQLLogBegin(sllSQL);
|
|
if fSQLPrepared = '' then
|
|
raise ESQLDBPostgres.CreateUTF8('%.ExecutePrepared: Statement not prepared', [self]);
|
|
if fParamCount <> fPreparedParamsCount then
|
|
raise ESQLDBPostgres.CreateUTF8('%.ExecutePrepared: Query expects % parameters ' +
|
|
'but % bound', [self, fPreparedParamsCount, fParamCount]);
|
|
inherited ExecutePrepared;
|
|
for i := 0 to fParamCount - 1 do // set parameters as expected by PostgreSQL
|
|
begin
|
|
// mark parameter as textual by default, with no blob len
|
|
fPGParamFormats[i] := 0;
|
|
fPGparamLengths[i] := 0;
|
|
// convert parameter value as text stored in p^.VData
|
|
p := @fParams[i];
|
|
if p^.VArray <> nil then
|
|
begin
|
|
if not (p^.VType in [ftInt64, ftDouble, ftCurrency, ftDate, ftUTF8]) then
|
|
raise ESQLDBPostgres.CreateUTF8('%.ExecutePrepared: Invalid array type % ' +
|
|
'on bound parameter #%', [Self, ToText(p^.VType)^, i]);
|
|
p^.VData := BoundArrayToJSONArray(p^.VArray);
|
|
end
|
|
else
|
|
begin
|
|
case p^.VType of
|
|
ftNull:
|
|
p^.VData := '';
|
|
ftInt64:
|
|
// use SwapEndian + binary ?
|
|
Int64ToUtf8(p^.VInt64, RawUTF8(p^.VData));
|
|
ftCurrency:
|
|
Curr64ToStr(p^.VInt64, RawUTF8(p^.VData));
|
|
ftDouble:
|
|
DoubleToStr(PDouble(@p^.VInt64)^, RawUTF8(p^.VData));
|
|
ftDate:
|
|
// Postgres expects space instead of T in ISO8601 expanded format
|
|
p^.VData := DateTimeToIso8601(
|
|
PDateTime(@p^.VInt64)^, true, ' ', fForceDateWithMS);
|
|
ftUTF8:
|
|
; // text already in p^.VData
|
|
ftBlob:
|
|
begin
|
|
fPGParamFormats[i] := 1; // binary
|
|
fPGparamLengths[i] := length(p^.VData);
|
|
end;
|
|
else
|
|
raise ESQLDBPostgres.CreateUTF8('%.ExecutePrepared: cannot bind ' +
|
|
'parameter #% of type %', [self, i, ToText(p^.VType)^]);
|
|
end;
|
|
end;
|
|
fPGParams[i] := pointer(p^.VData);
|
|
end;
|
|
c := TSQLDBPostgresConnection(Connection);
|
|
if fPreparedStmtName <> '' then
|
|
fRes := PQ.ExecPrepared(c.fPGConn, pointer(fPreparedStmtName), fPreparedParamsCount,
|
|
pointer(fPGParams), pointer(fPGparamLengths), pointer(fPGParamFormats), PGFMT_TEXT)
|
|
else if fPreparedParamsCount = 0 then
|
|
// PQexec handles multiple SQL commands
|
|
fRes := PQ.Exec(c.fPGConn, pointer(fSQLPrepared)) else
|
|
fRes := PQ.ExecParams(c.fPGConn, pointer(fSQLPrepared), fPreparedParamsCount, nil,
|
|
pointer(fPGParams), pointer(fPGparamLengths), pointer(fPGParamFormats), PGFMT_TEXT);
|
|
PQ.Check(c.fPGConn, fRes, @fRes, {forceClean=}false);
|
|
fResStatus := PQ.ResultStatus(fRes);
|
|
if fExpectResults then
|
|
begin
|
|
if fResStatus <> PGRES_TUPLES_OK then
|
|
begin // paranoid check
|
|
PQ.Clear(fRes);
|
|
fRes := nil;
|
|
raise ESQLDBPostgres.CreateUTF8('%.ExecutePrepared: result expected but ' +
|
|
'statement did not return tuples', [self]);
|
|
end;
|
|
fTotalRowsRetrieved := PQ.ntuples(fRes);
|
|
fCurrentRow := -1;
|
|
if fColumn.Count = 0 then // if columns exist then statement is already cached
|
|
BindColumns;
|
|
SQLLogEnd(' rows=%', [fTotalRowsRetrieved]);
|
|
end
|
|
else
|
|
SQLLogEnd;
|
|
end;
|
|
|
|
function TSQLDBPostgresStatement.UpdateCount: integer;
|
|
begin
|
|
result := GetCardinalDef(PQ.cmdTuples(fRes), 0);
|
|
end;
|
|
|
|
procedure TSQLDBPostgresStatement.Reset;
|
|
begin
|
|
ReleaseRows;
|
|
fResStatus := PGRES_EMPTY_QUERY;
|
|
inherited Reset;
|
|
end;
|
|
|
|
function TSQLDBPostgresStatement.Step(SeekFirst: boolean): boolean;
|
|
begin
|
|
if (fRes = nil) or (fResStatus <> PGRES_TUPLES_OK) then
|
|
raise ESQLDBPostgres.CreateUTF8('%.Execute should be called before Step', [self]);
|
|
if SeekFirst then
|
|
fCurrentRow := -1;
|
|
result := fCurrentRow + 1 < fTotalRowsRetrieved;
|
|
if not result then
|
|
exit;
|
|
inc(fCurrentRow);
|
|
end;
|
|
|
|
procedure TSQLDBPostgresStatement.ReleaseRows;
|
|
begin
|
|
if fRes <> nil then
|
|
begin
|
|
PQ.clear(fRes);
|
|
fRes := nil;
|
|
end;
|
|
inherited ReleaseRows;
|
|
end;
|
|
|
|
function TSQLDBPostgresStatement.ColumnInt(Col: integer): int64;
|
|
begin
|
|
CheckColAndRowset(Col);
|
|
result := GetInt64(PQ.GetValue(fRes, fCurrentRow, Col));
|
|
end;
|
|
|
|
function TSQLDBPostgresStatement.ColumnNull(Col: integer): boolean;
|
|
begin
|
|
CheckColAndRowset(Col);
|
|
result := (PQ.GetIsNull(fRes, fCurrentRow, Col) = 1);
|
|
end;
|
|
|
|
function TSQLDBPostgresStatement.ColumnDouble(Col: integer): double;
|
|
begin
|
|
CheckColAndRowset(Col);
|
|
result := GetExtended(PQ.GetValue(fRes, fCurrentRow, Col));
|
|
end;
|
|
|
|
function TSQLDBPostgresStatement.ColumnDateTime(Col: integer): TDateTime;
|
|
begin
|
|
CheckColAndRowset(Col);
|
|
Iso8601ToDateTimePUTF8CharVar(PQ.GetValue(fRes, fCurrentRow, Col),
|
|
PQ.GetLength(fRes, fCurrentRow, Col), result);
|
|
end;
|
|
|
|
function TSQLDBPostgresStatement.ColumnCurrency(Col: integer): currency;
|
|
begin
|
|
CheckColAndRowset(Col);
|
|
PInt64(@result)^ := StrToCurr64(PQ.GetValue(fRes, fCurrentRow, Col));
|
|
end;
|
|
|
|
function TSQLDBPostgresStatement.ColumnUTF8(Col: integer): RawUTF8;
|
|
begin
|
|
CheckColAndRowset(Col);
|
|
PQ.GetRawUTF8(fRes, fCurrentRow, Col, result);
|
|
end;
|
|
|
|
function TSQLDBPostgresStatement.ColumnBlob(Col: integer): RawByteString;
|
|
var
|
|
P: PAnsiChar;
|
|
begin // PGFMT_TEXT was used -> need to convert into binary
|
|
CheckColAndRowset(Col);
|
|
P := pointer(PQ.GetValue(fRes, fCurrentRow, Col));
|
|
SetString(result, P, BlobInPlaceDecode(P, PQ.GetLength(fRes, fCurrentRow, col)));
|
|
end;
|
|
|
|
procedure TSQLDBPostgresStatement.ColumnsToJSON(WR: TJSONWriter);
|
|
var
|
|
col: integer;
|
|
P: pointer;
|
|
begin
|
|
if (fRes = nil) or (fResStatus <> PGRES_TUPLES_OK) or (fCurrentRow < 0) then
|
|
raise ESQLDBPostgres.CreateUTF8('%.ColumnToJSON unexpected', [self]);
|
|
if WR.Expand then
|
|
WR.Add('{');
|
|
for col := 0 to fColumnCount - 1 do
|
|
with fColumns[col] do
|
|
begin
|
|
if WR.Expand then
|
|
WR.AddFieldName(ColumnName); // add '"ColumnName":'
|
|
if PQ.GetIsNull(fRes, fCurrentRow, col) = 1 then
|
|
WR.AddShort('null')
|
|
else
|
|
begin
|
|
P := PQ.GetValue(fRes, fCurrentRow, col);
|
|
case ColumnType of
|
|
ftNull:
|
|
WR.AddShort('null');
|
|
ftInt64, ftDouble, ftCurrency:
|
|
WR.AddNoJSONEscape(P, PQ.GetLength(fRes, fCurrentRow, col));
|
|
ftUTF8:
|
|
if (ColumnAttr = JSONOID) or (ColumnAttr = JSONBOID) then
|
|
WR.AddNoJSONEscape(P, PQ.GetLength(fRes, fCurrentRow, col))
|
|
else
|
|
begin
|
|
WR.Add('"');
|
|
WR.AddJSONEscape(P);
|
|
WR.Add('"');
|
|
end;
|
|
ftDate:
|
|
begin
|
|
WR.Add('"');
|
|
if (PQ.GetLength(fRes, fCurrentRow, col) > 10) and (PAnsiChar(P)[10] = ' ') then
|
|
PAnsiChar(P)[10] := 'T'; // ensure strict ISO-8601 encoding
|
|
WR.AddJSONEscape(P);
|
|
WR.Add('"');
|
|
end;
|
|
ftBlob:
|
|
if fForceBlobAsNull then
|
|
WR.AddShort('null')
|
|
else
|
|
WR.WrBase64(P, BlobInPlaceDecode(P,
|
|
PQ.GetLength(fRes, fCurrentRow, col)), {withmagic=}true);
|
|
else
|
|
raise ESQLDBPostgres.CreateUTF8('%.ColumnsToJSON: %?', [self, ToText(ColumnType)^]);
|
|
end;
|
|
end;
|
|
WR.Add(',');
|
|
end;
|
|
WR.CancelLastComma; // cancel last ','
|
|
if WR.Expand then
|
|
WR.Add('}');
|
|
end;
|
|
|
|
|
|
initialization
|
|
TSQLDBPostgresConnectionProperties.RegisterClassNameForDefinition;
|
|
end.
|