/* -*-objc-*- */ /** Implementation of SQLClientECPG for GNUStep Copyright (C) 2004 Free Software Foundation, Inc. Written by: Richard Frith-Macdonald 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$ */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "config.h" #define SQLCLIENT_PRIVATE @public #include "SQLClient.h" @interface SQLClientECPG : SQLClient @end @interface SQLClientECPG(Embedded) - (const char *) blobFromData: (NSData*)data; - (NSData *) dataFromBlob: (const char *)blob; - (BOOL) dbFromDate: (NSDate*)d toBuffer: (char*)b length: (int)l; - (BOOL) dbFromString: (NSString*)s toBuffer: (char*)b length: (int)l; - (NSDate*) dbToDateFromBuffer: (char*)b length: (int)l; - (NSString*) dbToStringFromBuffer: (char*)b length: (int)l; @end /* This looks like a Postgres specific issue/feature/bug - for some * reason, ':' inside a string often causes the string to be * horribly mutilated before being handed to the database * ... probably postgres is trying to replace variables or something * - avoid all these problems by replacing ':' with its octal code * \\072. */ static NSString * hackForPrepare(NSString *s) { NSRange r; r = [s rangeOfString: @":"]; if (r.length > 0) { s = [s stringByReplacingString: @":" withString: @"\\072"]; } return s; } EXEC SQL INCLUDE sql3types; EXEC SQL INCLUDE sqlca; EXEC SQL WHENEVER SQLERROR CALL SQLErrorHandler(); /** * Return YES of the last SQL error indicated we are out of data, * NO otherwise. */ BOOL SQLOutOfData() { if (sqlca.sqlcode == 100) { return YES; } else { return NO; } } /** * This error handler is called for most errors ... so we can get it to * raise an exception for us. */ void SQLErrorHandler() { int code = sqlca.sqlcode; const char *ptr = sqlca.sqlerrm.sqlerrmc; const char *e0 = "'no connection to the server'"; const char *e1 = "Error in transaction processing"; sqlca.sqlcode = 0; // Reset error code NSLog (@"Raising an exception, %d, %s", code, sqlca.sqlerrm.sqlerrmc); if (strncmp(ptr, e0, strlen(e0)) == 0 || strncmp(ptr, e1, strlen(e1)) == 0) { [NSException raise: SQLConnectionException format: @"SQL Error: SQLCODE=(%d): %s", code, ptr]; } else { [NSException raise: SQLException format: @"SQL Error: SQLCODE=(%d): %s", code, ptr]; } } @implementation SQLClientECPG static NSDate *future = nil; + (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]; } } - (BOOL) backendConnect { if (connected == NO) { if ([self database] != nil && [self user] != nil && [self password] != nil) { Class c = NSClassFromString(@"CmdClient"); [[self class] purgeConnections: nil]; NS_DURING { EXEC SQL BEGIN DECLARE SECTION; const char *database_c; const char *user_c; const char *password_c; const char *client_c; EXEC SQL END DECLARE SECTION; database_c = [[self database] UTF8String]; user_c = [[self user] UTF8String]; password_c = [[self password] UTF8String]; client_c = [[self clientName] UTF8String]; if (c != 0) { if ([self debugging] > 0) { [self debug: @"Connect to '%@' database %s user %s as %s", [self name], database_c, user_c, client_c]; } } EXEC SQL CONNECT TO :database_c AS :client_c USER :user_c USING :password_c; connected = YES; if (c != 0) { if ([self debugging] > 0) { [self debug: @"Connected to '%@' (%s)", [self name], client_c]; } } EXEC SQL AT :client_c SET AUTOCOMMIT TO ON; // For backwards compat, make this the default EXEC SQL SET CONNECTION TO :client_c; } NS_HANDLER { [self debug: @"Error connecting to '%@' database: %@", [self name], localException]; } NS_ENDHANDLER } else { [self debug: @"Connect to '%@' with no user/password/database configured", [self name]]; } } return connected; } - (void) backendDisconnect { if (connected == YES) { NS_DURING { EXEC SQL BEGIN DECLARE SECTION; const char *client_c; EXEC SQL END DECLARE SECTION; if ([self isInTransaction] == YES) { [self rollback]; } client_c = [[self clientName] UTF8String]; if ([self debugging] > 0) { [self debug: @"Disconnecting client %@", [self clientName]]; } EXEC SQL DISCONNECT :client_c; if ([self debugging] > 0) { [self debug: @"Disconnected client %@", [self clientName]]; } } NS_HANDLER { [self debug: @"Error disconnecting from database (%@): %@", [self clientName], localException]; } NS_ENDHANDLER connected = NO; } } - (NSInteger) backendExecute: (NSArray*)info { EXEC SQL BEGIN DECLARE SECTION; char *statement; char *handle; EXEC SQL END DECLARE SECTION; NSAutoreleasePool *arp = [NSAutoreleasePool new]; unsigned int length; NSString *stmt = [info objectAtIndex: 0]; length = [stmt length]; if (length == 0) { [arp release]; [NSException raise: NSInternalInconsistencyException format: @"Statement produced null string"]; } statement = (char*)[hackForPrepare(stmt) UTF8String]; length = strlen(statement); statement = (char*)[self insertBLOBs: info intoStatement: statement length: length withMarker: "'''" length: 3 giving: &length]; handle = (char*)[[self clientName] UTF8String]; /* * Ensure we have a working connection. */ if ([self connect] == NO) { [arp release]; [NSException raise: SQLException format: @"Unable to connect to '%@' to execute statement %@", [self name], stmt]; } NS_DURING { EXEC SQL PREPARE command from :statement; EXEC SQL AT :handle EXECUTE command; } NS_HANDLER { NSString *n = [localException name]; NSString *msg = [localException reason]; if ([n isEqual: SQLConnectionException] == YES) { [self disconnect]; } /* * remove line number information from database exception message * since it's meaningless to the developer as it's the line number * in this file rather than the code which is calling us. */ if ([n isEqual: SQLException] == YES || [n isEqual: SQLConnectionException] == YES) { NSRange r; r = [msg rangeOfString: @" in line " options: NSBackwardsSearch]; if (r.length > 0) { msg = [msg substringToIndex: r.location]; localException = [NSException exceptionWithName: n reason: msg userInfo: nil]; } } if ([self debugging] > 0) { [self debug: @"Error executing statement:\n%@\n%@", stmt, localException]; } [localException retain]; [arp release]; [localException autorelease]; [localException raise]; } NS_ENDHANDLER [arp release]; return -1; } 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 { EXEC SQL BEGIN DECLARE SECTION; bool aBool; int anInt; int count; int index; int indicator; int type; int length; int octetLength; int precision; int scale; int returnedOctetLength; int dtiCode; char fieldName[120]; char *aString; float aFloat; double aDouble; char *query; char *handle; EXEC SQL END DECLARE SECTION; NSAutoreleasePool *arp = [NSAutoreleasePool new]; NSMutableArray *records; BOOL isOpen = NO; BOOL localTransaction = NO; length = [stmt length]; if (length == 0) { [arp release]; [NSException raise: NSInternalInconsistencyException format: @"Statement produced null string"]; } query = (char*)[hackForPrepare(stmt) UTF8String]; handle = (char*)[[self clientName] UTF8String]; records = [[ltype alloc] initWithCapacity: 32]; /* * Ensure we have a working connection. */ if ([self connect] == NO) { [arp release]; [NSException raise: SQLException format: @"Unable to connect to '%@' to run query %@", [self name], stmt]; } NS_DURING { EXEC SQL ALLOCATE DESCRIPTOR myDesc; EXEC SQL PREPARE myQuery from :query; if ([self isInTransaction] == NO) { EXEC SQL AT :handle BEGIN; localTransaction = YES; } EXEC SQL AT :handle DECLARE myCursor CURSOR FOR myQuery; EXEC SQL AT :handle OPEN myCursor; isOpen = YES; while (1) { EXEC SQL AT :handle FETCH IN myCursor INTO SQL DESCRIPTOR myDesc; if (sqlca.sqlcode) { break; } EXEC SQL GET DESCRIPTOR myDesc :count = COUNT; if (count > 0) { SQLRecord *record; id keys[count]; id values[count]; for (index = 1; index <= count; ++index) { id v; EXEC SQL GET DESCRIPTOR myDesc VALUE :index :indicator = INDICATOR, :length = LENGTH, :fieldName = NAME, :octetLength = OCTET_LENGTH, :precision = PRECISION, :returnedOctetLength = RETURNED_OCTET_LENGTH, :scale = SCALE, :type = TYPE; // printf("%s type:%d scale:%d\n", fieldName, type, scale); if (indicator == -1) { v = [NSNull null]; } else { /* * HACK ... for some reason date/time data seems to * get a negative type returned, so we check any * negative time to see if it is really date/time * and bodg the type code to fit. */ if (type < 0) { EXEC SQL GET DESCRIPTOR myDesc VALUE :index :dtiCode = DATETIME_INTERVAL_CODE; if (dtiCode != SQL3_DDT_ILLEGAL) { type = SQL3_DATE_TIME_TIMESTAMP; } } aString = 0; switch (type) { case SQL3_BOOLEAN: EXEC SQL GET DESCRIPTOR myDesc VALUE :index :aBool = DATA; if (aBool == 1) { v = [NSNumber numberWithBool: YES]; } else if (aBool == 0) { v = [NSNumber numberWithBool: NO]; } else { [NSException raise: NSGenericException format: @"Bad bool for '%s' - '%d'", fieldName, aBool]; } break; case SQL3_NUMERIC: case SQL3_DECIMAL: if (scale == 0) { EXEC SQL GET DESCRIPTOR myDesc VALUE :index :anInt = DATA; v = [NSNumber numberWithInt: anInt]; } else { EXEC SQL GET DESCRIPTOR myDesc VALUE :index :aFloat = DATA; v = [NSNumber numberWithFloat: aFloat]; } break; case SQL3_INTEGER: case SQL3_SMALLINT: EXEC SQL GET DESCRIPTOR myDesc VALUE :index :anInt = DATA; v = [NSNumber numberWithInt: anInt]; break; case SQL3_FLOAT: case SQL3_REAL: EXEC SQL GET DESCRIPTOR myDesc VALUE :index :aFloat = DATA; v = [NSNumber numberWithFloat: aFloat]; break; case SQL3_DOUBLE_PRECISION: EXEC SQL GET DESCRIPTOR myDesc VALUE :index :aDouble = DATA; v = [NSNumber numberWithDouble: aDouble]; break; case SQL3_DATE_TIME_TIMESTAMP: EXEC SQL GET DESCRIPTOR myDesc VALUE :index :dtiCode = DATETIME_INTERVAL_CODE, :aString = DATA; v = [self dbToDateFromBuffer: aString length: trim(aString)]; free(aString); break; case SQL3_INTERVAL: EXEC SQL GET DESCRIPTOR myDesc VALUE :index :aString = DATA; trim(aString); v = [NSString stringWithUTF8String: aString]; free(aString); break; case SQL3_CHARACTER: case SQL3_CHARACTER_VARYING: EXEC SQL GET DESCRIPTOR myDesc VALUE :index :aString = DATA; if (_shouldTrim) { trim(aString); } v = [NSString stringWithUTF8String: aString]; free(aString); break; case -17: /* * HACK ... BYTEA type determined by experiment. * who knows how/why this might change. */ EXEC SQL GET DESCRIPTOR myDesc VALUE :index :aString = DATA; v = [self dataFromBlob: aString]; free(aString); break; case -20: /* * HACK ... by experiment this seems to be an * integer returned by a function. */ EXEC SQL GET DESCRIPTOR myDesc VALUE :index :anInt = DATA; v = [NSNumber numberWithInt: anInt]; break; default: EXEC SQL GET DESCRIPTOR myDesc VALUE :index :aString = DATA; if (_shouldTrim) { trim(aString); } v = [NSString stringWithUTF8String: aString]; free(aString); if ([self debugging] > 0) { [self debug: @"Unknown data type (%d) for '%s' ... '%@'", type, fieldName, v]; } break; } } values[index-1] = v; keys[index-1] = [NSString stringWithUTF8String: fieldName]; } record = [rtype newWithValues: values keys: keys count: count]; [records addObject: record]; [record release]; } } isOpen = NO; EXEC SQL AT :handle CLOSE myCursor; if (localTransaction == YES) { EXEC SQL AT :handle COMMIT; localTransaction = NO; } EXEC SQL DEALLOCATE DESCRIPTOR myDesc; } NS_HANDLER { NSString *n = [localException name]; NSString *msg = [localException reason]; [records release]; records = nil; NS_DURING { if (isOpen == YES) { EXEC SQL AT :handle CLOSE myCursor; } if (localTransaction == YES) { EXEC SQL AT :handle ROLLBACK; } } NS_HANDLER { NSString *e = [localException name]; if ([e isEqual: SQLConnectionException] == YES) { [self disconnect]; } } NS_ENDHANDLER if ([n isEqual: SQLConnectionException] == YES) { [self disconnect]; } /* * remove line number information from database exception message * since it's meaningless to the developer as it's the line number * in this file rather than the code which is calling us. */ if ([n isEqual: SQLException] == YES || [n isEqual: SQLConnectionException] == YES) { NSRange r; r = [msg rangeOfString: @" in line " options: NSBackwardsSearch]; if (r.length > 0) { msg = [msg substringToIndex: r.location]; localException = [NSException exceptionWithName: n reason: msg userInfo: nil]; } } [localException retain]; [arp release]; [localException autorelease]; [localException raise]; } NS_ENDHANDLER [arp release]; 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; 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 if (c == '\'') { 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 = 2; unsigned int i; for (i = 0; i < sLen; i++) { unsigned char c = src[i]; if (c < 32 || c > 126 || c == ':') { length += 5; } else if (c == '\\') { length += 4; } else if (c == '\'') { length += 2; } else { length += 1; } } return length; } - (const char *) blobFromData: (NSData*)data { NSMutableData *md; unsigned sLen = [data length]; unsigned char *src = (unsigned char*)[data bytes]; unsigned dLen = 0; unsigned char *dst; unsigned i; for (i = 0; i < sLen; i++) { unsigned char c = src[i]; if (c < 32 || c > 126) { dLen += 4; } else if (c == 92) { dLen += 2; } else { dLen += 1; } } md = [NSMutableData dataWithLength: dLen + 1]; dst = (unsigned char*)[md mutableBytes]; dLen = 0; for (i = 0; i < sLen; i++) { unsigned char c = src[i]; if (c < 32 || c > 126) { dst[dLen] = '\\'; dst[dLen + 3] = (c & 7) + '0'; c >>= 3; dst[dLen + 2] = (c & 7) + '0'; c >>= 3; dst[dLen + 1] = (c & 7) + '0'; dLen += 4; } else if (c == 92) { dst[dLen++] = '\\'; dst[dLen++] = '\\'; } else { dst[dLen++] = c; } } dst[dLen] = '\0'; return (const char*)dst; // Owned by autoreleased NSMutableData } - (NSData *) dataFromBlob: (const char *)blob { NSMutableData *md; unsigned sLen = strlen(blob == 0 ? "" : blob); unsigned dLen = 0; unsigned char *dst; unsigned i; for (i = 0; i < sLen; i++) { unsigned c = blob[i]; dLen++; if (c == '\\') { c = blob[++i]; if (c != '\\') { i += 2; // Skip 2 digits octal } } } md = [NSMutableData dataWithLength: dLen]; dst = (unsigned char*)[md mutableBytes]; 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; } - (BOOL) dbFromDate: (NSDate*)d toBuffer: (char*)b length: (int)l { NSString *s; /* * Ensure we have a four digit year. */ if ([d timeIntervalSinceDate: future] > 0) { d = future; } s = [d descriptionWithCalendarFormat: @"%Y-%m-%d %H:%M:%S %z" timeZone: nil locale: nil]; return [self dbFromString: s toBuffer: b length: l]; } - (BOOL) dbFromString: (NSString*)s toBuffer: (char*)b length: (int)l { NSData *d; BOOL ok = YES; unsigned size = l; if (l <= 0) { [NSException raise: NSInvalidArgumentException format: @"-%@: length too small (%d)", NSStringFromSelector(_cmd), l]; } if (b == 0) { [NSException raise: NSInvalidArgumentException format: @"-%@: buffer is null", NSStringFromSelector(_cmd)]; } if (s == nil) { s = @""; } d = [s dataUsingEncoding: NSUTF8StringEncoding]; if (l < (int)[d length]) { /* * As the data is UTF8, we need to avoid truncating in the * middle of a multibyte character, so we shorten the * original string and reconvert to UTF8 until we find a * string that fits. */ if ((int)[s length] > l) { s = [s substringToIndex: l]; d = [s dataUsingEncoding: NSUTF8StringEncoding]; } while ((int)[d length] > l) { s = [s substringToIndex: [s length] - 1]; d = [s dataUsingEncoding: NSUTF8StringEncoding]; } ok = NO; } size = [d length]; memcpy(b, (const char*)[d bytes], size); /* * Pad with nuls and ensure there is a nul terminator. */ while ((int)size <= l) { b[size++] = '\0'; } return ok; } - (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; } - (NSString*) dbToStringFromBuffer: (char*)b length: (int)l { NSData *d; NSString *s; /* * Database fields are padded to the full field size with spaces or nuls ... * we need to remove that padding before placing in a string. */ while (l > 0 && b[l-1] <= ' ') { l--; } d = [[NSData alloc] initWithBytes: b length: l]; s = [[NSString alloc] initWithData: d encoding: NSUTF8StringEncoding]; [d release]; return [s autorelease]; } @end