From cc3c436432e592d15c46a4a9e38c6cb689eca0ee Mon Sep 17 00:00:00 2001 From: rowanbeentje Date: Mon, 20 Feb 2012 00:51:47 +0000 Subject: Initial commit of the new SPMySQL Framework, which is added to the project and ready for use but not yet integrated. This new framework should provide much of the functionality required from MCPKit and is based around its interface for relatively easy integration. The largest missing component is Hans' structure code which I believe is better placed outside the framework. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From the Readme file: The SPMySQL Framework is intended to provide a stable MySQL connection framework, with the ability to run text-based queries and rapidly retrieve result sets with conversion from MySQL data types to Cocoa objects. SPMySQL.framework has an interface loosely based around that provided by MCPKit by Serge Cohen and Bertrand Mansion (http://mysql-cocoa.sourceforge.net/), and in particular the heavily modified Sequel Pro version (http://www.sequelpro.com/). It is a full rewrite of the original framework, although it includes code from patches implementing the following Sequel Pro functionality, largely contributed by Hans-Jörg Bibiko, Stuart Connolly, Jakob Egger, and Rowan Beentje: - Connection locking (Jakob et al) - Ping & keepalive (Rowan et al) - Query cancellation (Rowan et al) - Delegate setup (Stuart et al) - SSL support (Rowan et al) - Connection checking (Rowan et al) - Version state (Stuart et al) - Maximum packet size control (Hans et al) - Result multithreading and streaming (Rowan et al) - Improved encoding support & switching (Rowan et al) - Database structure; moved to inside the app (Hans et al) - Query reattempts and error-handling approach (Rowan et al) - Geometry result class (Hans et al) - Connection proxy (Stuart et al) --- Frameworks/SPMySQLFramework/Source/SPMySQLResult.m | 459 +++++++++++++++++++++ 1 file changed, 459 insertions(+) create mode 100644 Frameworks/SPMySQLFramework/Source/SPMySQLResult.m (limited to 'Frameworks/SPMySQLFramework/Source/SPMySQLResult.m') diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLResult.m b/Frameworks/SPMySQLFramework/Source/SPMySQLResult.m new file mode 100644 index 00000000..b110958d --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLResult.m @@ -0,0 +1,459 @@ +// +// $Id$ +// +// SPMySQLResult.m +// SPMySQLFramework +// +// Created by Rowan Beentje (rowan.beent.je) on January 26, 2012 +// Copyright (c) 2012 Rowan Beentje. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at + +#import "SPMySQLResult.h" +#import "SPMySQL Private APIs.h" + +static SPMySQLResultFieldProcessor fieldProcessingMap[256]; + +@implementation SPMySQLResult + +#pragma mark - +#pragma mark Synthesized properties + +@synthesize returnDataAsStrings; +@synthesize defaultRowReturnType; + +#pragma mark - +#pragma mark Setup and teardown + +/** + * In the one-off class initialisation, set up the result processing map + */ ++ (void)initialize +{ + + // Go through the list of enum_field_types in mysql_com.h, mapping each to the method for + // processing that result set. + fieldProcessingMap[MYSQL_TYPE_DECIMAL] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_TINY] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_SHORT] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_LONG] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_FLOAT] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_DOUBLE] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_NULL] = SPMySQLResultFieldAsNull; + fieldProcessingMap[MYSQL_TYPE_TIMESTAMP] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_LONGLONG] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_INT24] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_DATE] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_TIME] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_DATETIME] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_YEAR] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_NEWDATE] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_VARCHAR] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_BIT] = SPMySQLResultFieldAsBit; + fieldProcessingMap[MYSQL_TYPE_NEWDECIMAL] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_ENUM] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_SET] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_TINY_BLOB] = SPMySQLResultFieldAsBlob; + fieldProcessingMap[MYSQL_TYPE_MEDIUM_BLOB] = SPMySQLResultFieldAsBlob; + fieldProcessingMap[MYSQL_TYPE_LONG_BLOB] = SPMySQLResultFieldAsBlob; + fieldProcessingMap[MYSQL_TYPE_BLOB] = SPMySQLResultFieldAsBlob; + fieldProcessingMap[MYSQL_TYPE_VAR_STRING] = SPMySQLResultFieldAsStringOrBlob; + fieldProcessingMap[MYSQL_TYPE_STRING] = SPMySQLResultFieldAsStringOrBlob; + fieldProcessingMap[MYSQL_TYPE_GEOMETRY] = SPMySQLResultFieldAsGeometry; + fieldProcessingMap[MYSQL_TYPE_DECIMAL] = SPMySQLResultFieldAsString; +} + +/** + * Prevent SPMySQLResults from being init'd normally. + */ +- (id)init +{ + [NSException raise:NSInternalInconsistencyException format:@"SPMySQLResults should not be init'd directly; use initWithMySQLResult:stringEncoding: instead."]; + return nil; +} + +/** + * Standard init method, constructing the SPMySQLResult around a MySQL + * result pointer and the encoding to use when working with the data. + */ +- (id)initWithMySQLResult:(void *)theResult stringEncoding:(NSStringEncoding)theStringEncoding +{ + + // If no result set was passed in, return nil. + if (!theResult) return nil; + + if ((self = [super init])) { + stringEncoding = theStringEncoding; + queryExecutionTime = -1; + + // Get the result set and cache the number of fields and number of rows + resultSet = theResult; + numberOfFields = mysql_num_fields(resultSet); + numberOfRows = mysql_num_rows(resultSet); + currentRowIndex = 0; + + // Cache the field definitions and build up an array of cached field names and types + fieldDefinitions = mysql_fetch_fields(resultSet); + fieldNames = malloc(sizeof(NSString *) * numberOfFields); + fieldTypes = malloc(sizeof(unsigned int) * numberOfFields); + for (NSUInteger i = 0; i < numberOfFields; i++) { + MYSQL_FIELD aField = fieldDefinitions[i]; + fieldNames[i] = [[self _stringWithBytes:aField.name length:aField.name_length] retain]; + fieldTypes[i] = aField.type; + } + + defaultRowReturnType = SPMySQLResultRowAsDictionary; + } + + return self; +} + +- (void)dealloc +{ + mysql_free_result(resultSet); + + for (NSUInteger i = 0; i < numberOfFields; i++) { + [fieldNames[i] release]; + } + free(fieldNames); + free(fieldTypes); + + [super dealloc]; +} + +#pragma mark - +#pragma mark Result set information + +/** + * Return the number of fields in the result set. + */ +- (NSUInteger)numberOfFields +{ + return numberOfFields; +} + +/** + * Return the number of data rows in the result set. + */ +- (unsigned long long)numberOfRows +{ + return numberOfRows; +} + +/** + * Return how long the original query took to execute - including connection lag! + */ +- (double)queryExecutionTime +{ + return queryExecutionTime; +} + +#pragma mark - +#pragma mark Column information + +/** + * Retrieve the field names for the result set, as an NSArray of NSStrings. + */ +- (NSArray *)fieldNames +{ + return [NSArray arrayWithObjects:fieldNames count:numberOfFields]; +} + +/** + * For field definitions, see Result Categories/Field Definitions.h/m + */ + +#pragma mark - +#pragma mark Data retrieval + +/** + * Jump to a specified row in the result set; when the result set is initialised, + * the internal pointer automatically starts at 0. + */ +- (void)seekToRow:(unsigned long long)targetRow +{ + if (targetRow == currentRowIndex) return; + + if (targetRow >= numberOfRows) { + targetRow = numberOfRows - 1; + } + + mysql_data_seek(resultSet, targetRow); + currentRowIndex = targetRow; +} + +/** + * Retrieve the next row in the result set, using the internal pointer, in the + * instance-specified setDefaultRowReturnType: row format (defaulting to NSDictionary). + * If there are no rows remaining, returns nil. + */ +- (id)getRow +{ + return SPMySQLResultGetRow(self, SPMySQLResultRowAsDefault); +} + +/** + * Retrieve the next row in the result set, using the internal pointer, in the + * instance-specified setDefaultRowReturnType: row format (defaulting to NSDictionary). + * If there are no rows remaining, returns nil. + */ +- (NSArray *)getRowAsArray +{ + return SPMySQLResultGetRow(self, SPMySQLResultRowAsArray); +} + +/** + * Retrieve the next row in the result set, using the internal pointer, in the + * instance-specified setDefaultRowReturnType: row format (defaulting to NSDictionary). + * If there are no rows remaining, returns nil. + */ +- (NSDictionary *)getRowAsDictionary +{ + return SPMySQLResultGetRow(self, SPMySQLResultRowAsDictionary); +} + +/** + * Retrieve the next row in the result set, using the internal pointer, in the specified + * return format. + * If there are no rows remaining in the current iteration, returns nil. + */ +- (id)getRowAsType:(SPMySQLResultRowType)theType +{ + MYSQL_ROW theRow; + unsigned long *theRowDataLengths; + id theReturnData; + + // Retrieve the row in MySQL format, and the length of the data within the row + theRow = mysql_fetch_row(resultSet); + theRowDataLengths = mysql_fetch_lengths(resultSet); + + // If no row was returned, likely at the end of the result set - return nil + if (!theRow) return nil; + + // If the target type was unspecified, use the instance default + if (theType == SPMySQLResultRowAsDefault) theType = defaultRowReturnType; + + // Set up the return data as appropriate + if (theType == SPMySQLResultRowAsArray) { + theReturnData = [NSMutableArray arrayWithCapacity:numberOfFields]; + } else { + theReturnData = [NSMutableDictionary dictionaryWithCapacity:numberOfFields]; + } + + // Convert each of the cells in the row in turn + for (NSUInteger i = 0; i < numberOfFields; i++) { + id cellData = SPMySQLResultGetObject(self, theRow[i], theRowDataLengths[i], fieldTypes[i], i); + + // If object creation failed, display a null + if (!cellData) cellData = [NSNull null]; + + // Add to the result array/dictionary + if (theType == SPMySQLResultRowAsArray) { + [(NSMutableArray *)theReturnData addObject:cellData]; + } else { + [(NSMutableDictionary *)theReturnData setObject:cellData forKey:fieldNames[i]]; + } + } + + // Increment the row pointer index and set to NSNotFound if the end of the result set has + // been reached + currentRowIndex++; + if (currentRowIndex > numberOfRows) currentRowIndex = NSNotFound; + + return theReturnData; +} + +#pragma mark - +#pragma mark Data retrieval for fast enumeration + +/** + * Implement the fast enumeration endpoint. Rows for fast enumeration are retrieved in + * the instance default, as specified in setDefaultRowReturnType: or defaulting to + * NSDictionary. + */ +- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len +{ + + // If the start index is out of bounds, return 0 to indicate end of results + if (state->state >= numberOfRows) return 0; + + // Sync up the MySQL pointer position with the requested state if necessary + if (state->state != currentRowIndex) [self seekToRow:state->state]; + + // Determine how many objects to return - 128, len, or all items remaining + NSUInteger itemsToReturn = 128; + if (len < 128) itemsToReturn = len; + if (numberOfRows - state->state < itemsToReturn) { + itemsToReturn = (unsigned long)(numberOfRows - state->state); + } + + // Loop through the rows and add them to the result stack + NSUInteger i; + for (i = 0; i < itemsToReturn; i++) { + stackbuf[i] = SPMySQLResultGetRow(self, SPMySQLResultRowAsDefault); + } + + state->state += itemsToReturn; + state->itemsPtr = stackbuf; + state->mutationsPtr = (unsigned long *)self; + + return itemsToReturn; +} + +#pragma mark - +#pragma mark Data conversion + +/** + * Provides a binary representation of the supplied bytes as a returned NSString. + * The resulting binary representation will be zero-padded according to the supplied + * field length. + */ ++ (NSString *)bitStringWithBytes:(const char *)bytes length:(NSUInteger)length padToLength:(NSUInteger)padLength +{ + if (bytes == NULL) return nil; + + NSUInteger i = 0; + length--; + padLength--; + + // Generate a C string representation of the binary data + char *cStringBuffer = malloc(length + 1); + while (i <= padLength) { + cStringBuffer[padLength - i++] = ( (bytes[length - (i >> 3)] >> (i & 0x7)) & 1 ) ? '1' : '0'; + } + cStringBuffer[padLength+1] = '\0'; + + // Convert to a string + NSString *returnString = [NSString stringWithUTF8String:cStringBuffer]; + + // Free up memory and return + free(cStringBuffer); + return returnString; +} + +@end + +#pragma mark - +#pragma mark Result set internals + +@implementation SPMySQLResult (Private_API) + +/** + * Support internal string conversions which take a supplied byte sequence and length + * and convert them to an NSString using the instance encoding. Will preserve nul + * characters within the string. + */ +- (id)_stringWithBytes:(const void *)bytes length:(NSUInteger)length +{ + return [[[NSString alloc] initWithBytes:bytes length:length encoding:stringEncoding] autorelease]; +} + +/** + * Allow setting the execution time for the original query (including connection lag) + * so it can be requested later without relying on connection state. + */ +- (void)_setQueryExecutionTime:(double)theExecutionTime +{ + queryExecutionTime = theExecutionTime; +} + +/** + * Core data conversion function, taking C data provided by MySQL and converting + * to an appropriate return type. + * Note that the data passed in currently is *not* nul-terminated for fast + * streaming results, which is safe for the current implementation but should be + * kept in mind for future changes. + */ +- (id)_getObjectFromBytes:(char *)bytes ofLength:(NSUInteger)length fieldType:(unsigned int)fieldType fieldDefinitionIndex:(NSUInteger)fieldIndex +{ + + // A NULL pointer for the data indicates a null value; return a NSNull object. + if (bytes == NULL) return [NSNull null]; + + // Determine the field processor to use + SPMySQLResultFieldProcessor dataProcessor = fieldProcessingMap[fieldType]; + + // Switch the method to process the cell data based on the field type mapping. + // Do this in two passes: the first as logic may cause a change in processor required. + switch (dataProcessor) { + + // STRING and VAR_STRING types may be strings or binary types; check the binary flag + case SPMySQLResultFieldAsStringOrBlob: + if (fieldDefinitions[fieldIndex].flags & BINARY_FLAG) { + dataProcessor = SPMySQLResultFieldAsBlob; + } + break; + + // Blob types may be automatically be converted to strings, or may be non-binary + case SPMySQLResultFieldAsBlob: + if (!(fieldDefinitions[fieldIndex].flags & BINARY_FLAG)) { + dataProcessor = SPMySQLResultFieldAsString; + } + break; + + // In most cases, use the original data processor. + default: + break; + } + + // If this instance is set to convert all data as strings, alter the processor. + if (returnDataAsStrings && dataProcessor == SPMySQLResultFieldAsBlob) { + dataProcessor = SPMySQLResultFieldAsString; + } + + // Now switch the processing method again to actually process the data. + switch (dataProcessor) { + + // Convert string types using a method that will preserve any nul characters + // within the string + case SPMySQLResultFieldAsString: + case SPMySQLResultFieldAsStringOrBlob: + return [[[NSString alloc] initWithBytes:bytes length:length encoding:stringEncoding] autorelease]; + + // Convert BLOB types to NSData + case SPMySQLResultFieldAsBlob: + return [NSData dataWithBytes:bytes length:length]; + + // For Geometry types, use a special Geometry object to handle their complexity + case SPMySQLResultFieldAsGeometry: + return [SPMySQLGeometryData dataWithBytes:bytes length:length]; + + // For bit fields, get a zero-padded representation of the data + case SPMySQLResultFieldAsBit: + return [SPMySQLResult bitStringWithBytes:bytes length:length padToLength:fieldDefinitions[fieldIndex].length]; + + // Convert null types to NSNulls + case SPMySQLResultFieldAsNull: + return [NSNull null]; + + case SPMySQLResultFieldAsUnhandled: + NSLog(@"SPMySQLResult processing encountered an unknown field type (%d), falling back to NSData handling", fieldType); + return [NSData dataWithBytes:bytes length:length]; + } + + [NSException raise:NSInternalInconsistencyException format:@"Unhandled field type when processing SPMySQLResults"]; + return nil; +} + +@end -- cgit v1.2.3 From 0c91f439a5b23e1b5a6d8139eb47aa5694e2925f Mon Sep 17 00:00:00 2001 From: rowanbeentje Date: Fri, 24 Feb 2012 00:30:18 +0000 Subject: Improvements to SPMySQL framework: - Correctly record affected rows - Fix thread safety/autorelease issues when draining pools during fast iteration - Improve streaming result processing speed --- Frameworks/SPMySQLFramework/Source/SPMySQLResult.m | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'Frameworks/SPMySQLFramework/Source/SPMySQLResult.m') diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLResult.m b/Frameworks/SPMySQLFramework/Source/SPMySQLResult.m index b110958d..3ccd5727 100644 --- a/Frameworks/SPMySQLFramework/Source/SPMySQLResult.m +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLResult.m @@ -32,8 +32,10 @@ #import "SPMySQLResult.h" #import "SPMySQL Private APIs.h" +#import "SPMySQLArrayAdditions.h" static SPMySQLResultFieldProcessor fieldProcessingMap[256]; +static id NSNullPointer; @implementation SPMySQLResult @@ -52,6 +54,9 @@ static SPMySQLResultFieldProcessor fieldProcessingMap[256]; + (void)initialize { + // Cached NSNull singleton reference + if (!NSNullPointer) NSNullPointer = [NSNull null]; + // Go through the list of enum_field_types in mysql_com.h, mapping each to the method for // processing that result set. fieldProcessingMap[MYSQL_TYPE_DECIMAL] = SPMySQLResultFieldAsString; @@ -266,11 +271,11 @@ static SPMySQLResultFieldProcessor fieldProcessingMap[256]; id cellData = SPMySQLResultGetObject(self, theRow[i], theRowDataLengths[i], fieldTypes[i], i); // If object creation failed, display a null - if (!cellData) cellData = [NSNull null]; + if (!cellData) cellData = NSNullPointer; // Add to the result array/dictionary if (theType == SPMySQLResultRowAsArray) { - [(NSMutableArray *)theReturnData addObject:cellData]; + SPMySQLMutableArrayInsertObject(theReturnData, cellData, i); } else { [(NSMutableDictionary *)theReturnData setObject:cellData forKey:fieldNames[i]]; } @@ -389,7 +394,7 @@ static SPMySQLResultFieldProcessor fieldProcessingMap[256]; { // A NULL pointer for the data indicates a null value; return a NSNull object. - if (bytes == NULL) return [NSNull null]; + if (bytes == NULL) return NSNullPointer; // Determine the field processor to use SPMySQLResultFieldProcessor dataProcessor = fieldProcessingMap[fieldType]; @@ -445,7 +450,7 @@ static SPMySQLResultFieldProcessor fieldProcessingMap[256]; // Convert null types to NSNulls case SPMySQLResultFieldAsNull: - return [NSNull null]; + return NSNullPointer; case SPMySQLResultFieldAsUnhandled: NSLog(@"SPMySQLResult processing encountered an unknown field type (%d), falling back to NSData handling", fieldType); -- cgit v1.2.3