// // $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" #import "SPMySQLArrayAdditions.h" static SPMySQLResultFieldProcessor fieldProcessingMap[256]; static id NSNullPointer; @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 { // 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; 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 = NSNullPointer; // Add to the result array/dictionary if (theType == SPMySQLResultRowAsArray) { SPMySQLMutableArrayInsertObject(theReturnData, cellData, i); } 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 NSNullPointer; // 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 NSNullPointer; 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