libs-sqlclient/Postgres.m
Richard Frith-MacDonald b00d51e017 mkae version 1.5.3
git-svn-id: svn+ssh://svn.gna.org/svn/gnustep/libs/sqlclient/trunk@33914 72102866-910b-0410-8b05-ffd578937521
2011-09-30 17:21:04 +00:00

866 lines
18 KiB
Objective-C

/* -*-objc-*- */
/** Implementation of SQLClientPostgres for GNUStep
Copyright (C) 2004 Free Software Foundation, Inc.
Written by: Richard Frith-Macdonald <rfm@gnu.org>
Date: April 2004
This file is part of the SQLClient Library.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free
Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111 USA.
$Date$ $Revision$
*/
#import <Foundation/NSString.h>
#import <Foundation/NSData.h>
#import <Foundation/NSDate.h>
#import <Foundation/NSCalendarDate.h>
#import <Foundation/NSCharacterSet.h>
#import <Foundation/NSException.h>
#import <Foundation/NSProcessInfo.h>
#import <Foundation/NSNotification.h>
#import <Foundation/NSUserDefaults.h>
#import <Foundation/NSMapTable.h>
#import <Foundation/NSLock.h>
#import <Foundation/NSNull.h>
#import <Foundation/NSValue.h>
#import <Foundation/NSAutoreleasePool.h>
#include "config.h"
#define SQLCLIENT_PRIVATE @public
#include "SQLClient.h"
#include <libpq-fe.h>
@interface SQLClientPostgres : SQLClient
@end
@interface SQLClientPostgres(Embedded)
- (NSData*) dataFromBLOB: (const char *)blob;
- (NSDate*) dbToDateFromBuffer: (char*)b length: (int)l;
@end
typedef struct {
PGconn *_connection;
BOOL _escapeStrings; /* Can we use E'...' syntax? */
} ConnectionInfo;
#define cInfo ((ConnectionInfo*)(self->extra))
#define connection (cInfo->_connection)
#define escapeStrings (cInfo->_escapeStrings)
static NSDate *future = nil;
static NSNull *null = nil;
@implementation SQLClientPostgres
+ (void) initialize
{
if (future == nil)
{
future = [NSCalendarDate dateWithString: @"9999-01-01 00:00:00 +0000"
calendarFormat: @"%Y-%m-%d %H:%M:%S %z"
locale: nil];
[future retain];
null = [NSNull null];
[null retain];
}
}
static NSString*
connectQuote(NSString *str)
{
NSMutableString *m;
m = [str mutableCopy];
[m replaceString: @"\\" withString: @"\\\\"];
[m replaceString: @"'" withString: @"\\'"];
[m replaceCharactersInRange: NSMakeRange(0, 0) withString: @"'"];
[m appendString: @"'"];
return [m autorelease];
}
- (BOOL) backendConnect
{
if (extra == 0)
{
extra = NSZoneMalloc(NSDefaultMallocZone(), sizeof(ConnectionInfo));
memset(extra, '\0', sizeof(ConnectionInfo));
}
if (connection == 0)
{
connected = NO;
if ([self database] != nil)
{
NSString *host = nil;
NSString *port = nil;
NSString *dbase = [self database];
NSString *str;
NSRange r;
NSMutableString *m;
[[self class] purgeConnections: nil];
r = [dbase rangeOfString: @"@"];
if (r.length > 0)
{
host = [dbase substringFromIndex: NSMaxRange(r)];
dbase = [dbase substringToIndex: r.location];
r = [host rangeOfString: @":"];
if (r.length > 0)
{
port = [host substringFromIndex: NSMaxRange(r)];
host = [host substringToIndex: r.location];
}
}
m = [NSMutableString stringWithCapacity: 156];
[m appendString: @"dbname="];
[m appendString: connectQuote(dbase)];
str = connectQuote(host);
if (str != nil)
{
unichar c = [str characterAtIndex: 1];
if (c >= '0' && c <= '9')
{
[m appendString: @" hostaddr="]; // Numeric IP
}
else
{
[m appendString: @" host="]; // Domain name
}
[m appendString: str];
}
str = connectQuote(port);
if (str != nil)
{
[m appendString: @" port="];
[m appendString: str];
}
str = connectQuote([self user]);
if (str != nil)
{
[m appendString: @" user="];
[m appendString: str];
}
str = connectQuote([self password]);
if (str != nil)
{
[m appendString: @" password="];
[m appendString: str];
}
if ([self debugging] > 0)
{
[self debug: @"Connect to '%@' as %@", m, [self name]];
}
connection = PQconnectdb([m UTF8String]);
if (PQstatus(connection) != CONNECTION_OK)
{
[self debug: @"Error connecting to '%@' (%@) - %s",
[self name], m, PQerrorMessage(connection)];
PQfinish(connection);
connection = 0;
}
else if (PQsetClientEncoding(connection, "UTF-8") < 0)
{
[self debug: @"Error setting UTF-8 with '%@' (%@) - %s",
[self name], m, PQerrorMessage(connection)];
PQfinish(connection);
connection = 0;
}
else
{
const char *p;
p = PQparameterStatus(connection, "standard_conforming_strings");
if (p != 0)
{
escapeStrings = YES;
}
else
{
escapeStrings = NO;
}
connected = YES;
if ([self debugging] > 0)
{
[self debug: @"Connected to '%@'", [self name]];
}
}
}
else
{
[self debug:
@"Connect to '%@' with no user/password/database configured",
[self name]];
}
}
return connected;
}
- (void) backendDisconnect
{
if (extra != 0 && connection != 0)
{
NS_DURING
{
if ([self isInTransaction] == YES)
{
[self rollback];
}
if ([self debugging] > 0)
{
[self debug: @"Disconnecting client %@", [self clientName]];
}
PQfinish(connection);
connection = 0;
if ([self debugging] > 0)
{
[self debug: @"Disconnected client %@", [self clientName]];
}
}
NS_HANDLER
{
connection = 0;
[self debug: @"Error disconnecting from database (%@): %@",
[self clientName], localException];
}
NS_ENDHANDLER
connected = NO;
}
}
- (void) backendExecute: (NSArray*)info
{
NSAutoreleasePool *arp = [NSAutoreleasePool new];
PGresult *result = 0;
NSString *stmt = [info objectAtIndex: 0];
if ([stmt length] == 0)
{
[arp release];
[NSException raise: NSInternalInconsistencyException
format: @"Statement produced null string"];
}
NS_DURING
{
const char *statement;
unsigned length;
/*
* Ensure we have a working connection.
*/
if ([self connect] == NO)
{
[NSException raise: SQLException
format: @"Unable to connect to '%@' to execute statement %@",
[self name], stmt];
}
statement = (char*)[stmt UTF8String];
length = strlen(statement);
statement = [self insertBLOBs: info
intoStatement: statement
length: length
withMarker: "'?'''?'"
length: 7
giving: &length];
result = PQexec(connection, statement);
if (result == 0 || PQresultStatus(result) == PGRES_FATAL_ERROR)
{
NSString *str;
const char *cstr;
if (result == 0)
{
cstr = PQerrorMessage(connection);
}
else
{
cstr = PQresultErrorMessage(result);
}
str = [NSString stringWithUTF8String: cstr];
[self disconnect];
[NSException raise: SQLException format: @"Error executing %@: %@",
stmt, str];
}
if (PQresultStatus(result) != PGRES_COMMAND_OK)
{
[NSException raise: SQLException format: @"Error executing %@: %s",
stmt, PQresultErrorMessage(result)];
}
}
NS_HANDLER
{
NSString *n = [localException name];
if ([n isEqual: SQLConnectionException] == YES)
{
[self disconnect];
}
if (result != 0)
{
PQclear(result);
}
[localException retain];
[arp release];
[localException autorelease];
[localException raise];
}
NS_ENDHANDLER
if (result != 0)
{
PQclear(result);
}
[arp release];
}
static unsigned int trim(char *str)
{
char *start = str;
while (isspace(*str))
{
str++;
}
if (str != start)
{
strcpy(start, str);
}
str = start;
while (*str != '\0')
{
str++;
}
while (str > start && isspace(str[-1]))
{
*--str = '\0';
}
return (str - start);
}
- (NSMutableArray*) backendQuery: (NSString*)stmt
recordType: (id)rtype
listType: (id)ltype
{
NSAutoreleasePool *arp = [NSAutoreleasePool new];
PGresult *result = 0;
NSMutableArray *records = nil;
if ([stmt length] == 0)
{
[arp release];
[NSException raise: NSInternalInconsistencyException
format: @"Statement produced null string"];
}
NS_DURING
{
char *statement;
/*
* Ensure we have a working connection.
*/
if ([self connect] == NO)
{
[NSException raise: SQLException
format: @"Unable to connect to '%@' to run query %@",
[self name], stmt];
}
statement = (char*)[stmt UTF8String];
result = PQexec(connection, statement);
if (result == 0 || PQresultStatus(result) == PGRES_FATAL_ERROR)
{
NSString *str;
const char *cstr;
if (result == 0)
{
cstr = PQerrorMessage(connection);
}
else
{
cstr = PQresultErrorMessage(result);
}
str = [NSString stringWithUTF8String: cstr];
[self disconnect];
[NSException raise: SQLException format: @"Error executing %@: %@",
stmt, str];
}
if (PQresultStatus(result) == PGRES_TUPLES_OK)
{
int recordCount = PQntuples(result);
int fieldCount = PQnfields(result);
NSString *keys[fieldCount];
int types[fieldCount];
int modifiers[fieldCount];
int formats[fieldCount];
int i;
for (i = 0; i < fieldCount; i++)
{
keys[i] = [NSString stringWithUTF8String: PQfname(result, i)];
types[i] = PQftype(result, i);
modifiers[i] = PQfmod(result, i);
formats[i] = PQfformat(result, i);
}
records = [[ltype alloc] initWithCapacity: recordCount];
for (i = 0; i < recordCount; i++)
{
SQLRecord *record;
id values[fieldCount];
int j;
for (j = 0; j < fieldCount; j++)
{
id v = null;
if (PQgetisnull(result, i, j) == 0)
{
char *p = PQgetvalue(result, i, j);
int size = PQgetlength(result, i, j);
if ([self debugging] > 1)
{
[self debug: @"%@ type:%d mod:%d size: %d\n",
keys[j], types[j], modifiers[j], size];
}
if (formats[j] == 0) // Text
{
switch (types[j])
{
case 1082: // Date
case 1083: // Time
case 1114: // Timestamp without time zone.
case 1184: // Timestamp with time zone.
v = [self dbToDateFromBuffer: p
length: trim(p)];
break;
case 16: // BOOL
if (*p == 't')
{
v = @"YES";
}
else
{
v = @"NO";
}
break;
case 17: // BYTEA
v = [self dataFromBLOB: p];
break;
default:
trim(p);
v = [NSString stringWithUTF8String: p];
break;
}
}
else // Binary
{
NSLog(@"Binary data treated as NSNull "
@"in %@ type:%d mod:%d size:%d\n",
keys[j], types[j], modifiers[j], size);
}
}
values[j] = v;
}
record = [rtype newWithValues: values
keys: keys
count: fieldCount];
[records addObject: record];
[record release];
}
}
else
{
[NSException raise: SQLException format: @"Error executing %@: %s",
stmt, PQresultErrorMessage(result)];
}
}
NS_HANDLER
{
NSString *n = [localException name];
if ([n isEqual: SQLConnectionException] == YES)
{
[self disconnect];
}
if (result != 0)
{
PQclear(result);
}
[records release];
records = nil;
[localException retain];
[arp release];
[localException autorelease];
[localException raise];
}
NS_ENDHANDLER
[arp release];
if (result != 0)
{
PQclear(result);
}
return [records autorelease];
}
- (unsigned) copyEscapedBLOB: (NSData*)blob into: (void*)buf
{
const unsigned char *src = [blob bytes];
unsigned sLen = [blob length];
unsigned char *ptr = (unsigned char*)buf;
unsigned length = 0;
unsigned i;
if (YES == escapeStrings)
{
ptr[length++] = 'E';
}
ptr[length++] = '\'';
for (i = 0; i < sLen; i++)
{
unsigned char c = src[i];
if (c < 32 || c > 126 || c == '\'')
{
ptr[length] = '\\';
ptr[length+1] = '\\';
ptr[length + 4] = (c & 7) + '0';
c >>= 3;
ptr[length + 3] = (c & 7) + '0';
c >>= 3;
ptr[length + 2] = (c & 7) + '0';
length += 5;
}
else if (c == '\\')
{
ptr[length++] = '\\';
ptr[length++] = '\\';
ptr[length++] = '\\';
ptr[length++] = '\\';
}
else
{
ptr[length++] = c;
}
}
ptr[length++] = '\'';
return length;
}
- (unsigned) lengthOfEscapedBLOB: (NSData*)blob
{
unsigned int sLen = [blob length];
unsigned char *src = (unsigned char*)[blob bytes];
unsigned int length = sLen + 2;
unsigned int i;
if (YES == escapeStrings)
{
length++; // Allow for leading 'E'
}
for (i = 0; i < sLen; i++)
{
unsigned char c = src[i];
if (c < 32 || c > 126 || c == '\'')
{
length += 4;
}
else if (c == '\\')
{
length += 3;
}
}
return length;
}
- (NSData *) dataFromBLOB: (const char *)blob
{
NSMutableData *md;
unsigned sLen = strlen(blob == 0 ? "" : blob);
unsigned dLen = 0;
unsigned char *dst;
unsigned i;
if (sLen > 1 && '\\' == blob[0] && 'x' == blob[1])
{
dLen = (sLen - 2) / 2;
dst = (unsigned char*)NSAllocateCollectable(dLen, 0);
md = [NSMutableData dataWithBytesNoCopy: dst length: dLen];
dLen = 0;
for (i = 2; i < sLen; i += 2)
{
unsigned hi = blob[i];
unsigned lo = blob[i + 1];
hi = (hi > '9') ? (hi - 'a' + 10) : (hi - '0');
lo = (lo > '9') ? (lo - 'a' + 10) : (lo - '0');
dst[dLen++] = (hi << 4) + lo;
}
}
else
{
for (i = 0; i < sLen; i++)
{
unsigned c = blob[i];
dLen++;
if (c == '\\')
{
c = blob[++i];
if (c != '\\')
{
i += 2; // Skip 2 digits octal
}
}
}
dst = (unsigned char*)NSAllocateCollectable(i, dLen);
md = [NSMutableData dataWithBytesNoCopy: dst length: dLen];
dLen = 0;
for (i = 0; i < sLen; i++)
{
unsigned c = blob[i];
if (c == '\\')
{
c = blob[++i];
if (c != '\\')
{
c = c - '0';
c <<= 3;
c += blob[++i] - '0';
c <<= 3;
c += blob[++i] - '0';
}
}
dst[dLen++] = c;
}
}
return md;
}
- (NSDate*) dbToDateFromBuffer: (char*)b length: (int)l
{
char buf[l+32]; /* Allow space to expand buffer. */
NSDate *d;
BOOL milliseconds = NO;
NSString *s;
int i;
memcpy(buf, b, l);
b = buf;
/*
* Find end of string.
*/
for (i = 0; i < l; i++)
{
if (b[i] == '\0')
{
l = i;
break;
}
}
while (l > 0 && isspace(b[l-1]))
{
l--;
}
b[l] = '\0';
if (l == 10)
{
s = [NSString stringWithUTF8String: b];
return [NSCalendarDate dateWithString: s
calendarFormat: @"%Y-%m-%d"
locale: nil];
}
else
{
int e;
i = l;
/* Convert +/-HH:SS timezone to +/-HHSS
*/
if (i > 5 && b[i-3] == ':' && (b[i-6] == '+' || b[i-6] == '-'))
{
b[i-3] = b[i-2];
b[i-2] = b[i-1];
b[--i] = '\0';
}
while (i-- > 0)
{
if (b[i] == '+' || b[i] == '-')
{
break;
}
if (b[i] == ':' || b[i] == ' ')
{
i = 0;
break; /* No time zone found */
}
}
if (i == 0)
{
/* A date and time without a timezone ... assume gmt */
strcpy(b + l, " +0000");
i = l + 1;
l += 6;
}
e = i;
if (isdigit(b[i-1]))
{
/*
* Make space between seconds and timezone.
*/
memmove(&b[i+1], &b[i], l - i);
b[i++] = ' ';
b[++l] = '\0';
}
/*
* Ensure we have a four digit timezone value.
*/
if (isdigit(b[i+1]) && isdigit(b[i+2]))
{
if (b[i+3] == '\0')
{
// Two digit time zone ... append zero minutes
b[l++] = '0';
b[l++] = '0';
b[l] = '\0';
}
else if (b[i+3] == ':')
{
// Zone with colon before minutes ... remove it
b[i+3] = b[i+4];
b[i+4] = b[i+5];
b[--l] = '\0';
}
}
/* FIXME ... horrible kludge for postgres returning timestamps with
fractional second information. Force it to 3 digit millisecond */
while (i-- > 0)
{
if (b[i] == '.')
{
milliseconds = YES;
i++;
if (!isdigit(b[i]))
{
memmove(&b[i+3], &b[i], e-i);
l += 3;
memcpy(&b[i], "000", 3);
}
i++;
if (!isdigit(b[i]))
{
memmove(&b[i+2], &b[i], e-i);
l += 2;
memcpy(&b[i], "00", 2);
}
i++;
if (!isdigit(b[i]))
{
memmove(&b[i+1], &b[i], e-i);
l += 1;
memcpy(&b[i], "0", 1);
}
i++;
break;
}
}
if (i > 0 && i < e)
{
memmove(&b[i], &b[e], l - e);
l -= (e - i);
}
b[l] = '\0';
if (l == 0)
{
return nil;
}
s = [NSString stringWithUTF8String: b];
if (milliseconds == YES)
{
d = [NSCalendarDate dateWithString: s
calendarFormat: @"%Y-%m-%d %H:%M:%S.%F %z"
locale: nil];
}
else
{
d = [NSCalendarDate dateWithString: s
calendarFormat: @"%Y-%m-%d %H:%M:%S %z"
locale: nil];
}
return d;
}
}
- (void) dealloc
{
if (extra != 0)
{
[self disconnect];
NSZoneFree(NSDefaultMallocZone(), extra);
}
[super dealloc];
}
- (NSString*) quoteString: (NSString *)s
{
NSData *d = [s dataUsingEncoding: NSUTF8StringEncoding];
unsigned l = [d length];
unsigned char *to = NSZoneMalloc(NSDefaultMallocZone(), (l * 2) + 3);
#ifdef HAVE_PQESCAPESTRINGCONN
int err;
[self connect];
l = PQescapeStringConn(connection, (char*)(to + 1), [d bytes], l, &err);
#else
l = PQescapeString(to + 1, [d bytes], l);
#endif
to[0] = '\'';
to[l + 1] = '\'';
s = [[NSString alloc] initWithBytesNoCopy: to
length: l + 2
encoding: NSUTF8StringEncoding
freeWhenDone: YES];
return [s autorelease];
}
@end