From 2250d65fc0b886b0236d1b0dae1e771ac9ac5900 Mon Sep 17 00:00:00 2001 From: rfm Date: Wed, 11 Mar 2015 17:16:14 +0000 Subject: [PATCH] Add preliminary array support git-svn-id: svn+ssh://svn.gna.org/svn/gnustep/libs/sqlclient/trunk@38400 72102866-910b-0410-8b05-ffd578937521 --- ChangeLog | 9 ++ Postgres.m | 347 +++++++++++++++++++++++++++++++++++++++--------- SQLClient.h | 12 ++ SQLClient.m | 9 ++ SQLClientPool.m | 16 +-- testPostgres.m | 50 +++++-- 6 files changed, 362 insertions(+), 81 deletions(-) diff --git a/ChangeLog b/ChangeLog index 2c286f2..4e7c546 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,12 @@ +2015-03-11 Richard Frith-Macdonald + * SQLClientPool.m: Fixup for ewxposing prepare method + * SQLClient.h: + * SQLClient.m: + * Postgres.m: + * testPostgres.m: + Add simple array support for char/varchar/text, integer/real, + timestamp, bool and bytea. + 2015-03-02 Richard Frith-Macdonald * Postgres.h: Drop support for old versions of postgres which didn't diff --git a/Postgres.m b/Postgres.m index ecbd181..44efdb5 100644 --- a/Postgres.m +++ b/Postgres.m @@ -485,6 +485,221 @@ static unsigned int trim(char *str) return (str - start); } +- (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 + 1)) + { + v = [[NSString alloc] initWithUTF8String: start]; + } + 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 ('D' == t) + { + /* This is expected to be bytea data + */ + v = [[self dataFromBLOB: buf] retain]; + } + else + { + v = [NSString alloc]; + v = [v initWithBytesNoCopy: buf + length: len + encoding: NSUTF8StringEncoding + freeWhenDone: YES]; + } + } + } + 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); + if (strcmp(start, "NULL") == 0) + { + v = null; + } + else if ('T' == t) + { + v = [[self dbToDateFromBuffer: start length: len] retain]; + } + else if ('D' == t) + { + v = [[self dataFromBLOB: start] retain]; + } + else if ('B' == t) + { + if (*start == 't') + v = @"YES"; + else + v = @"NO"; + } + else + { + v = [[NSString alloc] initWithUTF8String: start]; + } + *p = save; + } + if (nil != v) + { + [a addObject: v]; + [v release]; + } + if (',' == *p) + { + p++; + } + } + if ('}' == *p) + { + p++; + } + return p; +} + +- (id) parseField: (char *)p type: (int)t +{ + char arrayType = 0; + + switch (t) + { + case 1082: // Date + return [self dbToDateFromBuffer: p length: trim(p)]; + + case 1083: // Time (treat as string) + trim(p); + return [NSString stringWithUTF8String: p]; + + case 1114: // Timestamp without time zone. + case 1184: // Timestamp with time zone. + return [self dbToDateFromBuffer: p length: trim(p)]; + + case 16: // BOOL + if (*p == 't') + { + return @"YES"; + } + else + { + return @"NO"; + } + + case 17: // BYTEA + return [self dataFromBLOB: p]; + + case 18: // "char" + return [NSString stringWithUTF8String: p]; + + case 20: // INT8 + case 21: // INT2 + case 23: // INT4 + trim(p); + return [NSString stringWithUTF8String: p]; + break; + + case 1182: // DATE ARRAY + 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 + case 1021: // FLOAT ARRAY + case 1022: // DOUBLE ARRAY + case 1002: // CHAR ARRAY + case 1009: // TEXT ARRAY + case 1015: // VARCHAR ARRAY + if ('{' == *p) + { + NSMutableArray *a; + + a = [NSMutableArray arrayWithCapacity: 10]; + p = [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) + { + trim(p); + } + return [NSString stringWithUTF8String: p]; + } +} + - (NSMutableArray*) backendQuery: (NSString*)stmt recordType: (id)rtype listType: (id)ltype @@ -542,17 +757,17 @@ static unsigned int trim(char *str) int recordCount = PQntuples(result); int fieldCount = PQnfields(result); NSString *keys[fieldCount]; - int types[fieldCount]; - int modifiers[fieldCount]; - int formats[fieldCount]; + int ftype[fieldCount]; + int fmod[fieldCount]; + int fformat[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); + ftype[i] = PQftype(result, i); + fmod[i] = PQfmod(result, i); + fformat[i] = PQfformat(result, i); } records = [[ltype alloc] initWithCapacity: recordCount]; @@ -574,69 +789,17 @@ static unsigned int trim(char *str) if ([self debugging] > 1) { [self debug: @"%@ type:%d mod:%d size: %d\n", - keys[j], types[j], modifiers[j], size]; + keys[j], ftype[j], fmod[j], size]; } - if (formats[j] == 0) // Text + if (fformat[j] == 0) // Text { - switch (types[j]) - { - case 1082: // Date - v = [self dbToDateFromBuffer: p - length: trim(p)]; - break; - - case 1083: // Time (treat as string) - trim(p); - v = [NSString stringWithUTF8String: p]; - break; - - 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; - - case 18: // "char" - v = [NSString stringWithUTF8String: p]; - break; - - case 20: // INT8 - case 21: // INT2 - case 23: // INT4 - trim(p); - v = [NSString stringWithUTF8String: p]; - break; - - case 25: // TEXT - default: - if (YES == _shouldTrim) - { - trim(p); - } - v = [NSString stringWithUTF8String: p]; - break; - } + v = [self parseField: p type: ftype[j]]; } else // Binary { NSLog(@"Binary data treated as NSNull " @"in %@ type:%d mod:%d size:%d\n", - keys[j], types[j], modifiers[j], size); + keys[j], ftype[j], fmod[j], size); } } values[j] = v; @@ -1012,6 +1175,66 @@ static unsigned int trim(char *str) [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"]; + } + 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; +} + - (NSString*) quoteString: (NSString *)s { NSData *d = [s dataUsingEncoding: NSUTF8StringEncoding]; diff --git a/SQLClient.h b/SQLClient.h index c761de2..f6f9b4c 100644 --- a/SQLClient.h +++ b/SQLClient.h @@ -796,6 +796,18 @@ SQLCLIENT_PRIVATE */ - (NSString*) quotef: (NSString*)fmt, ...; +/* Produce a quoted string from an array on databases where arrays are + * supported (currently only Postgres).
+ * If the s argument is not nil, the quoted array is appended to it rather + * than being produced in a new string (this method uses that feature to + * recursively quote nested arrays).
+ * The q argument determines whether string values found in the array + * are quoted or added literally. + */ +- (NSMutableString*) quoteArray: (NSArray *)a + toString: (NSMutableString *)s + quotingStrings: (BOOL)q; + /** * Convert a big (64 bit) integer to a string suitable for use in an SQL query. */ diff --git a/SQLClient.m b/SQLClient.m index 1114769..df1f937 100644 --- a/SQLClient.m +++ b/SQLClient.m @@ -1567,6 +1567,15 @@ static int poolConnections = 0; return quoted; } +- (NSMutableString*) quoteArray: (NSArray *)a + toString: (NSMutableString *)s + quotingStrings: (BOOL)q +{ + [NSException raise: NSGenericException + format: @"%@ not supported for this database", NSStringFromSelector(_cmd)]; + return nil; +} + - (NSString*) quoteBigInteger: (int64_t)i { return [NSString stringWithFormat: @"%"PRId64, i]; diff --git a/SQLClientPool.m b/SQLClientPool.m index 7544977..89783df 100644 --- a/SQLClientPool.m +++ b/SQLClientPool.m @@ -516,10 +516,6 @@ @end -@interface SQLClient (Private) -- (NSMutableArray*) _prepare: (NSString*)stmt args: (va_list)args; -@end - @implementation SQLClientPool (ConvenienceMethods) - (NSString*) buildQuery: (NSString*)stmt, ... @@ -532,7 +528,7 @@ * First check validity and concatenate parts of the query. */ va_start (ap, stmt); - sql = [[db _prepare: stmt args: ap] objectAtIndex: 0]; + sql = [[db prepare: stmt args: ap] objectAtIndex: 0]; va_end (ap); [self swallowClient: db]; @@ -556,7 +552,7 @@ va_list ap; va_start (ap, stmt); - stmt = [[db _prepare: stmt args: ap] objectAtIndex: 0]; + stmt = [[db prepare: stmt args: ap] objectAtIndex: 0]; va_end (ap); result = [db cache: seconds simpleQuery: stmt]; [self swallowClient: db]; @@ -612,7 +608,7 @@ va_list ap; va_start (ap, stmt); - info = [db _prepare: stmt args: ap]; + info = [db prepare: stmt args: ap]; va_end (ap); result = [db simpleExecute: info]; [self swallowClient: db]; @@ -638,7 +634,7 @@ * First check validity and concatenate parts of the query. */ va_start (ap, stmt); - stmt = [[db _prepare: stmt args: ap] objectAtIndex: 0]; + stmt = [[db prepare: stmt args: ap] objectAtIndex: 0]; va_end (ap); result = [db simpleQuery: stmt]; [self swallowClient: db]; @@ -663,7 +659,7 @@ va_list ap; va_start (ap, stmt); - stmt = [[db _prepare: stmt args: ap] objectAtIndex: 0]; + stmt = [[db prepare: stmt args: ap] objectAtIndex: 0]; va_end (ap); result = [db simpleQuery: stmt]; [self swallowClient: db]; @@ -690,7 +686,7 @@ va_list ap; va_start (ap, stmt); - stmt = [[db _prepare: stmt args: ap] objectAtIndex: 0]; + stmt = [[db prepare: stmt args: ap] objectAtIndex: 0]; va_end (ap); result = [db simpleQuery: stmt]; [self swallowClient: db]; diff --git a/testPostgres.m b/testPostgres.m index 8f21d7d..6da17f8 100644 --- a/testPostgres.m +++ b/testPostgres.m @@ -120,7 +120,7 @@ main() @"Delivery TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, " @"Reference CHAR(128), " @"Destination CHAR(15) NOT NULL, " - @"Payload CHAR(250) DEFAULT '' NOT NULL" + @"Payload CHAR(250) DEFAULT '' NOT NULL," @")", nil]; [db execute: @@ -155,8 +155,11 @@ main() } [db execute: @"INSERT INTO Queue (Consumer, Destination," @" ServiceID, Payload) VALUES (", - [db quote: name], @", ", [db quote: destination], @", ", sid, @", ", - @"'helo there'", @")", nil]; + [db quote: name], @", ", + [db quote: destination], @", ", + sid, @", ", + @"'helo there'", + @")", nil]; [arp release]; } NSLog(@"End producing"); @@ -252,25 +255,54 @@ main() @"intval int, " @"when1 timestamp with time zone, " @"when2 timestamp, " - @"b bytea" + @"b bytea," + @"extra1 int[]," + @"extra2 varchar[]," + @"extra3 bytea[]," + @"extra4 boolean[]," + @"extra5 timestamp[]" @")", nil]; - if (1 != [db execute: @"insert into xxx " - @"(k, char1, boolval, intval, when1, when2, b) " + if (1 != [db execute: @"insert into xxx (k, char1, boolval, intval," + @" when1, when2, b, extra1, extra2, extra3, extra4, extra5) " @"values (" - @"'hello', " + @"'{hello', " @"'X', " @"TRUE, " @"1, " @"CURRENT_TIMESTAMP, " @"CURRENT_TIMESTAMP, ", - data, - @")", + data, @", ", + [db quoteArray: + [NSArray arrayWithObjects: @"1", @"2", [NSNull null], nil] + toString: nil + quotingStrings: NO], @", ", + [db quoteArray: + [NSArray arrayWithObjects: @"on,e", @"t'wo", @"many", nil] + toString: nil + quotingStrings: YES], @", ", + [db quoteArray: + [NSArray arrayWithObjects: data, nil] + toString: nil + quotingStrings: YES], @", ", + [db quoteArray: + [NSArray arrayWithObjects: @"TRUE", @"FALSE", nil] + toString: nil + quotingStrings: NO], @", ", + [db quoteArray: + [NSArray arrayWithObjects: [NSDate date], nil] + toString: nil + quotingStrings: YES], @")", nil]) { NSLog(@"Insert failed to return row count"); } + +[db setDebugging: 9]; +[db query: @"select * from xxx", nil]; +[db setDebugging: 0]; + [db execute: @"insert into xxx " @"(k, char1, boolval, intval, when1, when2, b) " @"values ("