aboutsummaryrefslogtreecommitdiffstats
path: root/Frameworks/SPMySQLFramework/Source/SPMySQLResult.m
diff options
context:
space:
mode:
Diffstat (limited to 'Frameworks/SPMySQLFramework/Source/SPMySQLResult.m')
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLResult.m464
1 files changed, 464 insertions, 0 deletions
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLResult.m b/Frameworks/SPMySQLFramework/Source/SPMySQLResult.m
new file mode 100644
index 00000000..3ccd5727
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLResult.m
@@ -0,0 +1,464 @@
+//
+// $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 <http://code.google.com/p/sequel-pro/>
+
+#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