Skip to content
Permalink
master
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
/// 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.