1 Star 0 Fork 0

大量/mORMot

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
SynDBPostgres.pas 33.86 KB
一键复制 编辑 原始数据 按行查看 历史
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008
/// 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.
Loading...
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/wzdlsoft/mORMot.git
[email protected]:wzdlsoft/mORMot.git
wzdlsoft
mORMot
mORMot
master

搜索帮助