libs-sqlclient/Postgres.m

1911 lines
48 KiB
Mathematica
Raw Normal View History

/* -*-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/NSAutoreleasePool.h>
#import <Foundation/NSCalendarDate.h>
#import <Foundation/NSCharacterSet.h>
#import <Foundation/NSData.h>
#import <Foundation/NSDate.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSException.h>
#import <Foundation/NSFileHandle.h>
#import <Foundation/NSLock.h>
#import <Foundation/NSMapTable.h>
#import <Foundation/NSNotification.h>
#import <Foundation/NSNotificationQueue.h>
#import <Foundation/NSNull.h>
#import <Foundation/NSProcessInfo.h>
#import <Foundation/NSRunLoop.h>
#import <Foundation/NSString.h>
#import <Foundation/NSThread.h>
#import <Foundation/NSTimeZone.h>
#import <Foundation/NSUserDefaults.h>
#import <Foundation/NSValue.h>
#include "config.h"
#define SQLCLIENT_PRIVATE @public
#include "SQLClient.h"
#include <libpq-fe.h>
@interface SQLClientPostgres : SQLClient
{
NSDictionary *options;
}
@end
@interface SQLClientPostgres(Embedded)
- (NSData*) dataFromBLOB: (const char *)blob;
- (NSDate*) dbToDateFromBuffer: (char*)b length: (int)l;
@end
#if defined(GNUSTEP_BASE_LIBRARY) && !defined(__MINGW__)
@interface SQLClientPostgres (RunLoop) <RunLoopEvents>
- (void) receivedEvent: (void*)data
type: (RunLoopEventType)type
extra: (void*)extra
forMode: (NSString*)mode;
@end
#endif
typedef struct {
PGconn *_connection;
int _backendPID;
int _descriptor; // For monitoring in run loop
NSRunLoop *_runLoop; // For listen/unlisten monitoring
NSDictionary *_options;
} ConnectionInfo;
#define cInfo ((ConnectionInfo*)(self->extra))
#define backendPID (cInfo->_backendPID)
#define connection (cInfo->_connection)
#define options (cInfo->_options)
static NSDate *future = nil;
static NSNull *null = nil;
#if defined(GNUSTEP)
static NSString *placeholder = nil;
#else
static Class stringClass = Nil;
static SEL allocStringSel = 0;
static IMP allocStringImp = 0;
#endif
static SEL initStringSel = 0;
static IMP initStringImp = 0;
static NSTimeZone *zones[47]; // For -23 to +23 hours
static inline NSString*
newString(const char *b, int l, NSStringEncoding e)
{
#if defined(GNUSTEP)
return (*initStringImp)(placeholder, initStringSel, b, l, e);
#else
NSString *s = (*allocStringImp)(stringClass, allocStringSel);
return (*initStringImp)(s, initStringSel, b, l, e);
#endif
}
static NSDate*
newDateFromBuffer(const char *b, int l)
{
NSCalendarDate *d;
NSTimeZone *zone = nil;
2024-01-26 15:30:31 +00:00
int microseconds = 0;
int day;
int month;
int year;
int hour;
int minute;
int second;
int i;
i = 0;
if (i >= l || !isdigit(b[i])) return nil;
year = b[i++] - '0';
if (i >= l || !isdigit(b[i])) return nil;
year = year * 10 + b[i++] - '0';
if (i >= l || !isdigit(b[i])) return nil;
year = year * 10 + b[i++] - '0';
if (i >= l || !isdigit(b[i])) return nil;
year = year * 10 + b[i++] - '0';
if (i >= l || b[i++] != '-') return nil;
if (i >= l || !isdigit(b[i])) return nil;
month = b[i++] - '0';
if (i >= l || !isdigit(b[i])) return nil;
month = month * 10 + b[i++] - '0';
if (month < 1 || month > 12) return nil;
if (i >= l || b[i++] != '-') return nil;
if (i >= l || !isdigit(b[i])) return nil;
day = b[i++] - '0';
if (i >= l || !isdigit(b[i])) return nil;
day = day * 10 + b[i++] - '0';
if (day < 1 || day > 31) return nil;
if (i == l)
{
hour = 0;
minute = 0;
second = 0;
zone = [NSTimeZone localTimeZone];
}
else
{
if (i >= l || b[i++] != ' ') return nil;
if (i >= l || !isdigit(b[i])) return nil;
hour = b[i++] - '0';
if (i >= l || !isdigit(b[i])) return nil;
hour = hour * 10 + b[i++] - '0';
if (hour < 0 || hour > 23) return nil;
if (i >= l || b[i++] != ':') return nil;
if (i >= l || !isdigit(b[i])) return nil;
minute = b[i++] - '0';
if (i >= l || !isdigit(b[i])) return nil;
minute = minute * 10 + b[i++] - '0';
if (minute < 0 || minute > 59) return nil;
if (i >= l || b[i++] != ':') return nil;
if (i >= l || !isdigit(b[i])) return nil;
second = b[i++] - '0';
if (i >= l || !isdigit(b[i])) return nil;
second = second * 10 + b[i++] - '0';
if (second < 0 || second > 60) return nil;
if (i < l && '.' == b[i])
{
i++;
2024-01-26 15:25:18 +00:00
if (i >= l || !isdigit(b[i])) return nil;
2024-01-26 15:30:31 +00:00
microseconds = b[i++] - '0';
microseconds *= 10;
if (i < l && isdigit(b[i])) microseconds += b[i++] - '0';
microseconds *= 10;
if (i < l && isdigit(b[i])) microseconds += b[i++] - '0';
microseconds *= 10;
if (i < l && isdigit(b[i])) microseconds += b[i++] - '0';
microseconds *= 10;
if (i < l && isdigit(b[i])) microseconds += b[i++] - '0';
microseconds *= 10;
if (i < l && isdigit(b[i])) microseconds += b[i++] - '0';
while (i < l && isdigit(b[i]))
i++;
}
if (i < l && ('+' == b[i] || '-' == b[i]))
{
char sign = b[i++];
int timezone;
if (i >= l || !isdigit(b[i])) return nil;
timezone = b[i++] - '0';
if (i >= l || !isdigit(b[i])) return nil;
timezone = timezone * 10 + b[i++] - '0';
if (timezone < 0 || timezone > 23) return nil;
timezone *= 60; // Convert to minutes
if (i < l && ':' == b[i])
{
int tzmin;
i++;
if (i >= l || !isdigit(b[i])) return nil;
tzmin = b[i++] - '0';
if (i >= l || !isdigit(b[i])) return nil;
tzmin = tzmin * 10 + b[i++] - '0';
if (tzmin < 0 || tzmin > 59) return nil;
timezone += tzmin;
}
if ('-' == sign)
timezone = -timezone;
if (timezone % 60 == 0)
{
zone = zones[23 + timezone / 60];
}
else
{
zone = [NSTimeZone timeZoneForSecondsFromGMT: timezone * 60];
}
}
else
{
zone = [NSTimeZone localTimeZone];
}
}
d = [NSCalendarDate alloc];
if (year <= 1)
{
static NSTimeInterval p = 0.0;
if (0.0 == p)
{
p = [[NSDate distantPast] timeIntervalSinceReferenceDate];
}
d = [d initWithTimeIntervalSinceReferenceDate: p];
[d setTimeZone: zone];
}
else if (year > 4000)
{
static NSTimeInterval f = 0.0;
if (0.0 == f)
{
f = [[NSDate distantFuture] timeIntervalSinceReferenceDate];
}
d = [d initWithTimeIntervalSinceReferenceDate: f];
[d setTimeZone: zone];
}
else
{
d = [d initWithYear: year
month: month
day: day
hour: hour
minute: minute
second: second
timeZone: zone];
2024-01-26 15:30:31 +00:00
/* Postgres support six digits precision, but the ObjC APIs tend
* to use milliseconds. For now, truncate.
*/
if (microseconds > 0)
{
2024-01-26 15:30:31 +00:00
int milliseconds = microseconds / 1000;
2024-01-26 15:30:31 +00:00
if (milliseconds > 0)
{
NSTimeInterval ti;
ti = milliseconds;
ti /= 1000.0;
ti += [d timeIntervalSinceReferenceDate];
d = [d initWithTimeIntervalSinceReferenceDate: ti];
[d setTimeZone: zone];
}
}
}
[d setCalendarFormat: @"%Y-%m-%d %H:%M:%S %z"];
return d;
}
static inline NSString *
sanitize(NSString *str, NSRange range)
{
if (range.length != 0)
{
str = [str stringByReplacingCharactersInRange: range
withString: @"'******'"];
}
return str;
}
@implementation SQLClientPostgres
+ (void) initialize
{
if (future == nil)
{
int i;
initStringSel = @selector(initWithBytes:length:encoding:);
#if defined(GNUSTEP)
placeholder = [NSString alloc];
initStringImp
= [[placeholder class] instanceMethodForSelector: initStringSel];
#else
stringClass = [NSString class];
allocStringSel = @selector(alloc);
allocStringImp = [stringClass methodForSelector: allocStringSel];
initStringImp = [stringClass instanceMethodForSelector: initStringSel];
#endif
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];
for (i = -23; i <= 23; i++)
{
zones[i + 23]
= [[NSTimeZone timeZoneForSecondsFromGMT: i * 60 * 60] 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];
}
/* Encapsulates cleanup to be done when the database connection is lost
* or should be dropped by us (eg due to an unrecoverable error).
*/
- (void) _backendDisconnected
{
if (extra != 0 && connection != 0)
{
#if defined(GNUSTEP_BASE_LIBRARY) && !defined(__MINGW__)
if (cInfo->_runLoop != nil)
{
if (cInfo->_descriptor >= 0)
{
[cInfo->_runLoop removeEvent: (void*)(uintptr_t)cInfo->_descriptor
type: ET_RDESC
forMode: NSDefaultRunLoopMode
all: YES];
cInfo->_descriptor = -1;
}
DESTROY(cInfo->_runLoop);
}
#endif
PQfinish(connection);
connection = 0;
connected = NO;
}
}
- (BOOL) backendConnect
{
if (extra == 0)
{
extra = NSZoneMalloc(NSDefaultMallocZone(), sizeof(ConnectionInfo));
memset(extra, '\0', sizeof(ConnectionInfo));
2017-08-25 09:37:52 +00:00
cInfo->_descriptor = -1;
}
if (connection == 0)
{
connected = NO;
if ([self database] != nil)
{
NSString *host = nil;
NSString *port = nil;
NSString *dbase = [self database];
2023-08-16 15:22:29 +00:00
NSString *sslmode;
int timeout;
NSString *str;
NSRange r;
NSRange pwRange = NSMakeRange(NSNotFound, 0);
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="];
pwRange = NSMakeRange([m length], [str length]);
[m appendString: str];
}
str = connectQuote([self clientName]);
if (str != nil)
{
[m appendString: @" application_name="];
[m appendString: str];
}
2023-08-16 15:22:29 +00:00
sslmode = [options objectForKey: @"sslmode"];
if ([sslmode isEqual: @"require"])
{
str = connectQuote(@"require");
if (str != nil)
{
[m appendString: @" sslmode="];
[m appendString: str];
}
}
2023-08-16 15:22:29 +00:00
timeout = [[options objectForKey: @"connect_timeout"] intValue];
if (timeout > 0)
{
[m appendFormat: @" connect_timeout=%d", timeout];
}
if ([self debugging] > 0)
{
[self debug: @"Connect to '%@' as %@ (%@)",
sanitize(m, pwRange), [self name], [self clientName]];
}
connection = PQconnectdb([m UTF8String]);
if (PQstatus(connection) != CONNECTION_OK)
{
[self debug: @"Error connecting to '%@' (%@) - %s",
[self name], sanitize(m, pwRange), PQerrorMessage(connection)];
[self _backendDisconnected];
}
else if (PQsetClientEncoding(connection, "UTF-8") < 0)
{
[self debug: @"Error setting UTF-8 with '%@' (%@) - %s",
[self name], sanitize(m, pwRange), PQerrorMessage(connection)];
[self _backendDisconnected];
}
else
{
const char *p;
backendPID = PQbackendPID(connection);
connected = YES;
p = PQparameterStatus(connection, "standard_conforming_strings");
if (p != 0)
{
PGresult *result;
/* If the escape_string_warning setting is on,
* the server will warn about backslashes even
* in properly quoted strings, so turn it off.
*/
if (strcmp(p, "on") == 0)
{
result = PQexec(connection,
"SET escape_string_warning=off");
}
else
{
result = PQexec(connection,
"SET standard_conforming_strings=on;"
"SET escape_string_warning=off");
}
if (0 == result
|| PQresultStatus(result) != PGRES_COMMAND_OK)
{
[self debug: @"Error setting string handling"
@" with '%@' (%@) - %s",
[self name], sanitize(m, pwRange),
PQerrorMessage(connection)];
if (result != 0)
{
PQclear(result);
result = 0;
}
[self _backendDisconnected];
}
if (result != 0)
{
PQclear(result);
}
}
else
{
[self _backendDisconnected];
[self debug: @"Postgres without standard conforming strings"];
}
if ([self debugging] > 0)
{
if (YES == connected)
{
[self debug: @"Connected to '%@'", [self name]];
}
else
{
[self debug: @"Disconnected '%@'", [self name]];
}
}
}
}
else
{
[self debug:
@"Connect to '%@' with no user/password/database configured",
[self name]];
}
}
return connected;
}
/* Called by the -disconnect method aftert it has updated ivars/locks.
*/
- (void) backendDisconnect
{
if (extra != 0 && connection != 0)
{
NS_DURING
{
if ([self debugging] > 0)
{
[self debug: @"Disconnecting client %@", [self clientName]];
}
[self _backendDisconnected];
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) _postNotification: (NSNotification*)n
{
NSNotificationQueue *nq;
/* Post asynchronously
*/
if ([self debugging] > 0)
{
[self debug: @"Notified (database): %@", n];
}
nq = [NSNotificationQueue defaultQueue];
[nq enqueueNotification: n
postingStyle: NSPostASAP
coalesceMask: NSNotificationNoCoalescing
forModes: nil];
}
- (void) _postNotifications: (NSArray*)notifications
{
NSUInteger count = [notifications count];
NSUInteger index;
for (index = 0; index < count; index++)
{
NSNotification *n = [notifications objectAtIndex: index];
NS_DURING
{
[self _postNotification: n];
}
NS_HANDLER
{
NSLog(@"Problem posting notification: %@ %@",
n, localException);
}
NS_ENDHANDLER
}
}
- (void) _post: (NSMutableArray*)notifications
{
if ([notifications count] > 0)
{
if ([[NSRunLoop currentRunLoop] currentMode] == nil)
{
if ([self debugging] > 0)
{
[self debug: @"Notifying (main thread): %@", notifications];
}
NS_DURING
{
[self performSelectorOnMainThread: @selector(_postNotifications:)
withObject: notifications
waitUntilDone: NO];
}
NS_HANDLER
{
NSLog(@"Problem posting to main thread: %@ %@",
notifications, localException);
}
NS_ENDHANDLER
}
else
{
if ([self debugging] > 0)
{
[self debug: @"Notifying (receiving thread): %@", notifications];
}
NS_DURING
{
[self _postNotifications: notifications];
}
NS_HANDLER
{
NSLog(@"Problem posting in receiving thread: %@ %@",
notifications, localException);
}
NS_ENDHANDLER
}
[notifications removeAllObjects];
}
}
/* This method must only be called when the receiver is locked.
*/
- (void) _checkNotifications: (BOOL)async
{
NSMutableArray *notifications = nil;
PGnotify *notify;
/* While postgres sometimes de-duplicates notifications it is not guaranteed
* that it will do so, and it is therefore possible for the database server
* to send many duplicate notifications.
* So we read the notifications and add them to an array only if they are not
* already present, flushing the buffer when it gets large.
*/
while ((notify = PQnotifies(connection)) != 0)
{
NS_DURING
{
static NSNumber *nY = nil;
static NSNumber *nN = nil;
NSNotification *n;
NSMutableDictionary *userInfo;
NSString *name;
if (nil == nN)
{
ASSIGN(nN, [NSNumber numberWithBool: NO]);
}
if (nil == nY)
{
ASSIGN(nY, [NSNumber numberWithBool: YES]);
}
name = [[NSString alloc] initWithUTF8String: notify->relname];
userInfo = [[NSMutableDictionary alloc] initWithCapacity: 3];
if (0 != notify->extra)
{
NSString *payload;
payload = [[NSString alloc] initWithUTF8String: notify->extra];
if (nil != payload)
{
[userInfo setObject: payload forKey: @"Payload"];
RELEASE(payload);
}
}
if (notify->be_pid == backendPID)
{
[userInfo setObject: nY forKey: @"Local"];
}
else
{
[userInfo setObject: nN forKey: @"Local"];
}
if (YES == async)
{
[userInfo setObject: nY forKey: @"Async"];
}
else
{
[userInfo setObject: nN forKey: @"Async"];
}
n = [NSNotification notificationWithName: name
object: self
userInfo: (NSDictionary*)userInfo];
if (nil == notifications)
{
notifications = [[NSMutableArray alloc] initWithCapacity: 10];
}
if (NO == [notifications containsObject: n])
{
[notifications addObject: n];
}
RELEASE(name);
RELEASE(userInfo);
}
NS_HANDLER
{
NSLog(@"Problem handling %@ notification: %@",
(async ? @"asynchronous" : @"query/execute"), localException);
}
NS_ENDHANDLER
PQfreemem(notify);
if ([notifications count] >= 1000)
{
NSLog(@"WARNING ... 1000 dbase notifications in buffer (flushing)");
[self _post: notifications];
}
}
/* Now that we have read all the available notifications from the database,
* we post them locally in the current thread (if its run loop is active)
* or the main thread.
*/
[self _post: notifications];
RELEASE(notifications);
}
- (NSInteger) backendExecute: (NSArray*)info
{
NSAutoreleasePool *arp = [NSAutoreleasePool new];
NSInteger rowCount = -1;
PGresult *result = 0;
2017-06-30 10:55:33 +00:00
NSString *stmt = SQLClientUnProxyLiteral([info objectAtIndex: 0]);
if ([stmt length] == 0)
{
[arp release];
[NSException raise: NSInternalInconsistencyException
format: @"Statement produced null string"];
}
NS_DURING
{
const char *statement;
const char *tuples;
unsigned length;
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 (0 == result
|| (PQresultStatus(result) != PGRES_COMMAND_OK
&& PQresultStatus(result) != PGRES_TUPLES_OK))
{
NSString *str;
const char *cstr;
if (0 == result)
{
cstr = PQerrorMessage(connection);
}
else
{
cstr = PQresultErrorMessage(result);
}
str = [NSString stringWithUTF8String: cstr];
if (nil == str)
{
str = [NSString stringWithCString: cstr];
}
if (result != 0)
{
PQclear(result);
result = 0;
}
if (PQstatus(connection) != CONNECTION_OK)
{
[self disconnect];
2017-08-27 11:41:22 +00:00
[NSException raise: SQLConnectionException
format: @"Error executing %@: %@", stmt, str];
}
else
{
[NSException raise: SQLException
format: @"Error executing %@: %@", stmt, str];
}
}
tuples = PQcmdTuples(result);
if (0 != tuples)
{
rowCount = atol(tuples);
}
}
NS_HANDLER
{
if (result != 0)
{
PQclear(result);
}
if (YES == connected && PQstatus(connection) != CONNECTION_OK)
{
[self disconnect];
}
[localException retain];
[arp release];
[localException autorelease];
[localException raise];
}
NS_ENDHANDLER
if (result != 0)
{
PQclear(result);
}
[self _checkNotifications: NO];
[arp release];
return rowCount;
}
- (void) backendListen: (NSString*)name
{
[self execute: @"LISTEN ", name, nil];
#if defined(GNUSTEP_BASE_LIBRARY) && !defined(__MINGW__)
if (extra != 0 && connection != 0)
{
if (nil == cInfo->_runLoop)
{
ASSIGN(cInfo->_runLoop, [NSRunLoop currentRunLoop]);
if (nil == cInfo->_runLoop)
{
[NSException raise: NSInternalInconsistencyException
format: @"Observer can't be set up ... no runloop in thread"];
}
}
else if ([NSRunLoop currentRunLoop] != cInfo->_runLoop)
{
[NSException raise: NSInternalInconsistencyException
format: @"Observer added to the same client from another runloop"];
}
if (cInfo->_descriptor < 0)
{
int descriptor = PQsocket(connection);
if (descriptor < 0)
{
DESTROY(cInfo->_runLoop);
[NSException raise: NSInternalInconsistencyException
format: @"Observer can't be set up ... bad file descriptor"];
}
2017-08-25 09:37:52 +00:00
cInfo->_descriptor = descriptor;
}
[cInfo->_runLoop addEvent: (void*)(uintptr_t)cInfo->_descriptor
type: ET_RDESC
watcher: self
forMode: NSDefaultRunLoopMode];
}
#endif
}
- (void) backendNotify: (NSString*)name payload: (NSString*)more
{
if (nil == more)
{
[self execute: @"NOTIFY ", name, nil];
}
else
{
[self execute: @"NOTIFY ", name, @",", [self quote: more], nil];
}
}
static inline unsigned int trim(char *str, unsigned len)
{
while (len > 0 && isspace(str[len - 1]))
{
len--;
}
return len;
}
- (char*) parseIntoArray: (NSMutableArray *)a type: (int)t from: (char*)p
{
p++; /* Step past '{' */
while (*p && *p != '}')
{
id v = nil;
/* Ignore leading space before field data.
*/
while (isspace(*p))
{
p++;
}
if ('{' == *p)
{
/* Found a nested array.
*/
v = [[NSMutableArray alloc] initWithCapacity: 10];
p = [self parseIntoArray: v type: t from: p];
}
else if ('\"' == *p)
{
char *start = ++p;
int len = 0;
/* Found something quoted (char, varchar, text, bytea etc)
*/
while (*p != '\0' && *p != '\"')
{
if ('\\' == *p)
{
p++;
}
p++;
len++;
}
if ('\"' == *p)
{
*p = '\0';
}
if (len == (p - start))
{
if ('T' == t)
{
/* This is expected to be a timestamp
*/
v = newDateFromBuffer(start, p - start);
}
else if ('D' == t)
{
/* This is expected to be bytea data
*/
v = [[self dataFromBLOB: start] retain];
}
else
{
v = newString(start, p - start, NSUTF8StringEncoding);
}
}
else
{
char *buf;
char *ptr;
int i;
buf = malloc(len+1);
ptr = start;
i = 0;
while (*ptr != '\0')
{
if ('\\' == *ptr)
{
ptr++;
}
buf[i++] = *ptr++;
}
buf[len] = '\0';
if ('T' == t)
{
/* This is expected to be a timestamp
*/
v = newDateFromBuffer(buf, len);
free(buf);
}
else if ('D' == t)
{
/* This is expected to be bytea data
*/
v = [[self dataFromBLOB: buf] retain];
free(buf);
}
else
{
v = [NSString alloc];
v = [v initWithBytesNoCopy: buf
length: len
encoding: NSUTF8StringEncoding
freeWhenDone: YES];
}
}
*p++ = '\"';
}
else
{
char *start = p;
char save;
int len;
/* This is an unquoted field ... could be NULL or a boolean,
* or a numeric field, or a timestamp or just a simple string.
*/
while (*p != '\0' && *p != ',' && *p != '}')
{
p++;
}
save = *p;
*p = '\0';
len = trim(start, p - start);
if (strcmp(start, "NULL") == 0)
{
v = null;
}
else if ('T' == t)
{
v = newDateFromBuffer(start, len);
}
else if ('D' == t)
{
v = [[self dataFromBLOB: start] retain];
}
else if ('B' == t)
{
if (*start == 't')
v = @"YES";
else
v = @"NO";
}
else if ('I' == t || 'F' == t)
{
v = SQLClientNewLiteral(start, p - start);
}
else
{
v = newString(start, p - start, NSUTF8StringEncoding);
}
*p = save;
}
if (nil != v)
{
[a addObject: v];
[v release];
}
if (',' == *p)
{
p++;
}
}
if ('}' == *p)
{
p++;
}
return p;
}
- (id) newParseField: (char *)p type: (int)t size: (int)s
{
char arrayType = 0;
switch (t)
{
case 1082: // Date (treat as string)
s = trim(p, s);
return newString(p, s, NSASCIIStringEncoding);
case 1083: // Time (treat as string)
s = trim(p, s);
return newString(p, s, NSASCIIStringEncoding);
case 1114: // Timestamp without time zone.
case 1184: // Timestamp with time zone.
return newDateFromBuffer(p, trim(p, s));
case 16: // BOOL
if (*p == 't')
{
return @"YES";
}
else
{
return @"NO";
}
case 17: // BYTEA
return [[self dataFromBLOB: p] retain];
case 18: // "char"
return newString(p, s, NSUTF8StringEncoding);
case 20: // INT8
case 21: // INT2
case 23: // INT4
s = trim(p, s);
return SQLClientNewLiteral(p, s);
case 700: // FLOAT4
case 701: // FLOAT8
s = trim(p, s);
return SQLClientNewLiteral(p, s);
case 1115: // TS without TZ ARRAY
case 1185: // TS with TZ ARRAY
if (0 == arrayType) arrayType = 'T'; // Timestamp
case 1000: // BOOL ARRAY
if (0 == arrayType) arrayType = 'B'; // Boolean
case 1001: // BYTEA ARRAY
if (0 == arrayType) arrayType = 'D'; // Data
case 1005: // INT2 ARRAY
case 1007: // INT4 ARRAY
case 1016: // INT8 ARRAY
if (0 == arrayType) arrayType = 'I'; // Integer
case 1021: // FLOAT ARRAY
case 1022: // DOUBLE ARRAY
if (0 == arrayType) arrayType = 'F'; // Float
case 1002: // CHAR ARRAY
case 1009: // TEXT ARRAY
case 1014: // "char" ARRAY
case 1015: // VARCHAR ARRAY
case 1182: // DATE ARRAY
case 1183: // TIME ARRAY
case 1263: // CSTRING ARRAY
if ('{' == *p)
{
NSMutableArray *a;
a = [[NSMutableArray alloc] initWithCapacity: 10];
2022-09-27 09:21:04 +00:00
(void)[self parseIntoArray: a type: arrayType from: p];
if ([self debugging] > 2)
{
NSLog(@"Parsed array is %@", a);
}
return a;
}
case 25: // TEXT
default:
if (YES == _shouldTrim)
{
s = trim(p, s);
}
return newString(p, s, NSUTF8StringEncoding);
}
}
- (NSMutableArray*) backendQuery: (NSString*)stmt
recordType: (id)rtype
listType: (id)ltype
{
NSAutoreleasePool *arp = [NSAutoreleasePool new];
PGresult *result = 0;
NSMutableArray *records = nil;
2017-06-30 10:55:33 +00:00
stmt = SQLClientUnProxyLiteral(stmt);
if ([stmt length] == 0)
{
[arp release];
[NSException raise: NSInternalInconsistencyException
format: @"Statement produced null string"];
}
NS_DURING
{
char *statement;
statement = (char*)[stmt UTF8String];
result = PQexec(connection, statement);
if (0 == result
|| (PQresultStatus(result) != PGRES_COMMAND_OK
&& PQresultStatus(result) != PGRES_TUPLES_OK))
{
NSString *str;
const char *cstr;
if (0 == result)
{
cstr = PQerrorMessage(connection);
}
else
{
cstr = PQresultErrorMessage(result);
}
str = [NSString stringWithUTF8String: cstr];
if (nil == str)
{
str = [NSString stringWithCString: cstr];
}
if (result != 0)
{
PQclear(result);
result = 0;
}
if (PQstatus(connection) != CONNECTION_OK)
{
[self disconnect];
2017-08-27 11:41:22 +00:00
[NSException raise: SQLConnectionException
format: @"Error executing %@: %@", stmt, str];
}
else
{
[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 ftype[fieldCount];
int fmod[fieldCount];
int fformat[fieldCount];
SQLRecordKeys *k = nil;
int d = [self debugging];
int i;
for (i = 0; i < fieldCount; i++)
{
keys[i] = [NSString stringWithUTF8String: PQfname(result, i)];
ftype[i] = PQftype(result, i);
fmod[i] = PQfmod(result, i);
fformat[i] = PQfformat(result, i);
}
records = [[ltype alloc] initWithCapacity: recordCount];
/* Create buffers to store the previous row from the
* database and the previous objc values.
*/
int len[fieldCount];
const char *ptr[fieldCount];
id obj[fieldCount];
for (i = 0; i < fieldCount; i++)
{
len[i] = -1;
obj[i] = nil;
}
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 (d > 1)
{
2024-01-26 14:12:01 +00:00
#if 0
/* For even more debug we can write some of the
* data retrieved, but that may be a security
* issue.
*/
if (0 == fformat[j] && size <= 100)
{
[self debug:
@"%@ type:%d mod:%d size: %d %*.*s\n",
keys[j], ftype[j], fmod[j], size,
size, size, p];
}
else
#endif
{
[self debug: @"%@ type:%d mod:%d size: %d\n",
keys[j], ftype[j], fmod[j], size];
}
}
/* Often many rows will contain the same data in
* one or more columns, so we check to see if the
* value we have just read is small and identical
* to the value in the same column of the previous
* row. Only if it isn't do we create a new object.
*/
if (size == len[j] && size <= 20
&& memcmp(p, ptr[j], (size_t)size) == 0)
{
v = obj[j];
}
else
{
[obj[j] release];
if (fformat[j] == 0) // Text
{
v = [self newParseField: p
type: ftype[j]
size: size];
obj[j] = v;
len[j] = size;
ptr[j] = p;
}
else // Binary
{
NSLog(@"Binary data treated as NSNull "
@"in %@ type:%d mod:%d size:%d\n",
keys[j], ftype[j], fmod[j], size);
}
}
}
values[j] = v;
}
if (nil == k)
{
/* We don't have keys information, so use the
* constructor where we list keys and, if the
* resulting record provides keys information
* on the first record, we save it for later.
*/
record = [rtype newWithValues: values
keys: keys
count: fieldCount];
if (0 == i && [record respondsToSelector: @selector(keys)])
{
k = [record keys];
}
}
else
{
record = [rtype newWithValues: values keys: k];
}
[records addObject: record];
[record release];
}
for (i = 0; i < fieldCount; i++)
{
[obj[i] release];
}
}
else
{
[NSException raise: SQLException format: @"Error executing %@: %s",
stmt, "query produced no result"];
}
}
NS_HANDLER
{
if (result != 0)
{
PQclear(result);
result = 0;
}
if (YES == connected && PQstatus(connection) != CONNECTION_OK)
{
[self disconnect];
}
[records release];
records = nil;
[localException retain];
[arp release];
[localException autorelease];
[localException raise];
}
NS_ENDHANDLER
[arp release];
if (result != 0)
{
PQclear(result);
}
[self _checkNotifications: NO];
return [records autorelease];
}
- (void) backendUnlisten: (NSString*)name
{
#if defined(GNUSTEP_BASE_LIBRARY) && !defined(__MINGW__)
if (extra != 0 && cInfo->_runLoop != nil && cInfo->_descriptor >= 0)
{
BOOL all = ([_names count] == 0) ? YES : NO;
/* Remove the event added to listen for this name.
* If there are no remaining names listened for, cleanup.
*/
[cInfo->_runLoop removeEvent: (void*)(uintptr_t)cInfo->_descriptor
type: ET_RDESC
forMode: NSDefaultRunLoopMode
all: all];
if (YES == all)
{
cInfo->_descriptor = -1;
2017-08-25 09:36:39 +00:00
DESTROY(cInfo->_runLoop);
}
}
#endif
[self execute: @"UNLISTEN ", name, nil];
}
- (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;
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;
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. */
NSCalendarDate *d;
BOOL milliseconds = NO;
BOOL timezone = NO;
NSString *s;
int i;
int e;
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];
}
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)
{
e = l;
}
else
{
timezone = YES;
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';
}
}
}
/* kludge for 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 (YES == timezone)
{
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];
}
}
else
{
if (milliseconds == YES)
{
d = [NSCalendarDate dateWithString: s
calendarFormat: @"%Y-%m-%d %H:%M:%S.%F"
locale: nil];
}
else
{
d = [NSCalendarDate dateWithString: s
calendarFormat: @"%Y-%m-%d %H:%M:%S"
locale: nil];
}
}
[d setCalendarFormat: @"%Y-%m-%d %H:%M:%S %z"];
return d;
}
- (void) dealloc
{
if (extra != 0)
{
if (YES == connected)
{
[self disconnect];
}
RELEASE(options);
NSZoneFree(NSDefaultMallocZone(), extra);
}
[super dealloc];
}
- (NSMutableString*) quoteArray: (NSArray *)a
toString: (NSMutableString *)s
quotingStrings: (BOOL)q
{
NSUInteger count;
NSUInteger index;
NSAssert([a isKindOfClass: [NSArray class]], NSInvalidArgumentException);
if (nil == s)
{
s = [NSMutableString stringWithCapacity: 1000];
}
[s appendString: @"ARRAY["];
count = [a count];
for (index = 0; index < count; index++)
{
id o = [a objectAtIndex: index];
if (index > 0)
{
[s appendString: @","];
}
if ([o isKindOfClass: [NSArray class]])
{
[self quoteArray: (NSArray *)o toString: s quotingStrings: q];
}
else if ([o isKindOfClass: [NSString class]])
{
if (YES == q)
{
o = [self quoteString: (NSString*)o];
}
[s appendString: (NSString*)o];
}
else if ([o isKindOfClass: [NSDate class]])
{
[s appendString: [self quote: (NSString*)o]];
[s appendString: @"::timestamp with time zone"];
}
else if ([o isKindOfClass: [NSData class]])
{
unsigned len = [self lengthOfEscapedBLOB: o];
uint8_t *buf;
buf = malloc(len+1);
[self copyEscapedBLOB: o into: buf];
buf[len] = '\0';
[s appendFormat: @"%s::bytea", buf];
free(buf);
}
else
{
o = [self quote: (NSString*)o];
[s appendString: (NSString*)o];
}
}
[s appendString: @"]"];
return s;
}
- (void) setOptions: (NSDictionary*)o
{
if (0 == extra)
{
extra = NSZoneMalloc(NSDefaultMallocZone(), sizeof(ConnectionInfo));
memset(extra, '\0', sizeof(ConnectionInfo));
cInfo->_descriptor = -1;
}
ASSIGNCOPY(options, o);
}
@end
#if defined(GNUSTEP_BASE_LIBRARY) && !defined(__MINGW__)
@implementation SQLClientPostgres (RunLoop)
- (void) receivedEvent: (void*)data
type: (RunLoopEventType)type
extra: (void*)extra
forMode: (NSString*)mode
{
2017-08-27 11:41:22 +00:00
BOOL wasConnected;
/* Ensure that the receiver is locked so that no other thread can
* be using the database connection while we use it.
*/
[lock lock];
2017-08-27 11:41:22 +00:00
wasConnected = [self connected];
if (0 == connection)
{
/* The connection has gone, so we must remove the descriptor from
* the current run loop as we can no longer handle events.
*/
[[NSRunLoop currentRunLoop] removeEvent: data
type: ET_RDESC
forMode: NSDefaultRunLoopMode
all: YES];
2024-01-26 14:12:01 +00:00
[self debug: @"Listen event on disconnected client, desc: %d",
(int)(intptr_t)data];
}
else
{
if (0 == PQconsumeInput(connection))
{
NSString *nam = [[[self name] copy] autorelease];
char msg[1000];
strncpy(msg, PQerrorMessage(connection), sizeof(msg)-1);
msg[sizeof(msg)-1] = '\0';
if (PQstatus(connection) != CONNECTION_OK
2024-01-26 14:12:01 +00:00
|| PQsocket(connection) != (int)(intptr_t)data)
{
/* The connection has been lost, so we must disconnect,
* which will stop us receiving events on the descriptor.
*/
[self disconnect];
}
[self debug: @"Error consuming input for '%@' - %s", nam, msg];
}
else
{
[self _checkNotifications: YES];
}
}
/* If we are listening, try to reconnect.
*/
2017-08-27 11:41:22 +00:00
if (YES == wasConnected && NO == [self connected] && [_names count] > 0)
{
[self connect];
}
[lock unlock];
}
@end
#endif