|
|
1.34 misha 1: /** @file
2: Parser PgSQL driver.
3:
4: Copyright(c) 2001, 2003 ArtLebedev Group (http://www.artlebedev.com)
5:
6: Author: Alexandr Petrosian <paf@design.ru> (http://paf.design.ru)
7:
8: 2007.10.25 using PgSQL 8.1.5
9: */
1.35 ! moko 10: static const char *RCSId="$Id: parser3pgsql.C,v 1.34 2008-12-25 02:33:56 misha Exp $";
1.34 misha 11:
12: #include "config_includes.h"
13:
14: #include "pa_sql_driver.h"
15:
16: #include <libpq-fe.h>
17: #include <libpq/libpq-fs.h>
18:
19: // from catalog/pg_type.h
20: #define BOOLOID 16
21: #define INT8OID 20
22: #define INT2OID 21
23: #define INT4OID 23
24: #define OIDOID 26
25: #define FLOAT4OID 700
26: #define FLOAT8OID 701
27: #define DATEOID 1082
28: #define TIMEOID 1083
29: #define TIMESTAMPOID 1114
30: #define TIMESTAMPTZOID 1184
31: #define TIMETZOID 1266
32: #define NUMERICOID 1700
33:
34: // LO_BUFSIZE from interfaces\libpq\fe-lobj.c = 8192 (0x2000)
35: // actually writing chunks of that size failed, reduced it twice
36: #define LO_BUFSIZE 0x1000
37:
38:
39: #include "ltdl.h"
40:
41: #define MAX_COLS 500
42:
43: #define MAX_STRING 0x400
44: #define MAX_NUMBER 20
45:
46: #if _MSC_VER
47: # define snprintf _snprintf
48: # define strcasecmp _stricmp
49: #endif
50:
51: #ifndef max
52: inline int max(int a,int b){ return a>b?a:b; }
53: inline int min(int a,int b){ return a<b?a:b; }
54: #endif
55:
56: static char *lsplit(char *string, char delim){
57: if(string){
58: if(char *v=strchr(string, delim)){
59: *v=0;
60: return v+1;
61: }
62: }
63: return 0;
64: }
65:
66: static char *lsplit(char **string_ref, char delim){
67: char *result=*string_ref;
68: char *next=lsplit(*string_ref, delim);
69: *string_ref=next;
70: return result;
71: }
72:
73: static char* rsplit(char* string, char delim){
74: if(string){
75: if(char* v=strrchr(string, delim)){
76: *v=0;
77: return v+1;
78: }
79: }
80: return NULL;
81: }
82:
83: static void toupper_str(char *out, const char *in, size_t size){
84: while(size--)
85: *out++=(char)toupper(*in++);
86: }
87:
88: struct Connection {
89: SQL_Driver_services* services;
90:
91: PGconn *conn;
92: const char* client_charset;
93: bool autocommit;
94: bool without_default_transactions;
95: };
96:
97: /**
98: PgSQL server driver
99: */
100: class PgSQL_Driver : public SQL_Driver {
101: public:
102:
103: PgSQL_Driver() : SQL_Driver() {
104: }
105:
106: /// get api version
107: int api_version(){ return SQL_DRIVER_API_VERSION; }
108:
109: /// initialize driver by loading sql dynamic link library
110: const char *initialize(char *dlopen_file_spec){
111: return dlopen_file_spec?
112: dlink(dlopen_file_spec):"client library column is empty";
113: }
114:
115: #define throwPQerror connection.services->_throw(PQerrorMessage(connection.conn))
116: #define PQclear_throw(msg) { \
117: PQclear(res); \
118: connection.services->_throw(msg); \
119: }
120: #define PQclear_throwPQerror PQclear_throw(PQerrorMessage(connection.conn))
121:
122: /** connect
123: @param url
124: format: @b user:pass@host[:port]|[local]/database?
125: ClientCharset=charset& // transcode by parser
126: charset=value& // transcode by server with 'SET CLIENT_ENCODING=value'
127: datestyle=value& // 'SET DATESTYLE=value' available values are: ISO|SQL|Postgres|European|US|German [default=ISO]
128: autocommit=1& // each transaction is commited automatically (default)
129: WithoutDefaultTransaction=0 // 1 -- disable any BEGIN TRAN/COMMIT/ROLLBACK [can NOT be used with autocommit option]
130: */
131: void connect(
132: char* url,
133: SQL_Driver_services& services,
134: void** connection_ref ///< output: Connection*
135: ){
136: char* user=url;
137: char* host=rsplit(user, '@');
138: char* db=lsplit(host, '/');
139: char* pwd=lsplit(user, ':');
140: char* port=lsplit(host, ':');
141:
142: char *options=lsplit(db, '?');
143:
144: char* charset=0;
145: char* datestyle=0;
146:
147: Connection& connection=*(Connection *)services.malloc(sizeof(Connection));
148: *connection_ref=&connection;
149: connection.services=&services;
150: connection.client_charset=0;
151: connection.autocommit=true;
152: connection.without_default_transactions=false;
153:
154: connection.conn=PQsetdbLogin(
155: (host&&strcasecmp(host, "local")==0)?NULL/* local Unix domain socket */:host, port,
156: NULL, NULL, db, user, pwd);
157:
158: if(!connection.conn)
159: services._throw("PQsetdbLogin failed");
160:
161: if(PQstatus(connection.conn)!=CONNECTION_OK)
162: throwPQerror;
163:
164: while(options){
165: if(char *key=lsplit(&options, '&')){
166: if(*key){
167: if(char *value=lsplit(key, '=')){
168: if(strcmp(key, "ClientCharset")==0){
169: toupper_str(value, value, strlen(value));
170: connection.client_charset=value;
171: } else if(strcasecmp(key, "charset")==0){
172: charset=value;
173: } else if(strcasecmp(key, "datestyle")==0){
174: datestyle=value;
175: } else if(strcasecmp(key, "autocommit")==0){
176: if(connection.without_default_transactions)
177: services._throw("options WithoutDefaultTransaction and autocommit can't be used together");
178: if(atoi(value)==0)
179: connection.autocommit=false;
180: } else if(strcmp(key, "WithoutDefaultTransaction")==0){
181: if(!connection.autocommit)
182: services._throw("options WithoutDefaultTransaction and autocommit can't be used together");
183: if(atoi(value)==1){
184: connection.without_default_transactions=true;
185: connection.autocommit=false;
186: }
187: } else
188: services._throw("unknown connect option" /*key*/);
189: } else
190: services._throw("connect option without =value" /*key*/);
191: }
192: }
193: }
194:
195: if(charset){
196: char statement[MAX_STRING]="SET CLIENT_ENCODING=";
197: strncat(statement, charset, MAX_STRING);
198:
199: _execute_cmd(connection, statement);
200: }
201:
202: if(datestyle){
203: char statement[MAX_STRING]="SET DATESTYLE=";
204: strncat(statement, datestyle, MAX_STRING);
205:
206: _execute_cmd(connection, statement);
207: }
208:
209: _transaction_begin(connection);
210: }
211:
212: void disconnect(void *aconnection){
213: Connection& connection=*static_cast<Connection*>(aconnection);
214: PQfinish(connection.conn);
215: connection.conn=0;
216: }
217:
218: void commit(void *aconnection){
219: Connection& connection=*static_cast<Connection*>(aconnection);
220: _transaction_commit(connection);
221: _transaction_begin(connection);
222: }
223:
224: void rollback(void *aconnection){
225: Connection& connection=*static_cast<Connection*>(aconnection);
226: _transaction_rollback(connection);
227: _transaction_begin(connection);
228: }
229:
230: bool ping(void *aconnection) {
231: Connection& connection=*static_cast<Connection*>(aconnection);
232: return PQstatus(connection.conn)==CONNECTION_OK;
233: }
234:
1.35 ! moko 235: // charset here is services.request_charset(), not connection.client_charset
! 236: // thus we can't use the sql server quoting support
! 237: const char* quote(void *aconnection, const char *str, unsigned int length)
! 238: {
! 239: const char* from;
! 240: const char* from_end=str+length;
! 241:
! 242: size_t quoted=0;
! 243:
! 244: for(from=str; from<from_end; from++){
! 245: switch (*from) {
! 246: case '\'':
! 247: case '\\':
! 248: quoted++;
! 249: }
! 250: }
! 251:
! 252: if(!quoted)
! 253: return str;
! 254:
1.34 misha 255: Connection& connection=*static_cast<Connection*>(aconnection);
1.35 ! moko 256: char *result=(char*)connection.services->malloc_atomic(length + quoted + 1);
! 257: char *to = result;
1.34 misha 258:
1.35 ! moko 259: for(from=str; from<from_end; from++){
! 260: switch (*from) {
! 261: case '\'': // "'" -> "''"
! 262: *to++='\'';
! 263: break;
! 264: case '\\': // "\" -> "\\"
! 265: *to++='\\';
! 266: break;
! 267: }
! 268: *to++=*from;
! 269: }
! 270:
! 271: *to=0;
1.34 misha 272: return result;
273: }
1.35 ! moko 274:
1.34 misha 275: void query(void *aconnection,
276: const char *astatement,
277: size_t placeholders_count, Placeholder* placeholders,
278: unsigned long offset, unsigned long limit,
279: SQL_Driver_query_event_handlers& handlers
280: ){
281: Connection& connection=*static_cast<Connection*>(aconnection);
282: SQL_Driver_services& services=*connection.services;
283: PGconn *conn=connection.conn;
284:
285: const char* client_charset=connection.client_charset;
286: const char* request_charset=services.request_charset();
287: bool transcode_needed=client_charset && strcmp(client_charset, request_charset)!=0;
288:
289: const char** paramValues;
290: if(placeholders_count>0){
291: int binds_size=sizeof(char)*placeholders_count;
292: paramValues = static_cast<const char**>(services.malloc_atomic(binds_size));
293: _bind_parameters(placeholders_count, placeholders, paramValues, connection, transcode_needed);
294: }
295:
296: if(transcode_needed){
297: // transcode query from $request:charset to ?ClientCharset
298: size_t length=strlen(astatement);
299: services.transcode(astatement, length,
300: astatement, length,
301: request_charset,
302: client_charset);
303: }
304:
305: const char *statement=_preprocess_statement(connection, astatement, offset, limit);
306: // error after prepare?
307:
308: PGresult *res;
309: if(placeholders_count>0){
310: res=PQexecParams(conn, statement, placeholders_count, NULL, paramValues, NULL, NULL, 0);
311: } else {
312: res=PQexec(conn, statement);
313: }
314: if(!res)
315: throwPQerror;
316:
317: bool failed=false;
318: SQL_Error sql_error;
319:
320: switch(PQresultStatus(res)) {
321: case PGRES_EMPTY_QUERY:
322: PQclear_throw("no query");
323: break;
324: case PGRES_COMMAND_OK: // empty result: insert|delete|update|...
325: PQclear(res);
326: if(connection.autocommit)
327: commit(aconnection);
328: return;
329: case PGRES_TUPLES_OK:
330: break;
331: default:
332: PQclear_throwPQerror;
333: break;
334: }
335:
336: #define CHECK(afailed) \
337: if(afailed) { \
338: failed=true; \
339: goto cleanup; \
340: }
341:
342: int column_count=PQnfields(res);
343: if(!column_count)
344: PQclear_throw("result contains no columns");
345:
346: if(column_count>MAX_COLS)
347: column_count=MAX_COLS;
348:
349: unsigned int column_types[MAX_COLS];
350: bool transcode_column[MAX_COLS];
351:
352: for(int i=0; i<column_count; i++){
353: column_types[i]=PQftype(res, i);
354: switch(column_types[i]){
355: case BOOLOID:
356: case INT8OID:
357: case INT2OID:
358: case INT4OID:
359: case FLOAT4OID:
360: case FLOAT8OID:
361: case DATEOID:
362: case TIMEOID:
363: case TIMESTAMPOID:
364: case TIMESTAMPTZOID:
365: case TIMETZOID:
366: case NUMERICOID:
367: transcode_column[i]=false;
368: break;
369: default:
370: transcode_column[i]=transcode_needed;
371: break;
372: }
373: char *name=PQfname(res, i);
374: size_t length=strlen(name);
375: char* strm=(char*)services.malloc(length+1);
376: memcpy(strm, name, length+1);
377: const char* str=strm;
378:
379: if(transcode_needed) // transcode column name from ?ClientCharset to $request:charset
380: services.transcode(str, length,
381: str, length,
382: client_charset,
383: request_charset);
384:
385: CHECK(handlers.add_column(sql_error, str, length));
386: }
387:
388: CHECK(handlers.before_rows(sql_error));
389:
390: if(unsigned long row_count=(unsigned long)PQntuples(res))
391: for(unsigned long r=0; r<row_count; r++) {
392: CHECK(handlers.add_row(sql_error));
393: for(int i=0; i<column_count; i++){
394: const char *cell=PQgetvalue(res, r, i);
395: size_t length;
396: const char* str;
397:
398: switch(column_types[i]){
399: case OIDOID:
400: {
401: char *error_pos=0;
402: Oid oid=cell?atoi(cell):0;
403: int fd=lo_open(conn, oid, INV_READ);
404: if(fd>=0){
405: // seek to end
406: if(lo_lseek(conn, fd, 0, SEEK_END)<0)
407: PQclear_throwPQerror;
408: // get length
409: int size_tell=lo_tell(conn, fd);
410: if(size_tell<0)
411: PQclear_throwPQerror;
412: // seek to begin
413: if(lo_lseek(conn, fd, 0, SEEK_SET)<0)
414: PQclear_throwPQerror;
415: length=(size_t)size_tell;
416: if(length){
417: // read
418: char* strm=(char*)services.malloc(length+1);
419: if(!lo_read_ex(conn, fd, strm, size_tell))
420: PQclear_throw("lo_read can not read all bytes of object");
421: strm[length]=0;
422: str=strm;
423: } else
424: str=0;
425: if(lo_close(conn, fd)<0)
426: PQclear_throwPQerror;
427: } else
428: PQclear_throwPQerror;
429: }
430: default:
431: // normal column, read it normally
432: length=(size_t)PQgetlength(res, r, i);
433: if(length){
434: char* strm=(char*)services.malloc(length+1);
435: memcpy(strm, cell, length+1);
436: str=strm;
437: } else
438: str=0;
439: }
440:
441: if(str && length && transcode_column[i]){
442: // transcode cell value from ?ClientCharset to $request:charset
443: services.transcode(str, length,
444: str, length,
445: client_charset,
446: request_charset);
447: }
448:
449: CHECK(handlers.add_row_cell(sql_error, str, length));
450: }
451: }
452: cleanup:
453: PQclear(res);
454: if(failed)
455: services._throw(sql_error);
456:
457: if(connection.autocommit)
458: commit(aconnection);
459: }
460:
461: private:
462: void _bind_parameters(
463: size_t placeholders_count,
464: Placeholder* placeholders,
465: const char** paramValues,
466: Connection& connection,
467: bool transcode_needed
468: ){
469: for(size_t i=0; i<placeholders_count; i++){
470: Placeholder& ph=placeholders[i];
471: if(transcode_needed){
472: size_t name_length;
473: connection.services->transcode(ph.name, strlen(ph.name),
474: ph.name, name_length,
475: connection.services->request_charset(),
476: connection.client_charset);
477:
478: if(ph.value) {
479: size_t value_length;
480: connection.services->transcode(ph.value, strlen(ph.value),
481: ph.value, value_length,
482: connection.services->request_charset(),
483: connection.client_charset);
484: }
485: }
486: int name_numner=atoi(ph.name);
487: if(name_numner <= 0 || (size_t)name_numner > placeholders_count)
488: connection.services->_throw("bad bind parameter key");
489:
490: paramValues[name_numner-1]=ph.value;
491: }
492: }
493:
494:
495: void _transaction_begin(Connection& connection){
496: _execute_transactions_cmd(connection, "BEGIN");
497: }
498:
499: void _transaction_commit(Connection& connection){
500: _execute_transactions_cmd(connection, "COMMIT");
501: }
502:
503: void _transaction_rollback(Connection& connection){
504: _execute_transactions_cmd(connection, "ROLLBACK");
505: }
506:
507: void _execute_transactions_cmd(const Connection& connection, const char *query){
508: if(!connection.without_default_transactions) // with option ?WithoutDefaultTransaction=1 user must execute BEGIN/COMMIT/ROLLBACK by himself
509: _execute_cmd(connection, query);
510: }
511:
512: // executes a query and throw away the result.
513: void _execute_cmd(const Connection& connection, const char *query){
514: if(PGresult *res=PQexec(connection.conn, query))
515: PQclear(res); // throw away the result [don't need but must call]
516: else
517: throwPQerror;
518: }
519:
520: const char *_preprocess_statement(
521: Connection& connection,
522: const char *astatement,
523: unsigned long offset,
524: unsigned long limit
525: ){
526: PGconn *conn=connection.conn;
527:
528: size_t statement_size=strlen(astatement);
529:
530: char *result=(char *)connection.services->malloc(statement_size
531: +MAX_NUMBER*2+15 // limit # offset #
532: +MAX_STRING // in case of short 'strings'
533: +1);
534: // offset & limit -> suffixes
535: const char *o;
536: if(offset || limit!=SQL_NO_LIMIT){
537: char *cur=result;
538: memcpy(cur, astatement, statement_size); cur+=statement_size;
539: if(limit!=SQL_NO_LIMIT)
540: cur+=snprintf(cur, 7+MAX_NUMBER, " limit %u", limit);
541: if(offset)
542: cur+=snprintf(cur, 8+MAX_NUMBER, " offset %u", offset);
543: o=result;
544: } else
545: o=astatement;
546:
547: // /**xxx**/'literal' -> oid
548: char *n=result;
549: while(*o) {
550: if(
551: o[0]=='/' &&
552: o[1]=='*' &&
553: o[2]=='*') { // name start
554: const char* saved_o=o;
555: o+=3;
556: while(*o)
557: if(
558: o[0]=='*' &&
559: o[1]=='*' &&
560: o[2]=='/' &&
561: o[3]=='\'') { // name end
562: saved_o=0; // found, marking that
563: o+=4;
564: Oid oid=lo_creat(conn, INV_READ|INV_WRITE);
565: if(oid==InvalidOid)
566: throwPQerror;
567: int fd=lo_open(conn, oid, INV_WRITE);
568: if(fd>=0) {
569: const char *start=o;
570: bool escaped=false;
571: while(*o && !(o[0]=='\'' && o[1]!='\'' && !escaped)) {
572: escaped=*o=='\\' || (o[0]=='\'' && o[1]=='\'');
573: if(escaped) {
574: // write pending, skip "\" or "'"
575: if(!lo_write_ex(conn, fd, start, o-start))
576: connection.services->_throw("lo_write could not write all bytes of object (1)");
577: start=++o;
578: } else
579: o++;
580: }
581: if(!lo_write_ex(conn, fd, start, o-start))
582: connection.services->_throw("lo_write can not write all bytes of object (2)");
583: if(lo_close(conn, fd)<0)
584: throwPQerror;
585: } else
586: throwPQerror;
587: if(*o)
588: o++; // skip "'"
589:
590: n+=snprintf(n, MAX_NUMBER, "%u", oid);
591: break;
592: } else
593: o++; // /**skip**/'xxx'
594: if(saved_o) {
595: o=saved_o;
596: *n++=*o++;
597: }
598: } else
599: *n++=*o++;
600: }
601: *n=0;
602:
603: return result;
604: }
605:
606: private: // lo_read/write exchancements
607:
608: bool lo_read_ex(PGconn *conn, int fd, const/*paf*/ char *buf, size_t len) {
609: return lo_rw_method (conn, fd, buf, len, lo_read);
610: }
611:
612: bool lo_write_ex(PGconn *conn, int fd, const/*paf*/ char *buf, size_t len) {
613: return lo_rw_method (conn, fd, buf, len, lo_write);
614: }
615:
616: bool lo_rw_method(PGconn *conn, int fd, const/*paf*/ char *buf, size_t len, int (*lo_func)(PGconn *conn, int fd, const/*paf*/ char *buf, size_t len)) {
617: int size_op;
618: while(len && (size_op=lo_func(conn, fd, buf, min(LO_BUFSIZE, len)))>0) {
619: buf+=size_op;
620: len-=size_op;
621: }
622: return len==0;
623: }
624:
625: private: // conn client library funcs
626:
627: typedef PGconn* (*t_PQsetdbLogin)(
628: const char *pghost,
629: const char *pgport,
630: const char *pgoptions,
631: const char *pgtty,
632: const char *dbName,
633: const char *login,
634: const char *pwd); t_PQsetdbLogin PQsetdbLogin;
635: typedef void (*t_PQfinish)(PGconn *conn); t_PQfinish PQfinish;
636: typedef char *(*t_PQerrorMessage)(const PGconn* conn); t_PQerrorMessage PQerrorMessage;
637: typedef ConnStatusType (*t_PQstatus)(const PGconn *conn); t_PQstatus PQstatus;
638: typedef PGresult *(*t_PQexec)(PGconn *conn,
639: const char *query); t_PQexec PQexec;
640: typedef PGresult *(*t_PQexecParams)(
641: PGconn *conn,
642: const char *query,
643: int nParams,
644: const Oid *paramTypes,
645: const char * const *paramValues,
646: const int *paramLengths,
647: const int *paramFormats,
648: int resultFormat); t_PQexecParams PQexecParams;
649:
650: typedef ExecStatusType (*t_PQresultStatus)(const PGresult *res); t_PQresultStatus PQresultStatus;
651: typedef int (*t_PQgetlength)(const PGresult *res,
652: int tup_num,
653: int field_num); t_PQgetlength PQgetlength;
654: typedef char* (*t_PQgetvalue)(const PGresult *res,
655: int tup_num,
656: int field_num); t_PQgetvalue PQgetvalue;
657: typedef int (*t_PQntuples)(const PGresult *res); t_PQntuples PQntuples;
658: typedef char *(*t_PQfname)(const PGresult *res,
659: int field_index); t_PQfname PQfname;
660: typedef int (*t_PQnfields)(const PGresult *res); t_PQnfields PQnfields;
661: typedef void (*t_PQclear)(PGresult *res); t_PQclear PQclear;
662:
663: typedef Oid (*t_PQftype)(const PGresult *res, int field_num); t_PQftype PQftype;
664:
665: typedef size_t (*t_PQescapeStringConn)(PGconn *conn,
666: char *to, const char *from, size_t length,
667: int *error); t_PQescapeStringConn PQescapeStringConn;
668:
669: typedef int (*t_lo_open)(PGconn *conn, Oid lobjId, int mode); t_lo_open lo_open;
670: typedef int (*t_lo_close)(PGconn *conn, int fd); t_lo_close lo_close;
671: typedef int (*t_lo_read)(PGconn *conn, int fd, const/*paf*/ char *buf, size_t len); t_lo_read lo_read;
672: typedef int (*t_lo_write)(PGconn *conn, int fd, const/*paf*/ char *buf, size_t len); t_lo_write lo_write;
673: typedef int (*t_lo_lseek)(PGconn *conn, int fd, int offset, int whence); t_lo_lseek lo_lseek;
674: typedef Oid (*t_lo_creat)(PGconn *conn, int mode); t_lo_creat lo_creat;
675: typedef int (*t_lo_tell)(PGconn *conn, int fd); t_lo_tell lo_tell;
676: typedef int (*t_lo_unlink)(PGconn *conn, Oid lobjId); t_lo_unlink lo_unlink;
677: typedef Oid (*t_lo_import)(PGconn *conn, const char *filename); t_lo_import lo_import;
678: typedef int (*t_lo_export)(PGconn *conn, Oid lobjId, const char *filename); t_lo_export lo_export;
679:
680: private: // conn client library funcs linking
681:
682: const char *dlink(const char *dlopen_file_spec) {
683: if(lt_dlinit())
684: return lt_dlerror();
685: lt_dlhandle handle=lt_dlopen(dlopen_file_spec);
686: if(!handle)
687: return "can not open the dynamic link module";
688:
689: #define DSLINK(name, action) \
690: name=(t_##name)lt_dlsym(handle, #name); \
691: if(!name) \
692: action;
693:
694: #define DLINK(name) DSLINK(name, return "function " #name " was not found")
695:
696: DLINK(PQsetdbLogin);
697: DLINK(PQerrorMessage);
698: DLINK(PQstatus);
699: DLINK(PQfinish);
700: DLINK(PQgetvalue);
701: DLINK(PQgetlength);
702: DLINK(PQntuples);
703: DLINK(PQfname);
704: DLINK(PQnfields);
705: DLINK(PQclear);
706: DLINK(PQresultStatus);
707: DLINK(PQexec);
708: DLINK(PQexecParams);
709: DLINK(PQftype);
710: DLINK(PQescapeStringConn);
711: DLINK(lo_open); DLINK(lo_close);
712: DLINK(lo_read); DLINK(lo_write);
713: DLINK(lo_lseek); DLINK(lo_creat);
714: DLINK(lo_tell); DLINK(lo_unlink);
715: DLINK(lo_import); DLINK(lo_export);
716:
717: return 0;
718: }
719: };
720:
721: extern "C" SQL_Driver *SQL_DRIVER_CREATE() {
722: return new PgSQL_Driver();
723: }