diff --git a/ChangeLog b/ChangeLog index 0ea3bdb..06e1aca 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,7 +1,14 @@ +2007-07-09 Richard Frith-Macdonald + + * SQLClient.m: Post notifications upon connect and disconnect. + 2007-07-07 Richard Frith-Macdonald * SQLClient.m: Fix error causing loss of some debug output when an exception occurs in a transaction. + Rewrite transaction code to support execution with automatic retry of + statements when batching. + * JDBC.m: Update for new transaction code 2007-04-01 Richard Frith-Macdonald diff --git a/JDBC.m b/JDBC.m index 253006b..6f56611 100644 --- a/JDBC.m +++ b/JDBC.m @@ -1675,64 +1675,39 @@ static int JDBCVARCHAR = 0; @implementation _JDBCTransaction -// Marker for the end of data owned by a statement -static id marker = @"End of statement data"; - -- (NSString*) description +- (BOOL) _batchable: (NSArray*)a { - return [NSString stringWithFormat: @"%@ with SQL '%@' for %@", - [super description], - (_count == 0 ? (id)@"" : (id)[_info objectAtIndex: 0]), _db]; + unsigned c = [a count]; + unsigned i; + + for (i = 0; i < c; i++) + { + if ([[a objectAtIndex: i] count] > 1) + { + return NO; + } + } + return YES; } -- (void) _addInfo: (NSArray*)info +- (void) _merge: (NSMutableArray*)a { - if (_count == 0) - { - id o = [info objectAtIndex: 0]; - NSMutableArray *ma; + unsigned c = [_info count]; + unsigned i; - if ([o isKindOfClass: [NSString class]] == YES) + for (i = 0; i < c; i++) + { + id o = [_info objectAtIndex: i]; + + if ([o isKindOfClass: [NSArray class]] == YES) { - ma = [[NSMutableArray alloc] initWithObjects: &o count: 1]; - } + [a addObject: o]; + } else { - ma = [(NSArray*)o mutableCopy]; - } - [_info addObjectsFromArray: info]; - [_info replaceObjectAtIndex: 0 withObject: ma]; - RELEASE(ma); + [(_JDBCTransaction*)o _merge: a]; + } } - else - { - unsigned c = [info count]; - unsigned i = 1; - id o = [info objectAtIndex: 0]; - NSMutableArray *ma = [_info objectAtIndex: 0]; - - if ([o isKindOfClass: [NSString class]] == YES) - { - [ma addObject: (NSString*)o]; - } - else - { - [ma addObjectsFromArray: (NSArray*)o]; - } - while (i < c) - { - [_info addObject: [info objectAtIndex: i++]]; - } - } - - /* If the info item being added is a simple statement rather than the - * content of another transaction, we must add an end-of-statement marker. - */ - if ([info lastObject] != marker) - { - [_info addObject: marker]; - } - _count++; } - (void) execute @@ -1768,13 +1743,16 @@ static id marker = @"End of statement data"; NS_DURING { - NSMutableArray *statements = [_info objectAtIndex: 0]; - unsigned numberOfStatements = [statements count]; + NSMutableArray *statements; + unsigned numberOfStatements; unsigned statement; - unsigned pos = 1; NSTimeInterval _duration = [_db durationLogging]; NSTimeInterval start = 0.0; + statements = [NSMutableArray arrayWithCapacity: 100]; + [self _merge: statements]; + numberOfStatements = [statements count]; + if (_duration >= 0) { start = GSTickerTimeNow(); @@ -1786,15 +1764,12 @@ static id marker = @"End of statement data"; } if (numberOfStatements > 1 && ji->addBatch != 0 - && [_info count] == numberOfStatements + 1) + && [self _batchable: statements] == YES) { jintArray ja; jint *array; int status = 0; - /* We have multiple statements without arguments ... so this - * is batchable. - */ for (statement = 0; statement < numberOfStatements; statement++) { NSString *stmt = [statements objectAtIndex: statement]; @@ -1837,20 +1812,20 @@ static id marker = @"End of statement data"; */ for (statement = 0; statement < numberOfStatements; statement++) { - NSString *stmt = [statements objectAtIndex: statement]; + NSArray *info = [statements objectAtIndex: statement]; + NSString *stmt = [info objectAtIndex: 0]; + unsigned c = [info count]; jmethodID jm; jobject js; - if ([_info objectAtIndex: pos] == marker) + if (c == 1) { - pos++; // Step past end of statement data. (*env)->CallIntMethod (env, ji->statement, ji->executeUpdate, JStringFromNSString(env, stmt)); } else { - NSData *data; - int i = 1; + unsigned i; jclass jc; stmt = [stmt stringByReplacingString: @"'?'''?'" @@ -1868,9 +1843,12 @@ static id marker = @"End of statement data"; /* Get data arguments for statement. */ - while ((data = [_info objectAtIndex: pos++]) != marker) - { - (*env)->CallIntMethod (env, js, jm, i++, + for (i = 1; i < c; i++) + { + NSData *data; + + data = [info objectAtIndex: i]; + (*env)->CallIntMethod (env, js, jm, i, ByteArrayFromNSData(env, data)); JException(env); } diff --git a/SQLClient.h b/SQLClient.h index 45a7e37..8ef172e 100644 --- a/SQLClient.h +++ b/SQLClient.h @@ -77,6 +77,10 @@ Support for standalone web applications ... eg to allow data to be added to the database by people posting web forms to the application. + + Supports notification of connection to and disconnection from the + database server. +
@@ -187,6 +191,18 @@ @class NSString; @class SQLTransaction; +/** + * Notification sent when an instance becomes connected to the database + * server. The notification object is the instance connected. + */ +NSString *SQLClientDidConnectNotification; + +/** + * Notification sent when an instance becomes disconnected from the database + * server. The notification object is the instance disconnected. + */ +NSString *SQLClientDidDisconnectNotification; + /** *

An enhanced array to represent a record returned from a query. * You should NOT try to create instances of this class @@ -971,6 +987,16 @@ extern unsigned SQLClientTimeTick(); */ @interface SQLClient(Convenience) +/** + * Returns a transaction object configured to handle batching and + * execute part of a batch of statements if execution of the whole + * fails.
+ * If stopOnFailure is YES than execution of the transaction will + * stop with the first statement to fail, otherwise it will execute + * all the statements it can, skipping any failued statements. + */ +- (SQLTransaction*) batch: (BOOL)stopOnFailure; + /** * Executes a query (like the -query:,... method) and checks the result * (raising an exception if the query did not contain a single record) @@ -998,6 +1024,7 @@ extern unsigned SQLClientTimeTick(); * use the receiver as the database connection to perform transactions. */ - (SQLTransaction*) transaction; + @end @@ -1154,11 +1181,13 @@ extern unsigned SQLClientTimeTick(); * database operations should be. If you have multiple threads, you * should create multiple SQLTransaction instances, at least one per thread. */ -@interface SQLTransaction : NSObject +@interface SQLTransaction : NSObject { SQLClient *_db; NSMutableArray *_info; unsigned _count; + BOOL _batch; + BOOL _stop; } /** @@ -1176,7 +1205,7 @@ extern unsigned SQLClientTimeTick(); - (void) add: (NSString*)stmt with: (NSDictionary*)values; /** - * Appends all the statements from the other transaction to the receiver.
+ * Appends a copy of the other transaction to the receiver.
* This provides a convenient way of merging transactions which have been * built by different code modules, in order to have them all executed * together in a single operation (for efficiency etc).
@@ -1188,6 +1217,11 @@ extern unsigned SQLClientTimeTick(); */ - (void) append: (SQLTransaction*)other; +/** + * Make a copy of the receiver. + */ +- (id) copyWithZone: (NSZone*)z; + /** * Returns the number of statements in this transaction. */ @@ -1218,6 +1252,24 @@ extern unsigned SQLClientTimeTick(); */ - (void) execute; +/** + *

This is similar to the -execute method, but may allow partial + * execution of the transaction if appropriate: + *

+ *

If the transaction was created using the [SQLClient-batch:] method and + * the transaction as a whole fails, individual statements are retried.
+ * The stopOnFailure flag for the batch creation indicates whether the + * retries are stopped at the first statement to fail, or continue (skipping + * any failed statements). + *

+ *

If the transaction has had transactions appended to it, those + * subsidiary transactions may succeed or fail atomically depending + * on their individual attributes. + *

+ * The method returns the number of statements which actually succeeded. + */ +- (unsigned) executeBatch; + /** * Resets the transaction, removing all previously added statements. * This allows the transaction object to be re-used for multiple diff --git a/SQLClient.m b/SQLClient.m index e2f2d2a..b313d5d 100644 --- a/SQLClient.m +++ b/SQLClient.m @@ -63,6 +63,12 @@ #define SUBCLASS_RESPONSIBILITY #endif +NSString *SQLClientDidConnectNotification + = @"SQLClientDidConnectNotification"; + +NSString *SQLClientDidDisconnectNotification + = @"SQLClientDidDisconnectNotification"; + static NSNull *null = nil; static Class NSStringClass = 0; static Class NSArrayClass = 0; @@ -1110,6 +1116,12 @@ static unsigned int maxConnections = 8; NS_ENDHANDLER } [lock unlock]; + if (connected == YES) + { + [[NSNotificationCenter defaultCenter] + postNotificationName: SQLClientDidConnectNotification + object: self]; + } } return connected; } @@ -1190,6 +1202,9 @@ static unsigned int maxConnections = 8; NS_ENDHANDLER } [lock unlock]; + [[NSNotificationCenter defaultCenter] + postNotificationName: SQLClientDidDisconnectNotification + object: self]; } } @@ -1659,6 +1674,7 @@ static unsigned int maxConnections = 8; [lock lock]; statement = [info objectAtIndex: 0]; + if ([statement isEqualToString: commitString]) { isCommit = YES; @@ -2292,6 +2308,20 @@ static unsigned int maxConnections = 8; @implementation SQLClient(Convenience) +- (SQLTransaction*) batch: (BOOL)stopOnFailure +{ + TDefs transaction; + + transaction = (TDefs)NSAllocateObject([SQLTransaction class], 0, + NSDefaultMallocZone()); + + transaction->_db = RETAIN(self); + transaction->_info = [NSMutableArray new]; + transaction->_batch = YES; + transaction->_stop = stopOnFailure; + return AUTORELEASE((SQLTransaction*)transaction); +} + - (SQLRecord*) queryRecord: (NSString*)stmt, ... { va_list ap; @@ -2539,101 +2569,209 @@ static unsigned int maxConnections = 8; (_count == 0 ? (id)@"" : (id)[_info objectAtIndex: 0]), _db]; } -- (void) _addInfo: (NSArray*)info -{ - if (_count == 0) - { - NSMutableString *ms = [[info objectAtIndex: 0] mutableCopy]; - - [_info addObjectsFromArray: info]; - [_info replaceObjectAtIndex: 0 withObject: ms]; - RELEASE(ms); - } - else - { - NSMutableString *ms = [_info objectAtIndex: 0]; - unsigned c = [info count]; - unsigned i = 1; - - [ms appendString: @";"]; - [ms appendString: [info objectAtIndex: 0]]; - while (i < c) - { - [_info addObject: [info objectAtIndex: i++]]; - } - } - _count++; -} - - (void) add: (NSString*)stmt,... { va_list ap; va_start (ap, stmt); - [self _addInfo: [_db _prepare: stmt args: ap]]; + [_info addObject: [_db _prepare: stmt args: ap]]; + _count++; va_end (ap); } - (void) add: (NSString*)stmt with: (NSDictionary*)values { - [self _addInfo: [_db _substitute: stmt with: values]]; + [_info addObject: [_db _substitute: stmt with: values]]; + _count++; } - (void) append: (SQLTransaction*)other { if (other != nil && other->_count > 0) { - [self _addInfo: other->_info]; + other = [other copy]; + [_info addObject: other]; + _count += other->_count; + RELEASE(other); } } +- (id) copyWithZone: (NSZone*)z +{ + SQLTransaction *c; + + c = (SQLTransaction*)NSCopyObject(self, 0, z); + c->_info = [c->_info mutableCopy]; + return c; +} + - (SQLClient*) db { return _db; } +- (void) _addSQL: (NSMutableString*)sql andArgs: (NSMutableArray*)args +{ + unsigned count = [_info count]; + unsigned index; + + for (index = 0; index < count; index++) + { + id o = [_info objectAtIndex: index]; + + if ([o isKindOfClass: NSArrayClass] == YES) + { + unsigned c = [(NSArray*)o count]; + + if (c > 0) + { + unsigned i; + + [sql appendString: [(NSArray*)o objectAtIndex: 0]]; + [sql appendString: @";"]; + for (i = 1; i < c; i++) + { + [args addObject: [(NSArray*)o objectAtIndex: i]]; + } + } + } + else + { + [(SQLTransaction*)o _addSQL: sql andArgs: args]; + } + } +} + +- (void) _countLength: (unsigned*)length andArgs: (unsigned*)args +{ + unsigned count = [_info count]; + unsigned index; + + for (index = 0; index < count; index++) + { + id o = [_info objectAtIndex: index]; + + if ([o isKindOfClass: NSArrayClass] == YES) + { + unsigned c = [(NSArray*)o count]; + + if (c > 0) + { + length += [[(NSArray*)o objectAtIndex: 0] length] + 1; + args += c - 1; + } + } + else + { + [(SQLTransaction*)o _countLength: length andArgs: args]; + } + } +} - (void) execute { if (_count > 0) { - BOOL wrapped = NO; - NSMutableString *sql = [_info objectAtIndex: 0]; + NSMutableArray *info = nil; NS_DURING { - if (_count > 1 && [_db isInTransaction] == NO) - { - wrapped = YES; - [sql replaceCharactersInRange: NSMakeRange(0, 0) - withString: @"begin;"]; - [sql replaceCharactersInRange: NSMakeRange([sql length], 0) - withString: @";commit"]; - } - [_db simpleExecute: _info]; - if (wrapped == YES) - { - wrapped = NO; - [sql replaceCharactersInRange: NSMakeRange([sql length] - 7, 7) - withString: @""]; - [sql replaceCharactersInRange: NSMakeRange(0, 6) - withString: @""]; - } + NSMutableString *sql; + unsigned sqlSize = 0; + unsigned argCount = 0; + + [self _countLength: &sqlSize andArgs: &argCount]; + + /* Allocate and initialise the transaction statement. + */ + info = [[NSMutableArray alloc] initWithCapacity: argCount + 1]; + sql = [[NSMutableString alloc] initWithCapacity: sqlSize + 13]; + [info addObject: sql]; + RELEASE(sql); + if ([_db isInTransaction] == NO) + { + [sql appendString: @"begin;"]; + } + + [self _addSQL: sql andArgs: info]; + + if ([_db isInTransaction] == NO) + { + [sql appendString: @"commit;"]; + } + + [_db simpleExecute: info]; } NS_HANDLER { - if (wrapped == YES) - { - [sql replaceCharactersInRange: NSMakeRange([sql length] - 7, 7) - withString: @""]; - [sql replaceCharactersInRange: NSMakeRange(0, 6) - withString: @""]; - } + RELEASE(info); [localException raise]; } NS_ENDHANDLER } } + +- (unsigned) executeBatch +{ + unsigned executed = 0; + + if (_count > 0) + { + NS_DURING + { + [self execute]; + executed = _count; + } + NS_HANDLER + { + if (_batch == YES) + { + unsigned count = [_info count]; + unsigned i; + + for (i = 0; i < count; i++) + { + BOOL success = NO; + + NS_DURING + { + id o = [_info objectAtIndex: i]; + + if ([o isKindOfClass: NSArrayClass] == YES) + { + [_db simpleExecute: (NSArray*)o]; + executed++; + success = YES; + } + else + { + unsigned result; + + result = [(SQLTransaction*)o executeBatch]; + executed += result; + if (result == [(SQLTransaction*)o count]) + { + success = YES; + } + } + } + NS_HANDLER + { + success = NO; + } + NS_ENDHANDLER + if (success == NO && _stop == YES) + { + break; + } + } + } + } + NS_ENDHANDLER + } + return executed; +} + - (void) reset { [_info removeAllObjects];