aboutsummaryrefslogtreecommitdiffstats
path: root/Frameworks/SPMySQLFramework/Source
diff options
context:
space:
mode:
authorstuconnolly <stuart02@gmail.com>2012-03-18 20:05:36 +0000
committerstuconnolly <stuart02@gmail.com>2012-03-18 20:05:36 +0000
commit524e8c356b4074f5be5933b0551374a130a8f6d1 (patch)
tree0dab40735f2d9484930050cd08376cbf15e4ea55 /Frameworks/SPMySQLFramework/Source
parentbe3263f8158cb6f3dfa1005f49beefa7e494b852 (diff)
downloadsequelpro-524e8c356b4074f5be5933b0551374a130a8f6d1.tar.gz
sequelpro-524e8c356b4074f5be5933b0551374a130a8f6d1.tar.bz2
sequelpro-524e8c356b4074f5be5933b0551374a130a8f6d1.zip
Bring outline view branch up to date with trunk (r3471:r3517).
Diffstat (limited to 'Frameworks/SPMySQLFramework/Source')
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQL Private APIs.h110
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQL.h63
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLArrayAdditions.h50
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.h60
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.m97
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Copying.h36
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Copying.m71
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Databases & Tables.h49
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Databases & Tables.m258
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Delegate & Proxy.h36
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Delegate & Proxy.m133
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Encoding.h53
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Encoding.m414
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Locking.h41
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Locking.m104
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Max Packet Size.h40
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Max Packet Size.m196
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Ping & KeepAlive.h55
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Ping & KeepAlive.m214
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.h106
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.m636
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Server Info.h50
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Server Info.m175
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection.h181
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection.m834
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnectionDelegate.h108
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnectionProxy.h77
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConstants.h76
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLFastStreamingResult.h47
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLFastStreamingResult.m405
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLFramework_Prefix.pch11
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLGeometryData.h53
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLGeometryData.m810
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLKeepAliveTimer.h45
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLKeepAliveTimer.m127
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Convenience Methods.h38
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Convenience Methods.m71
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Field Definitions.h38
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Field Definitions.m557
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLResult.h121
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLResult.m464
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResult.h54
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResult.m246
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLStringAdditions.h39
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLStringAdditions.m57
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLUtilities.h45
46 files changed, 7551 insertions, 0 deletions
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQL Private APIs.h b/Frameworks/SPMySQLFramework/Source/SPMySQL Private APIs.h
new file mode 100644
index 00000000..5772eb72
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQL Private APIs.h
@@ -0,0 +1,110 @@
+//
+// $Id$
+//
+// SPMySQLConnection_PrivateAPI.h
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on February 12, 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/>
+
+/**
+ * A collection of Private APIs from the various categories, to simplify
+ * inclusion across the categories.
+ */
+
+#import "Ping & KeepAlive.h"
+#import "Locking.h"
+#import "Conversion.h"
+
+
+@interface SPMySQLConnection (PrivateAPI)
+
+- (MYSQL *)_makeRawMySQLConnectionWithEncoding:(NSString *)encodingName isMasterConnection:(BOOL)isMaster;
+- (BOOL)_waitForNetworkConnectionWithTimeout:(double)timeoutSeconds;
+- (void)_updateConnectionVariables;
+- (void)_restoreConnectionVariables;
+- (BOOL)_checkConnectionIfNecessary;
+
+@end
+
+
+@interface SPMySQLConnection (Delegate_and_Proxy_Private_API)
+
+- (void)_proxyStateChange:(NSObject <SPMySQLConnectionProxy> *)aProxy;
+- (SPMySQLConnectionLostDecision)_delegateDecisionForLostConnection;
+
+@end
+
+
+@interface SPMySQLConnection (Databases_and_Tables_Private_API)
+
+- (BOOL)_storeAndAlterEncodingToUTF8IfRequired;
+
+@end
+
+
+@interface SPMySQLConnection (Max_Packet_Size_Private_API)
+
+- (void)_updateMaxQuerySize;
+- (void)_updateMaxQuerySizeEditability;
+- (BOOL)_attemptMaxQuerySizeIncreaseTo:(NSUInteger)targetSize;
+- (void)_restoreMaximumQuerySizeAfterQuery;
+
+@end
+
+
+@interface SPMySQLConnection (Querying_and_Preparation_Private_API)
+
+- (void)_flushMultipleResultSets;
+- (void)_updateLastErrorMessage:(NSString *)theErrorMessage;
+- (void)_updateLastErrorID:(NSUInteger)theErrorID;
+
+@end
+
+
+// SPMySQLResult Private API
+@interface SPMySQLResult (Private_API)
+
+- (NSString *)_stringWithBytes:(const void *)bytes length:(NSUInteger)length;
+- (void)_setQueryExecutionTime:(double)theExecutionTime;
+- (id)_getObjectFromBytes:(char *)bytes ofLength:(NSUInteger)length fieldType:(unsigned int)fieldType fieldDefinitionIndex:(NSUInteger)fieldIndex;
+
+@end
+
+/**
+ * Set up a static function to allow fast calling of SPMySQLResult data conversion with cached selectors
+ */
+static inline id SPMySQLResultGetObject(SPMySQLResult* self, char* bytes, NSUInteger length, unsigned int fieldType, NSUInteger fieldIndex)
+{
+ typedef id (*SPMySQLResultGetObjectMethodPtr)(SPMySQLResult*, SEL, char*, NSUInteger, unsigned int, NSUInteger);
+ static SPMySQLResultGetObjectMethodPtr cachedMethodPointer;
+ static SEL cachedSelector;
+
+ if (!cachedSelector) cachedSelector = @selector(_getObjectFromBytes:ofLength:fieldType:fieldDefinitionIndex:);
+ if (!cachedMethodPointer) cachedMethodPointer = (SPMySQLResultGetObjectMethodPtr)[self methodForSelector:cachedSelector];
+
+ return cachedMethodPointer(self, cachedSelector, bytes, length, fieldType, fieldIndex);
+} \ No newline at end of file
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQL.h b/Frameworks/SPMySQLFramework/Source/SPMySQL.h
new file mode 100644
index 00000000..3c4c78f8
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQL.h
@@ -0,0 +1,63 @@
+//
+// $Id$
+//
+// SPMySQL.h
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on January 22, 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/>
+
+@class SPMySQLConnection, SPMySQLResult, SPMySQLStreamingResult, SPMySQLFastStreamingResult;
+
+// Global include file for the framework.
+// Constants
+#import "SPMySQLConstants.h"
+
+// Required category additions
+#import "SPMySQLStringAdditions.h"
+
+// MySQL Connection Delegate and Proxy protocols
+#import "SPMySQLConnectionDelegate.h"
+#import "SPMySQLConnectionProxy.h"
+
+// MySQL Connection class and public categories
+#import "SPMySQLConnection.h"
+#import "Delegate & Proxy.h"
+#import "Databases & Tables.h"
+#import "Max Packet Size.h"
+#import "Querying & Preparation.h"
+#import "Encoding.h"
+#import "Server Info.h"
+
+// MySQL result set, streaming subclasses of same, and associated categories
+#import "SPMySQLResult.h"
+#import "SPMySQLStreamingResult.h"
+#import "SPMySQLFastStreamingResult.h"
+#import "Field Definitions.h"
+#import "Convenience Methods.h"
+
+// Result data objects
+#import "SPMySQLGeometryData.h"
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLArrayAdditions.h b/Frameworks/SPMySQLFramework/Source/SPMySQLArrayAdditions.h
new file mode 100644
index 00000000..46c6d8e0
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLArrayAdditions.h
@@ -0,0 +1,50 @@
+//
+// $Id$
+//
+// SPMySQLArrayAdditions.h
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on February 23, 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/>
+
+/**
+ * Set up a static function to allow fast mutable array insertion using
+ * cached selectors.
+ * At least in 10.7, inserting items into an array at a known point
+ * using NSMutableArray methods appears to be the fastest way of adding
+ * items to a CF/NSMutableArray.
+ */
+static inline void SPMySQLMutableArrayInsertObject(NSMutableArray *self, id anObject, NSUInteger anIndex)
+{
+ typedef id (*SPMySQLMutableArrayInsertObjectPtr)(NSMutableArray*, SEL, id, NSUInteger);
+ static SPMySQLMutableArrayInsertObjectPtr cachedMethodPointer;
+ static SEL cachedSelector;
+
+ if (!cachedSelector) cachedSelector = @selector(insertObject:atIndex:);
+ if (!cachedMethodPointer) cachedMethodPointer = (SPMySQLMutableArrayInsertObjectPtr)[self methodForSelector:cachedSelector];
+
+ cachedMethodPointer(self, cachedSelector, anObject, anIndex);
+}
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.h b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.h
new file mode 100644
index 00000000..0309ebdb
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.h
@@ -0,0 +1,60 @@
+//
+// $Id$
+//
+// Encoding.m
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on January 22, 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.
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+//
+// More info at <http://code.google.com/p/sequel-pro/>
+
+// This class is private to the framework.
+
+@interface SPMySQLConnection (Conversion)
+
++ (const char *)_cStringForString:(NSString *)aString usingEncoding:(NSStringEncoding)anEncoding returningLengthAs:(NSUInteger *)cStringLengthPointer;
+
+- (const char *)_cStringForString:(NSString *)aString;
+- (NSString *)_stringForCString:(const char *)cString;
+
+@end
+
+
+/**
+ * Set up a static function to allow fast calling with cached selectors
+ */
+static inline const char* _cStringForStringWithEncoding(NSString* aString, NSStringEncoding anEncoding, NSUInteger *cStringLengthPointer)
+{
+ static Class cachedClass;
+ static IMP cachedMethodPointer;
+ static SEL cachedSelector;
+
+ if (!cachedClass) cachedClass = [SPMySQLConnection class];
+ if (!cachedSelector) cachedSelector = @selector(_cStringForString:usingEncoding:returningLengthAs:);
+ if (!cachedMethodPointer) cachedMethodPointer = [SPMySQLConnection methodForSelector:cachedSelector];
+
+ return (const char *)(*cachedMethodPointer)(cachedClass, cachedSelector, aString, anEncoding, cStringLengthPointer);
+}
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.m b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.m
new file mode 100644
index 00000000..0a3cf99b
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.m
@@ -0,0 +1,97 @@
+//
+// $Id$
+//
+// Encoding.m
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on January 22, 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/>
+
+// This class is private to the framework.
+
+#import "Conversion.h"
+
+@implementation SPMySQLConnection (Conversion)
+
+/**
+ * Converts an NSString to a null-terminated C string, using the supplied encoding.
+ * Uses lossy conversion, so if a string cannot be entirely converted using
+ * the current encoding, a representation will be returned rather than null.
+ * The returned cString will correctly preserve any nul characters within the string,
+ * which prevents the use of faster functions like [NSString cStringUsingEncoding:].
+ * Pass in the third parameter to receive the length of the converted string, or pass
+ * in NULL if you do not want this information.
+ */
++ (const char *)_cStringForString:(NSString *)aString usingEncoding:(NSStringEncoding)anEncoding returningLengthAs:(NSUInteger *)cStringLengthPointer
+{
+
+ // Don't try and convert nil strings
+ if (!aString) return NULL;
+
+ // Perform a lossy conversion, using NSData to do the hard work
+ NSData *convertedData = [aString dataUsingEncoding:anEncoding allowLossyConversion:YES];
+ NSUInteger convertedDataLength = [convertedData length];
+
+ // Take the converted data - not null-terminated - and copy it to a null-terminated buffer
+ char *cStringBytes = malloc(convertedDataLength + 1);
+ memcpy(cStringBytes, [convertedData bytes], convertedDataLength);
+ cStringBytes[convertedDataLength] = 0L;
+
+ if (cStringLengthPointer) *cStringLengthPointer = convertedDataLength+1;
+
+ // Ensure the memory is autoreleased when needed, and return.
+ [NSData dataWithBytesNoCopy:cStringBytes length:convertedDataLength+1 freeWhenDone:YES];
+ return cStringBytes;
+}
+
+/**
+ * Converts an NSString to a null-terminated C string, using the current
+ * connection encoding.
+ */
+- (const char *)_cStringForString:(NSString *)aString
+{
+
+ // Use a cached reference to avoid dynamic method overhead
+ return _cStringForStringWithEncoding(aString, stringEncoding, NULL);
+}
+
+/**
+ * Converts a C string to an NSString using the supplied encoding.
+ * This method *will not* correctly preserve nul characters within c strings; instead
+ * the first nul character within the string will be treated as the line ending. This
+ * is unavoidable without supplying a string length, so this method should not be widely
+ * used for actual data conversion.
+ */
+- (NSString *)_stringForCString:(const char *)cString
+{
+
+ // Don't try and convert null strings
+ if (cString == NULL) return nil;
+
+ return [NSString stringWithCString:cString encoding:stringEncoding];
+}
+
+@end \ No newline at end of file
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Copying.h b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Copying.h
new file mode 100644
index 00000000..83d055d5
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Copying.h
@@ -0,0 +1,36 @@
+//
+// $Id$
+//
+// Copying.h
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on March 8, 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/>
+
+
+@interface SPMySQLConnection (Copying) <NSCopying>
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Copying.m b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Copying.m
new file mode 100644
index 00000000..c2df2a4b
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Copying.m
@@ -0,0 +1,71 @@
+//
+// $Id$
+//
+// Copying.m
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on March 8, 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 "Copying.h"
+
+@implementation SPMySQLConnection (Copying)
+
+/**
+ * Provide a copy of the SPMySQLConnection instance.
+ * The copy should inherit the full setup, but will not inherit
+ * the connection state - it will not be connected, and any connection
+ * details such as the selected database/encoding will not be inherited.
+ * Note that any proxy will not be referenced in the new connection, and
+ * should also be set if desired.
+ */
+- (id)copyWithZone:(NSZone *)zone
+{
+ SPMySQLConnection *copy = [[[self class] allocWithZone:zone] init];
+
+ // Synthesized details
+ [copy setDelegate:delegate];
+ [copy setHost:host];
+ [copy setUsername:username];
+ [copy setPassword:password];
+ [copy setPort:port];
+ [copy setUseSocket:useSocket];
+ [copy setSocketPath:socketPath];
+ [copy setUseSSL:useSSL];
+ [copy setSslKeyFilePath:sslKeyFilePath];
+ [copy setSslCertificatePath:sslCertificatePath];
+ [copy setSslCACertificatePath:sslCACertificatePath];
+ [copy setTimeout:timeout];
+ [copy setUseKeepAlive:useKeepAlive];
+ [copy setRetryQueriesOnConnectionFailure:retryQueriesOnConnectionFailure];
+ [copy setDelegateQueryLogging:delegateQueryLogging];
+
+ // Active connection state details, like selected database and encoding, are *not* copied.
+
+ return copy;
+}
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Databases & Tables.h b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Databases & Tables.h
new file mode 100644
index 00000000..332b2680
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Databases & Tables.h
@@ -0,0 +1,49 @@
+//
+// $Id$
+//
+// Databases & Tables.h
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on February 11, 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/>
+
+
+@interface SPMySQLConnection (Databases_and_Tables)
+
+// Database selection
+- (BOOL)selectDatabase:(NSString *)aDatabase;
+
+// Database lists
+- (NSArray *)databases;
+- (NSArray *)databasesLike:(NSString *)nameLikeString;
+
+// Table lists
+- (NSArray *)tables;
+- (NSArray *)tablesLike:(NSString *)nameLikeString;
+- (NSArray *)tablesFromDatabase:(NSString *)aDatabase;
+- (NSArray *)tablesLike:(NSString *)nameLikeString fromDatabase:(NSString *)aDatabase;
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Databases & Tables.m b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Databases & Tables.m
new file mode 100644
index 00000000..a95e060e
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Databases & Tables.m
@@ -0,0 +1,258 @@
+//
+// $Id$
+//
+// Databases & Tables.m
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on February 11, 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 "Databases & Tables.h"
+#import "SPMySQL Private APIs.h"
+
+@implementation SPMySQLConnection (Databases_and_Tables)
+
+#pragma mark -
+#pragma mark Database selection
+
+/**
+ * Selects the database the connection should work with. Typically, a database should be
+ * set on a connection before any database-specific queries are run.
+ * Returns whether the database was correctly set or not.
+ * As MySQL does not support deselecting databases, a nil databaseName will return NO.
+ */
+- (BOOL)selectDatabase:(NSString *)aDatabase
+{
+
+ // If no database was supplied, can't deselected - return NO.
+ if (!aDatabase) return NO;
+
+ // Database selection should be made in UTF8 to avoid name encoding issues
+ BOOL encodingChangeRequired = [self _storeAndAlterEncodingToUTF8IfRequired];
+
+ // Attempt to select the supplied database
+ [self queryString:[NSString stringWithFormat:@"USE %@", [aDatabase mySQLBacktickQuotedString]]];
+
+ // If selecting the database failed, return failure.
+ if ([self queryErrored]) {
+
+ // If the encoding needs to be restored, the error message and ID have to be stored so the
+ // actual error is still available to inspect on the class...
+ if (encodingChangeRequired) {
+ NSString *theErrorString = [self lastErrorMessage];
+ NSUInteger theErrorID = [self lastErrorID];
+
+ [self restoreStoredEncoding];
+
+ [self _updateLastErrorMessage:theErrorString];
+ [self _updateLastErrorID:theErrorID];
+ }
+
+ return NO;
+ }
+
+ // Restore the connection encoding if necessary
+ if (encodingChangeRequired) [self restoreStoredEncoding];
+
+ // Store new database name and return success
+ if (database) [database release];
+ database = [[NSString alloc] initWithString:aDatabase];
+
+ return YES;
+}
+
+#pragma mark -
+#pragma mark Database lists
+
+/**
+ * Retrieve an array of databases available to the current user, ordered as MySQL
+ * returns them.
+ * If an error occurred while retrieving the list of databases, nil will be returned;
+ * if no databases are available, an empty array will be returned.
+ */
+- (NSArray *)databases
+{
+
+ // Wrap the related databasesLike: function to avoid code duplication
+ return [self databasesLike:nil];
+}
+
+/**
+ * Retrieve an array of databases whose names are matched against the supplied name
+ * using MySQL LIKE syntax (with wildcard support for % and _). If no name is supplied,
+ * all databases will be returned, in the order that MySQL returns them.
+ * If an error occurred while retrieving the list of databases, nil will be returned;
+ * if no matching databases are available, an empty array will be returned.
+ */
+- (NSArray *)databasesLike:(NSString *)nameLikeString
+{
+ NSMutableArray *databaseList = nil;
+
+ // Database display should be made in UTF8 to avoid name encoding issues
+ BOOL encodingChangeRequired = [self _storeAndAlterEncodingToUTF8IfRequired];
+
+ // Build the query as appropriate
+ NSMutableString *databaseQuery = [NSMutableString stringWithString:@"SHOW DATABASES"];
+ if ([nameLikeString length]) {
+ [databaseQuery appendFormat:@" LIKE %@", [nameLikeString mySQLTickQuotedString]];
+ }
+
+ // Perform the query and record state
+ SPMySQLResult *databaseResult = [self queryString:databaseQuery];
+ [databaseResult setDefaultRowReturnType:SPMySQLResultRowAsArray];
+
+ // Retrieve the result into an array if the query was successful
+ if (![self queryErrored]) {
+ databaseList = [NSMutableArray arrayWithCapacity:(NSUInteger)[databaseResult numberOfRows]];
+ for (NSArray *dbRow in databaseResult) {
+ [databaseList addObject:[dbRow objectAtIndex:0]];
+ }
+ }
+
+ // Restore the connection encoding if necessary
+ if (encodingChangeRequired) [self restoreStoredEncoding];
+
+ return databaseList;
+}
+
+#pragma mark -
+#pragma mark Table lists
+
+/**
+ * Retrieve an array of tables in the currently selected database, ordered as MySQL
+ * returns them.
+ * If an error occurred while retrieving the list of tables, nil will be returned;
+ * if no tables are present, an empty array will be returned.
+ */
+- (NSArray *)tables
+{
+
+ // Wrap the related tablesLike:fromDatabase: function to avoid code duplication
+ return [self tablesLike:nil fromDatabase:nil];
+}
+
+/**
+ * Retrieve an array of tables in the currently selected database whose names are
+ * matched against the supplied name using MySQL LIKE syntax (with wildcard
+ * support for % and _). If no name is supplied, all tables in the selected
+ * database will be returned, in the order that MySQL returns them.
+ * If an error occurred while retrieving the list of tables, nil will be returned;
+ * if no matching tables are present, an empty array will be returned.
+ */
+- (NSArray *)tablesLike:(NSString *)nameLikeString
+{
+
+ // Wrap the related tablesLike:fromDatabase: function to avoid code duplication
+ return [self tablesLike:nameLikeString fromDatabase:nil];
+
+}
+
+/**
+ * Retrieve an array of tables in the specified database, ordered as MySQL returns them.
+ * If no database is specified, the current database will be used.
+ * If an error occurred while retrieving the list of tables, nil will be returned;
+ * if no tables are present in the specified database, an empty array will be returned.
+ */
+- (NSArray *)tablesFromDatabase:(NSString *)aDatabase
+{
+
+ // Wrap the related tablesLike:fromDatabase: function to avoid code duplication
+ return [self tablesLike:nil fromDatabase:aDatabase];
+
+}
+
+/**
+ * Retrieve an array of tables in the specified database whose names are matched
+ * against the supplied name using MySQL LIKE syntax (with wildcard support
+ * for % and _). If no name is supplied, all tables in the specified database
+ * will be returned, in the order that MySQL returns them.
+ * If no database is specified, the current database will be used.
+ * If an error occurred while retrieving the list of tables, nil will be returned;
+ * if no matching tables are present in the specified database, an empty array
+ * will be returned.
+ */
+- (NSArray *)tablesLike:(NSString *)nameLikeString fromDatabase:(NSString *)aDatabase
+{
+ NSMutableArray *tableList = nil;
+
+ // Table display should be made in UTF8 to avoid name encoding issues
+ BOOL encodingChangeRequired = [self _storeAndAlterEncodingToUTF8IfRequired];
+
+ // Build up the table lookup query
+ NSMutableString *tableQuery = [NSMutableString stringWithString:@"SHOW TABLES"];
+ if ([aDatabase length]) {
+ [tableQuery appendFormat:@" FROM %@", [aDatabase mySQLBacktickQuotedString]];
+ }
+ if ([nameLikeString length]) {
+ [tableQuery appendFormat:@" LIKE %@", [nameLikeString mySQLTickQuotedString]];
+ }
+
+ // Perform the query and record state
+ SPMySQLResult *tableResult = [self queryString:tableQuery];
+ [tableResult setDefaultRowReturnType:SPMySQLResultRowAsArray];
+
+ // Retrieve the result into an array if the query was successful
+ if (![self queryErrored]) {
+ tableList = [NSMutableArray arrayWithCapacity:(NSUInteger)[tableResult numberOfRows]];
+ for (NSArray *tableRow in tableResult) {
+ [tableList addObject:[tableRow objectAtIndex:0]];
+ }
+ }
+
+ // Restore the connection encoding if necessary
+ if (encodingChangeRequired) [self restoreStoredEncoding];
+
+ return tableList;
+}
+
+@end
+
+#pragma mark -
+#pragma mark Private API
+
+@implementation SPMySQLConnection (Databases_and_Tables_Private_API)
+
+/**
+ * A number of queries regarding database or table information have to be made in UTF8, not
+ * in the connection encoding, so that names can be fully displayed and used even if they
+ * use a different encoding. This provides a convenience method to check whether a change
+ * is required; if so, the current encoding is stored, the encoding is changed, and YES is
+ * returned so the process can be reversed afterwards.
+ */
+- (BOOL)_storeAndAlterEncodingToUTF8IfRequired
+{
+
+ // If the encoding is already UTF8, no change is required.
+ if ([encoding isEqualToString:@"utf8"] && !encodingUsesLatin1Transport) return NO;
+
+ // Store the current encoding for restoration afterwards, and update encoding
+ [self storeEncodingForRestoration];
+ [self setEncoding:@"utf8"];
+
+ return YES;
+}
+
+@end \ No newline at end of file
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Delegate & Proxy.h b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Delegate & Proxy.h
new file mode 100644
index 00000000..cf132fcf
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Delegate & Proxy.h
@@ -0,0 +1,36 @@
+//
+// $Id$
+//
+// Delegate & Proxy.h
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on February 9, 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/>
+
+
+@interface SPMySQLConnection (Delegate_and_Proxy)
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Delegate & Proxy.m b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Delegate & Proxy.m
new file mode 100644
index 00000000..3ac013cc
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Delegate & Proxy.m
@@ -0,0 +1,133 @@
+//
+// $Id$
+//
+// Delegate & Proxy.m
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on February 9, 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 "Delegate & Proxy.h"
+#import "SPMySQL Private APIs.h"
+
+@implementation SPMySQLConnection (Delegate_and_Proxy)
+
+#pragma mark -
+#pragma mark Connection delegate
+
+/**
+ * Override the synthesized delegate setter, to allow optimisations to oft-made
+ * checks by precacheing availability.
+ */
+- (void)setDelegate:(NSObject <SPMySQLConnectionDelegate> *)aDelegate
+{
+ delegate = aDelegate;
+
+ // Cache whether the delegate implements certain delegate methods
+ delegateSupportsWillQueryString = [delegate respondsToSelector:@selector(willQueryString:connection:)];
+ delegateSupportsConnectionLost = [delegate respondsToSelector:@selector(connectionLost:)];
+}
+
+#pragma mark -
+#pragma mark Connection proxy
+
+/**
+ * Override the synthesized proxy setter, to record the initial state and to
+ * set the state change selector.
+ */
+- (void)setProxy:(NSObject <SPMySQLConnectionProxy> *)aProxy
+{
+ proxy = [aProxy retain];
+ previousProxyState = [aProxy state];
+
+ [proxy setConnectionStateChangeSelector:@selector(_proxyStateChange:) delegate:self];
+}
+
+@end
+
+#pragma mark -
+
+@implementation SPMySQLConnection (Delegate_and_Proxy_Private_API)
+
+/**
+ * Handle any state changes in the associated connection proxy.
+ */
+- (void)_proxyStateChange:(NSObject <SPMySQLConnectionProxy> *)aProxy
+{
+
+ // Perform no actions if this isn't the current connection proxy, or if notifications
+ // are currently set to be ignored
+ if (aProxy != proxy || proxyStateChangeNotificationsIgnored) return;
+
+ SPMySQLConnectionProxyState newState = [aProxy state];
+
+ // If the connection proxy disconnects, trigger a reconnect; use a new thread to allow the
+ // main thread to process events as required.
+ if (state == SPMySQLConnected && newState == SPMySQLProxyIdle && previousProxyState == SPMySQLProxyConnected) {
+
+ // Clear the state change selector on the proxy until a connection is re-established
+ proxyStateChangeNotificationsIgnored = YES;
+
+ // Trigger a reconnect
+ [NSThread detachNewThreadSelector:@selector(reconnect) toTarget:self withObject:nil];
+ }
+
+ // Update the state record
+ previousProxyState = newState;
+}
+
+/**
+ * Ask the delegate for the connection lost decision. This can be called from
+ * any thread, and will call itself on the main thread if necessary, updating a global
+ * variable which is then returned on the child thread.
+ */
+- (SPMySQLConnectionLostDecision)_delegateDecisionForLostConnection
+{
+ SPMySQLConnectionLostDecision theDecision = SPMySQLConnectionLostDisconnect;
+
+ // If on the main thread, ask the delegate directly.
+ if ([NSThread isMainThread]) {
+ [delegateDecisionLock lock];
+ lastDelegateDecisionForLostConnection = [delegate connectionLost:self];
+ theDecision = lastDelegateDecisionForLostConnection;
+ [delegateDecisionLock unlock];
+
+ // Otherwise call ourself on the main thread, waiting until the reply is received.
+ } else {
+
+ // First check whether the application is in a modal state; if so, wait
+ while ([NSApp modalWindow]) usleep(100000);
+
+ [self performSelectorOnMainThread:@selector(_delegateDecisionForLostConnection) withObject:nil waitUntilDone:YES];
+ [delegateDecisionLock lock];
+ theDecision = lastDelegateDecisionForLostConnection;
+ [delegateDecisionLock unlock];
+ }
+
+ return theDecision;
+}
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Encoding.h b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Encoding.h
new file mode 100644
index 00000000..bb5bf25d
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Encoding.h
@@ -0,0 +1,53 @@
+//
+// $Id$
+//
+// Encoding.h
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on January 14, 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/>
+
+
+@interface SPMySQLConnection (Encoding)
+
+// Current connection encoding information
+- (NSString *)encoding;
+- (NSStringEncoding)stringEncoding;
+- (BOOL)encodingUsesLatin1Transport;
+
+// Setting connection encoding
+- (BOOL)setEncoding:(NSString *)theEncoding;
+- (BOOL)setEncodingUsesLatin1Transport:(BOOL)useLatin1;
+
+// Encoding storage and restoration
+- (void)storeEncodingForRestoration;
+- (void)restoreStoredEncoding;
+
+// Encoding conversion
++ (NSStringEncoding)stringEncodingForMySQLCharset:(const char *)mysqlCharset;
++ (NSString *)mySQLCharsetForStringEncoding:(NSStringEncoding)aStringEncoding;
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Encoding.m b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Encoding.m
new file mode 100644
index 00000000..8f684f29
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Encoding.m
@@ -0,0 +1,414 @@
+//
+// $Id$
+//
+// Encoding.m
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on January 14, 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 "Encoding.h"
+
+@implementation SPMySQLConnection (Encoding)
+
+#pragma mark -
+#pragma mark Current connection encoding information
+
+/**
+ * Returns the name of the current encoding - the MySQL character set - in
+ * use by the connection.
+ */
+- (NSString *)encoding
+{
+ return [NSString stringWithString:encoding];
+}
+
+/**
+ * Returns the NSStringEncoding currently in use by the connection to process
+ * queries and results.
+ */
+- (NSStringEncoding)stringEncoding
+{
+ return stringEncoding;
+}
+
+/**
+ * Returns whether the connection is set to use Latin1 transport for queries and
+ * results.
+ * Latin1 transport is a compatibility mode in place for compatibility with older
+ * incorrect setups, where databases and clients might both be set to use UTF8 (or
+ * other encodings) for storing and retrieving data, but the MySQL link was never
+ * set to UTF8 mode; as a result, multibyte characters where split by the connection
+ * into pairs of characters, resulting in malformed storage. The data works
+ * correctly if written and read in the same way, so this mode allows correct display
+ * of that data.
+ */
+- (BOOL)encodingUsesLatin1Transport
+{
+ return encodingUsesLatin1Transport;
+}
+
+#pragma mark -
+#pragma mark Setting connection encoding
+
+/**
+ * Set the name of the encoding - the MySQL character set - that the connection
+ * should use. If an encoding not recognised by the server is supplied, NO is
+ * returned.
+ * Calling this resets whether the connection should use Latin1 transport to NO.
+ */
+- (BOOL)setEncoding:(NSString *)theEncoding
+{
+
+ // MySQL versions prior to 4.1 don't support encoding changes; return NO on those
+ // versions.
+ if (![self serverVersionIsGreaterThanOrEqualTo:4 minorVersion:1 releaseVersion:0]) {
+ return NO;
+ }
+
+ // If the supplied encoding is already set, return success
+ if ([encoding isEqualToString:theEncoding] && !encodingUsesLatin1Transport) {
+ return YES;
+ }
+
+ // Run a query to set the connection encoding
+ [self queryString:[NSString stringWithFormat:@"SET NAMES %@", [theEncoding mySQLTickQuotedString]]];
+
+ // If the query errored, no encoding change occurred - return failure.
+ if ([self queryErrored]) return NO;
+
+ // Connection encoding was successfully set, update the instance settings,
+ // and return success.
+ [encoding release];
+ encoding = [[NSString alloc] initWithString:theEncoding];
+ stringEncoding = [SPMySQLConnection stringEncodingForMySQLCharset:[theEncoding UTF8String]];
+ encodingUsesLatin1Transport = NO;
+
+ return YES;
+}
+
+/**
+ * Sets the connection to use Latin1 transport for queries and results or not. All
+ * encodings will default to not use Latin1 transport..
+ * Latin1 transport is a compatibility mode in place for compatibility with older
+ * incorrect setups, where databases and clients might both be set to use UTF8 (or
+ * other encodings) for storing and retrieving data, but the MySQL link was never
+ * set to UTF8 mode; as a result, multibyte characters where split by the connection
+ * into pairs of characters, resulting in malformed storage. The data works
+ * correctly if written and read in the same way, so this mode allows correct display
+ * of that data.
+ */
+- (BOOL)setEncodingUsesLatin1Transport:(BOOL)useLatin1
+{
+
+ // MySQL versions prior to 4.1 don't support encoding changes; return NO on those
+ // versions.
+ if (![self serverVersionIsGreaterThanOrEqualTo:4 minorVersion:1 releaseVersion:0]) {
+ return NO;
+ }
+
+ // If the Latin1 mode is already set, return success
+ if (encodingUsesLatin1Transport == useLatin1) {
+ return YES;
+ }
+
+ // If disabling Latin1 transport, just restore the connection encoding
+ if (!useLatin1) {
+ return [self setEncoding:encoding];
+ }
+
+ // Otherwise attempt to set Latin1 transport. First, the result set encoding.
+ [self queryString:@"SET CHARACTER_SET_RESULTS=latin1"];
+
+ // If that failed, no encoding change occurred - return failure.
+ if ([self queryErrored]) return NO;
+
+ // Next, change the client character set, to also amend queries sent.
+ [self queryString:@"SET CHARACTER_SET_CLIENT=latin1"];
+
+ // If that failed, encoding details are in a partial state - attempt to restore
+ // the original details before returning failure.
+ if ([self queryErrored]) {
+ [self setEncoding:encoding];
+ return NO;
+ }
+
+ // Connecting encoding transport was successfully set, update the instance settings,
+ // and return success.
+ encodingUsesLatin1Transport = YES;
+ return YES;
+}
+
+#pragma mark -
+#pragma mark Encoding storage and restoration
+
+
+/**
+ * Store a previous encoding setting, to allow it to be easily restored
+ * later - used when the encoding needs to be temporarily changed.
+ */
+- (void)storeEncodingForRestoration
+{
+ if (previousEncoding) [previousEncoding release];
+ previousEncoding = [[NSString alloc] initWithString:encoding];
+ previousEncodingUsesLatin1Transport = encodingUsesLatin1Transport;
+}
+
+/**
+ * Restore a previously stored encoding setting, if available. Used in
+ * conjunection with -storeEncodingForRestoration for when the encoding needs
+ * to be temporarily changed.
+ */
+- (void)restoreStoredEncoding
+{
+ if (!previousEncoding || state == SPMySQLDisconnected || state == SPMySQLDisconnecting) {
+ return;
+ }
+
+ [self setEncoding:previousEncoding];
+ [self setEncodingUsesLatin1Transport:previousEncodingUsesLatin1Transport];
+}
+
+#pragma mark -
+#pragma mark Encoding conversion
+
+/**
+ * Map MySQL encodings to NSStringEncodings, using the list of encodings sourced
+ * from http://dev.mysql.com/doc/refman/5.6/en/charset-charsets.html and the same
+ * list on previous MySQL versions. Older versions also had less-standard lists,
+ * such as the charset options listed on
+ * http://dev.mysql.com/doc/refman/4.1/en/charset-map.html .
+ * For each, the equivalent NSStringEncoding, or conversion from CfStringEncoding,
+ * was found.
+ * If a supplied character set can not be matched, logs an error and falls back
+ * to UTF8 encoding.
+ */
++ (NSStringEncoding)stringEncodingForMySQLCharset:(const char *)mysqlCharset
+{
+
+ // Handle the most common cases first
+ if (!strcmp(mysqlCharset, "utf8")) {
+ return NSUTF8StringEncoding;
+ } else if (!strcmp(mysqlCharset, "latin1")) {
+ return NSISOLatin1StringEncoding;
+ } else if (!strcmp(mysqlCharset, "ascii")) {
+ return NSASCIIStringEncoding;
+
+ // Work down the rest of the 4.1+ charsets
+ } else if (!strcmp(mysqlCharset, "big5")) {
+ return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingBig5);
+ } else if (!strcmp(mysqlCharset, "dec8")) {
+ return NSISOLatin1StringEncoding; // Not exact, but very close
+ } else if (!strcmp(mysqlCharset, "cp850")) {
+ return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingDOSLatin1);
+ } else if (!strcmp(mysqlCharset, "koi8r")) {
+ return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingKOI8_R);
+ } else if (!strcmp(mysqlCharset, "latin2")) {
+ return NSISOLatin2StringEncoding;
+ } else if (!strcmp(mysqlCharset, "ujis")) {
+ return NSJapaneseEUCStringEncoding;
+ } else if (!strcmp(mysqlCharset, "sjis")) {
+ return NSShiftJISStringEncoding;
+ } else if (!strcmp(mysqlCharset, "hebrew")) {
+ return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingISOLatinHebrew);
+ } else if (!strcmp(mysqlCharset, "tis620")) {
+ return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingISOLatinThai);
+ } else if (!strcmp(mysqlCharset, "euckr")) {
+ return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingEUC_KR);
+ } else if (!strcmp(mysqlCharset, "koi8u")) {
+ return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingKOI8_U);
+ } else if (!strcmp(mysqlCharset, "gb2312")) {
+ return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingGB_2312_80);
+ } else if (!strcmp(mysqlCharset, "greek")) {
+ return NSWindowsCP1253StringEncoding;
+ } else if (!strcmp(mysqlCharset, "cp1250")) {
+ return NSWindowsCP1250StringEncoding;
+ } else if (!strcmp(mysqlCharset, "gbk")) {
+ return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingGBK_95);
+ } else if (!strcmp(mysqlCharset, "latin5")) {
+ return NSWindowsCP1254StringEncoding;
+ } else if (!strcmp(mysqlCharset, "ucs2")) {
+ return NSUnicodeStringEncoding;
+ } else if (!strcmp(mysqlCharset, "cp866")) {
+ return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingDOSRussian);
+ } else if (!strcmp(mysqlCharset, "macce")) {
+ return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingMacCentralEurRoman);
+ } else if (!strcmp(mysqlCharset, "macroman")) {
+ return NSMacOSRomanStringEncoding;
+ } else if (!strcmp(mysqlCharset, "cp852")) {
+ return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingDOSLatin2);
+ } else if (!strcmp(mysqlCharset, "latin7")) {
+ return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingISOLatin7);
+ } else if (!strcmp(mysqlCharset, "utf8mb4")) {
+ return NSUnicodeStringEncoding; // Is this correct?
+ } else if (!strcmp(mysqlCharset, "cp1251")) {
+ return NSWindowsCP1251StringEncoding;
+ } else if (!strcmp(mysqlCharset, "utf16")) {
+ return NSUnicodeStringEncoding;
+ } else if (!strcmp(mysqlCharset, "utf16le")) {
+ return NSUTF16LittleEndianStringEncoding;
+ } else if (!strcmp(mysqlCharset, "cp1256")) {
+ return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingWindowsArabic);
+ } else if (!strcmp(mysqlCharset, "cp1257")) {
+ return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingWindowsBalticRim);
+ } else if (!strcmp(mysqlCharset, "utf32")) {
+ return NSUTF32StringEncoding;
+ } else if (!strcmp(mysqlCharset, "binary")) {
+ return NSUTF8StringEncoding;
+ } else if (!strcmp(mysqlCharset, "cp932")) {
+ return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingDOSJapanese);
+ } else if (!strcmp(mysqlCharset, "eucjpms")) {
+ return NSJapaneseEUCStringEncoding;
+
+ // Continue with old < 4.1 mappings
+ } else if (!strcmp(mysqlCharset, "czech")) {
+ return NSISOLatin2StringEncoding;
+ } else if (!strcmp(mysqlCharset, "dos")) {
+ return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingDOSLatin1);
+ } else if (!strcmp(mysqlCharset, "german1")) {
+ return NSISOLatin1StringEncoding;
+ } else if (!strcmp(mysqlCharset, "usa7")) {
+ return NSASCIIStringEncoding;
+ } else if (!strcmp(mysqlCharset, "danish")) {
+ return NSISOLatin1StringEncoding;
+ } else if (!strcmp(mysqlCharset, "win1251")) {
+ return NSWindowsCP1251StringEncoding;
+ } else if (!strcmp(mysqlCharset, "euc_kr")) {
+ return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingEUC_KR);
+ } else if (!strcmp(mysqlCharset, "estonia")) {
+ return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingISOLatin7);
+ } else if (!strcmp(mysqlCharset, "hungarian")) {
+ return NSISOLatin2StringEncoding;
+ } else if (!strcmp(mysqlCharset, "koi8_ru")) {
+ return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingKOI8_R);
+ } else if (!strcmp(mysqlCharset, "koi8_ukr")) {
+ return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingKOI8_U);
+ } else if (!strcmp(mysqlCharset, "win1251ukr")) {
+ return NSWindowsCP1251StringEncoding;
+ } else if (!strcmp(mysqlCharset, "win1250")) {
+ return NSWindowsCP1250StringEncoding;
+ } else if (!strcmp(mysqlCharset, "croat")) {
+ return NSISOLatin2StringEncoding;
+ } else if (!strcmp(mysqlCharset, "latin1_de")) {
+ return NSISOLatin1StringEncoding;
+ }
+
+ /**
+ * Finally, certain other encodings, including the following:
+ * hp8
+ * swe7
+ * armscii8
+ * keybcs2
+ * geostd8
+ * ...don't appear to have OS X equivalents; for these and unhandled, log and
+ * fall back to UTF8 handling.
+ */
+ NSLog(@"SPMySQL Framework has encountered the MySQL encoding '%s' which it is unable to process correctly; falling back to UTF8 mapping.", mysqlCharset);
+ return NSUTF8StringEncoding;
+}
+
+/**
+ * Match a supplied NSStringEncoding to a MySQL character set, returning the MySQL
+ * name of that character set as an NSString.
+ * If the supplied NSStringEncoding could not be matched, logs an error and returns nil.
+ */
++ (NSString *)mySQLCharsetForStringEncoding:(NSStringEncoding)aStringEncoding
+{
+
+ // Switch through the list of NSStringEncodings from NSString, returning the most
+ // appropriate encoding for each
+ switch (aStringEncoding) {
+
+ case NSASCIIStringEncoding:
+ return @"ascii";
+
+ case NSJapaneseEUCStringEncoding:
+ return @"ujis";
+
+ case NSUTF8StringEncoding:
+ return @"utf8";
+
+ case NSISOLatin1StringEncoding:
+ return @"latin1";
+
+ case NSNonLossyASCIIStringEncoding:
+ return @"utf8";
+
+ case NSShiftJISStringEncoding:
+ return @"sjis";
+
+ case NSISOLatin2StringEncoding:
+ return @"latin2";
+
+ case NSUnicodeStringEncoding:
+ return @"ucs2";
+
+ case NSWindowsCP1251StringEncoding:
+ return @"cp1251";
+
+ case NSWindowsCP1252StringEncoding:
+ return @"latin1";
+
+ case NSWindowsCP1253StringEncoding:
+ return @"greek";
+
+ case NSWindowsCP1254StringEncoding:
+ return @"latin5";
+
+ case NSWindowsCP1250StringEncoding:
+ return @"cp1250";
+
+ case NSMacOSRomanStringEncoding:
+ return @"macroman";
+
+ case NSUTF16BigEndianStringEncoding:
+ return @"utf16";
+
+ case NSUTF16LittleEndianStringEncoding:
+ return @"utf16le";
+
+ case NSUTF32StringEncoding:
+ return @"utf32";
+
+ case NSUTF32BigEndianStringEncoding:
+ return @"utf32";
+ }
+
+ /**
+ * Certain string encodings, including the following:
+ * NSNEXTSTEPStringEncoding
+ * NSSymbolStringEncoding
+ * NSISO2022JPStringEncoding
+ * NSUTF32LittleEndianStringEncoding
+ *
+ * ...don't have equivalents; similarly, many CFStringEncodings aren't yet
+ * matched. For those, log and return nil.
+ */
+ NSLog(@"SPMySQL Framework was asked for the MySQL charset for the string encoding '%llu', which is currently unhandled.", (unsigned long long)aStringEncoding);
+ return nil;
+}
+@end \ No newline at end of file
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Locking.h b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Locking.h
new file mode 100644
index 00000000..90e11179
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Locking.h
@@ -0,0 +1,41 @@
+//
+// $Id$
+//
+// Locking.h
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on January 22, 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/>
+
+// This class is private to the framework.
+
+@interface SPMySQLConnection (Locking)
+
+- (void)_lockConnection;
+- (BOOL)_tryLockConnection;
+- (void)_unlockConnection;
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Locking.m b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Locking.m
new file mode 100644
index 00000000..d654066b
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Locking.m
@@ -0,0 +1,104 @@
+//
+// $Id$
+//
+// Locking.m
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on January 22, 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/>
+
+// This class is private to the framework.
+
+#import "Locking.h"
+#import "SPMySQL Private APIs.h"
+
+@implementation SPMySQLConnection (Locking)
+
+
+/**
+ * Lock the connection. This must be done before performing any operation
+ * that is not thread safe, eg. performing queries or pinging.
+ */
+- (void)_lockConnection
+{
+
+ // We can only start a query when the condition is SPMySQLConnectionIdle
+ [connectionLock lockWhenCondition:SPMySQLConnectionIdle];
+
+ // Set the condition to SPMySQLConnectionBusy
+ [connectionLock unlockWithCondition:SPMySQLConnectionBusy];
+}
+
+/**
+ * Attempt to lock the connection. If the connection is idle (unlocked), this method
+ * locks the connection and returns YES for success. The connection must afterward
+ * be unlocked using unlockConnection. If the connection is currently busy (locked),
+ * this method immediately returns NO and doesn't lock the connection.
+ */
+- (BOOL)_tryLockConnection
+{
+
+ // If the connection is already is use, return failure
+ if (![connectionLock tryLockWhenCondition:SPMySQLConnectionIdle]) {
+ return NO;
+ }
+
+ // We're allowed to use the connection; set it to busy, and return success
+ [connectionLock unlockWithCondition:SPMySQLConnectionBusy];
+ return YES;
+}
+
+
+/**
+ * Unlock the connection.
+ */
+- (void)_unlockConnection
+{
+
+ // Always lock the conditional lock before proceeding
+ [connectionLock lock];
+
+ // Check if the connection actually was busy. If it wasn't busy,
+ // it means the connection may have been unlocked twice. This is
+ // potentially dangerous, so we log this to the console
+ if ([connectionLock condition] != SPMySQLConnectionBusy) {
+ NSLog(@"SPMySQLConnection: Tried to unlock the connection, but it wasn't locked.");
+ }
+
+ // Since we connected with CLIENT_MULTI_RESULT, we must make sure there are not more results!
+ // This is still a bit of a dirty hack
+ if (state == SPMySQLConnected
+ && mySQLConnection && mySQLConnection->net.vio && mySQLConnection->net.buff && mysql_more_results(mySQLConnection))
+ {
+ NSLog(@"SPMySQLConnection: Discarding unretrieved results. This is currently normal when using CALL.");
+ [self _flushMultipleResultSets];
+ }
+
+ // Tell everyone that the connection is available again
+ [connectionLock unlockWithCondition:SPMySQLConnectionIdle];
+}
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Max Packet Size.h b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Max Packet Size.h
new file mode 100644
index 00000000..faa667d8
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Max Packet Size.h
@@ -0,0 +1,40 @@
+//
+// $Id$
+//
+// Max Packet Size.h
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on February 9, 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/>
+
+
+@interface SPMySQLConnection (Max_Packet_Size)
+
+- (NSUInteger)maxQuerySize;
+- (BOOL)isMaxQuerySizeEditable;
+- (NSUInteger)setGlobalMaxQuerySize:(NSUInteger)newMaxSize;
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Max Packet Size.m b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Max Packet Size.m
new file mode 100644
index 00000000..e0bfef52
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Max Packet Size.m
@@ -0,0 +1,196 @@
+//
+// $Id$
+//
+// Max Packet Size.m
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on February 9, 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 "Max Packet Size.h"
+#import "SPMySQL Private APIs.h"
+
+@implementation SPMySQLConnection (Max_Packet_Size)
+
+/**
+ * Retrieve the current maximum query size (MySQL's max_allowed_packet), as cached
+ * by the class. If the connection has been unable to retrieve this value, the
+ * default of 1MB will be returned.
+ */
+- (NSUInteger)maxQuerySize
+{
+ return maxQuerySize;
+}
+
+/**
+ * Retrieve whether the server's maximum query size (MySQL's max_allowed_packet) is
+ * editable by the current user.
+ */
+- (BOOL)isMaxQuerySizeEditable
+{
+ if (!maxQuerySizeEditabilityChecked) {
+ [self _updateMaxQuerySizeEditability];
+ }
+
+ return maxQuerySizeIsEditable;
+}
+
+/**
+ * Set the servers's global maximum query size - MySQL's max_allowed_packed - to the
+ * supplied size. Note that this *does not* affect the current connection; a reconnection
+ * is required to pick up the new size setting. As a result it may be important to restore
+ * the connection size after use.
+ * Validates the supplied size (eg 1GB limit) and applies it if appropriate, returning
+ * the set query size or NSNotFound on error.
+ */
+- (NSUInteger)setGlobalMaxQuerySize:(NSUInteger)newMaxSize
+{
+
+ // Perform basic validation. First, ensure the max query size is editable
+ if (![self isMaxQuerySizeEditable]) return NSNotFound;
+
+ // Validate sizes
+ if (newMaxSize < 1024) return NSNotFound;
+ if (newMaxSize > (1024 * 1024 * 1024)) newMaxSize = 1024 * 1024 * 1024;
+
+ // Perform a standard query to set the new size
+ [self queryString:[NSString stringWithFormat:@"SET GLOBAL max_allowed_packet = %lu", newMaxSize]];
+
+ // On failure, return NSNotFound - error state will have automatically been set
+ if ([self queryErrored]) return NSNotFound;
+
+ // Otherwise, set the local instance variable and return success
+ maxQuerySize = newMaxSize;
+ return maxQuerySize;
+}
+
+@end
+
+#pragma mark -
+
+@implementation SPMySQLConnection (Max_Packet_Size_Private_API)
+
+/**
+ * Update the max_allowed_packet size - the largest supported query size - from the server.
+ */
+- (void)_updateMaxQuerySize
+{
+
+ // Determine which query to run based on server version
+ NSString *packetQueryString;
+ if ([self serverMajorVersion] == 3) {
+ packetQueryString = @"SHOW VARIABLES LIKE 'max_allowed_packet'";
+ } else {
+ packetQueryString = @"SELECT @@global.max_allowed_packet";
+ }
+
+ // Make a standard query to the server to retrieve the information
+ SPMySQLResult *result = [self queryString:packetQueryString];
+ [result setReturnDataAsStrings:YES];
+
+ // Get the maximum size string
+ NSString *maxQuerySizeString = nil;
+ if ([self serverMajorVersion] == 3) {
+ maxQuerySizeString = [[result getRowAsArray] objectAtIndex:1];
+ } else {
+ maxQuerySizeString = [[result getRowAsArray] objectAtIndex:0];
+ }
+
+ // If a valid size was returned, update the instance variable
+ if (maxQuerySizeString) {
+ maxQuerySize = (NSUInteger)[maxQuerySizeString integerValue];
+ }
+}
+
+/**
+ * Perform a query to determine whether the current user has permission to edit the
+ * max_allowed_packet setting for their connection.
+ */
+- (void)_updateMaxQuerySizeEditability
+{
+ [self queryString:@"SET GLOBAL max_allowed_packet = @@global.max_allowed_packet"];
+ maxQuerySizeIsEditable = ![self queryErrored];
+ maxQuerySizeEditabilityChecked = YES;
+}
+
+/**
+ * Attempts to change the maximum query size in order to allow a query to be performed.
+ * Returns whether the change was successfully made.
+ */
+- (BOOL)_attemptMaxQuerySizeIncreaseTo:(NSUInteger)targetSize
+{
+
+ // If the query size is editable, attempt to increase the size
+ if ([self isMaxQuerySizeEditable]) {
+ NSUInteger newSize = [self setGlobalMaxQuerySize:targetSize];
+ if (newSize != NSNotFound) {
+
+ // Successfully increased the global size - reconnect to use it, and return success
+ [self reconnect];
+ return YES;
+ }
+ }
+
+ // Can not, or failed to, increase the max query size. Record an error message.
+ NSString *errorMessage = [NSString stringWithFormat:NSLocalizedString(@"The query length of %lu bytes is larger than max_allowed_packet size (%lu).", @"error message if max_allowed_packet < query size"), targetSize, maxQuerySize];
+ [self _updateLastErrorMessage:errorMessage];
+
+ // Update delegate error if it supports the protocol
+ if ([delegate respondsToSelector:@selector(queryGaveError:connection:)]) {
+ [delegate queryGaveError:errorMessage connection:self];
+ }
+
+ // Display an alert as this is a special failure
+ if ([delegate respondsToSelector:@selector(showErrorWithTitle:message:)]) {
+ [delegate showErrorWithTitle:NSLocalizedString(@"Error", @"error") message:errorMessage];
+ } else {
+ NSRunAlertPanel(NSLocalizedString(@"Error", @"error"), errorMessage, @"OK", nil, nil);
+ }
+
+ return NO;
+}
+
+/**
+ * Restore a maximum query size after temporarily increasing it for a query. This action
+ * may be called directly after a query, or may be before the next query if a streaming result
+ * had to be used.
+ */
+- (void)_restoreMaximumQuerySizeAfterQuery
+{
+
+ // Return if no action needs to be performed
+ if (queryActionShouldRestoreMaxQuerySize == NSNotFound) return;
+
+ // Move the target size to a local variable to prevent looping
+ NSUInteger targetMaxQuerySize = queryActionShouldRestoreMaxQuerySize;
+ queryActionShouldRestoreMaxQuerySize = NSNotFound;
+
+ // Enact the change
+ [self setGlobalMaxQuerySize:targetMaxQuerySize];
+}
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Ping & KeepAlive.h b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Ping & KeepAlive.h
new file mode 100644
index 00000000..e84c4ca6
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Ping & KeepAlive.h
@@ -0,0 +1,55 @@
+//
+// $Id$
+//
+// Ping & KeepAlive.h
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on January 14, 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/>
+
+// This class is private to the framework.
+
+typedef struct {
+ MYSQL *mySQLConnection;
+ BOOL *keepAlivePingActivePointer;
+ BOOL *keepAliveLastPingSuccessPointer;
+} SPMySQLConnectionPingDetails;
+
+@interface SPMySQLConnection (Ping_and_KeepAlive)
+
+// Keepalive ping initialisation
+- (void)_keepAlive;
+- (void)_threadedKeepAlive;
+
+// Master ping method
+- (BOOL)_pingConnectionUsingLoopDelay:(NSUInteger)loopDelay;
+
+// Ping thread internals
+void _backgroundPingTask(void *ptr);
+void _forceThreadExit(int signalNumber);
+void _pingThreadCleanup(void *pingDetails);
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Ping & KeepAlive.m b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Ping & KeepAlive.m
new file mode 100644
index 00000000..3ce0c0cd
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Ping & KeepAlive.m
@@ -0,0 +1,214 @@
+//
+// $Id$
+//
+// Ping & KeepAlive.m
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on January 14, 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 "Ping & KeepAlive.h"
+#import "Locking.h"
+#import <pthread.h>
+
+@implementation SPMySQLConnection (Ping_and_KeepAlive)
+
+#pragma mark -
+#pragma mark Keepalive ping initialisation
+
+/**
+ * Keeps the connection alive by running a ping.
+ * This method is called every ten seconds and spawns a thread which determines
+ * whether or not it should perform a ping.
+ */
+- (void)_keepAlive
+{
+
+ // Do nothing if not connected or if keepalive is disabled
+ if (state != SPMySQLConnected || !useKeepAlive) return;
+
+ // Check to see whether a ping is required. First, compare the last query
+ // and keepalive times against the keepalive interval.
+ // Compare against interval-1 to allow default keepalive intervals to repeat
+ // at the correct intervals (eg no timer interval delay).
+ uint64_t currentTime = mach_absolute_time();
+ if (_elapsedSecondsSinceAbsoluteTime(lastConnectionUsedTime) < keepAliveInterval - 1
+ || _elapsedSecondsSinceAbsoluteTime(lastKeepAliveTime) < keepAliveInterval - 1)
+ {
+ return;
+ }
+
+ // Attempt to lock the connection. If the connection is currently busy,
+ // we don't need a ping.
+ if (![self _tryLockConnection]) return;
+ [self _unlockConnection];
+
+ // Store the ping time
+ lastKeepAliveTime = currentTime;
+
+ [NSThread detachNewThreadSelector:@selector(_threadedKeepAlive) toTarget:self withObject:nil];
+}
+
+/**
+ * A threaded keepalive to avoid blocking the interface. Performs safety
+ * checks, and then creates a child pthread to actually ping the connection,
+ * forcing the thread to close after the timeout if it hasn't closed already.
+ */
+- (void)_threadedKeepAlive
+{
+
+ // If the maximum number of ping failures has been reached, trigger a reconnect
+ if (keepAliveLastPingBlocked || keepAlivePingFailures >= 3) {
+ [self reconnect];
+ return;
+ }
+
+ // Otherwise, perform a background ping.
+ BOOL pingResult = [self _pingConnectionUsingLoopDelay:10000];
+ if (pingResult) {
+ keepAlivePingFailures = 0;
+ } else {
+ keepAlivePingFailures++;
+ }
+}
+
+#pragma mark -
+#pragma mark Master ping method
+
+/**
+ * This function provides a method of pinging the remote server while also enforcing
+ * the specified connection time. This is required because low-level net reads can
+ * block indefinitely if the remote server disappears or on network issues - setting
+ * the MYSQL_OPT_READ_TIMEOUT (and the WRITE equivalent) would "fix" ping, but cause
+ * long queries to be terminated.
+ * The supplied loop delay number controls how tight the thread checking loop is, in
+ * microseconds, to allow differentiating foreground and background pings.
+ * Unlike mysql_ping, this function returns FALSE on failure and TRUE on success.
+ */
+- (BOOL)_pingConnectionUsingLoopDelay:(NSUInteger)loopDelay
+{
+ if (state != SPMySQLConnected) return NO;
+
+ uint64_t pingStartTime_t;
+ double pingElapsedTime;
+ BOOL threadCancelled = NO;
+
+ // Set up a query lock
+ [self _lockConnection];
+
+ keepAliveLastPingSuccess = NO;
+ keepAliveLastPingBlocked = NO;
+ keepAlivePingThreadActive = YES;
+
+ // Use a ping timeout defaulting to thirty seconds, but using the connection timeout if set
+ NSUInteger pingTimeout = 30;
+ if (timeout > 0) pingTimeout = timeout;
+
+ // Set up a struct containing details the ping task will need
+ SPMySQLConnectionPingDetails pingDetails;
+ pingDetails.mySQLConnection = mySQLConnection;
+ pingDetails.keepAliveLastPingSuccessPointer = &keepAliveLastPingSuccess;
+ pingDetails.keepAlivePingActivePointer = &keepAlivePingThreadActive;
+
+ // Create a pthread for the ping
+ pthread_attr_t attr;
+ pthread_attr_init(&attr);
+ pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
+ pthread_create(&keepAlivePingThread, &attr, (void *)&_backgroundPingTask, &pingDetails);
+
+ // Record the ping start time
+ pingStartTime_t = mach_absolute_time();
+
+ // Loop until the ping completes
+ do {
+ usleep((useconds_t)loopDelay);
+ pingElapsedTime = _elapsedSecondsSinceAbsoluteTime(pingStartTime_t);
+
+ // If the ping timeout has been exceeded, force a timeout; double-check that the
+ // thread is still active.
+ if (pingElapsedTime > pingTimeout && keepAlivePingThreadActive && !threadCancelled) {
+ pthread_cancel(keepAlivePingThread);
+ threadCancelled = YES;
+
+ // If the timeout has been exceeded by an additional two seconds, and the thread is
+ // still active, kill the thread. This can occur in certain network conditions causing
+ // a blocking read.
+ } else if (pingElapsedTime > (pingTimeout + 2) && keepAlivePingThreadActive) {
+ pthread_kill(keepAlivePingThread, SIGUSR1);
+ keepAlivePingThreadActive = NO;
+ keepAliveLastPingBlocked = YES;
+ }
+ } while (keepAlivePingThreadActive);
+
+ // Clean up
+ keepAlivePingThread = NULL;
+ pthread_attr_destroy(&attr);
+
+ // Unlock the connection
+ [self _unlockConnection];
+
+ return keepAliveLastPingSuccess;
+}
+
+#pragma mark -
+#pragma mark Ping thread internals
+
+/**
+ * Actually perform a keepalive ping - intended for use within a pthread.
+ */
+void _backgroundPingTask(void *ptr)
+{
+ SPMySQLConnectionPingDetails *pingDetails = (SPMySQLConnectionPingDetails *)ptr;
+
+ // Set up a cleanup routine
+ pthread_cleanup_push(_pingThreadCleanup, pingDetails);
+
+ // Set up a signal handler for SIGUSR1, to handle forced timeouts.
+ signal(SIGUSR1, _forceThreadExit);
+
+ // Perform a ping
+ *(pingDetails->keepAliveLastPingSuccessPointer) = (BOOL)(!mysql_ping(pingDetails->mySQLConnection));
+
+ // Call the cleanup routine
+ pthread_cleanup_pop(1);
+}
+
+/**
+ * Support forcing a thread to exit as a result of a signal.
+ */
+void _forceThreadExit(int signalNumber)
+{
+ pthread_exit(NULL);
+}
+
+void _pingThreadCleanup(void *pingDetails)
+{
+ SPMySQLConnectionPingDetails *pingDetailsStruct = pingDetails;
+ *(pingDetailsStruct->keepAlivePingActivePointer) = NO;
+}
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.h b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.h
new file mode 100644
index 00000000..0f086a89
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.h
@@ -0,0 +1,106 @@
+//
+// $Id$
+//
+// Querying & Preparation.h
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on January 14, 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/>
+
+
+@interface SPMySQLConnection (Querying_and_Preparation)
+
+// Data preparation
+- (NSString *)escapeAndQuoteString:(NSString *)theString;
+- (NSString *)escapeString:(NSString *)theString includingQuotes:(BOOL)includeQuotes;
+- (NSString *)escapeAndQuoteData:(NSData *)theData;
+- (NSString *)escapeData:(NSData *)theData includingQuotes:(BOOL)includeQuotes;
+
+// Queries
+- (SPMySQLResult *)queryString:(NSString *)theQueryString;
+- (SPMySQLFastStreamingResult *)streamingQueryString:(NSString *)theQueryString;
+- (id)streamingQueryString:(NSString *)theQueryString useLowMemoryBlockingStreaming:(BOOL)fullStreaming;
+- (id)queryString:(NSString *)theQueryString usingEncoding:(NSStringEncoding)theEncoding withResultType:(SPMySQLResultType)theReturnType;
+
+// Query convenience functions
+- (NSArray *)getAllRowsFromQuery:(NSString *)theQueryString;
+- (id)getFirstFieldFromQuery:(NSString *)theQueryString;
+
+// Query information
+- (unsigned long long)rowsAffectedByLastQuery;
+- (unsigned long long)lastInsertID;
+
+// Connection and query error state
+- (BOOL)queryErrored;
+- (NSString *)lastErrorMessage;
+- (NSUInteger)lastErrorID;
++ (BOOL)isErrorIDConnectionError:(NSUInteger)theErrorID;
+
+// Query cancellation
+- (void)cancelCurrentQuery;
+- (BOOL)lastQueryWasCancelledUsingReconnect;
+
+@end
+
+/**
+ * Set up static functions to allow fast calling with cached selectors
+ */
+
+static inline id SPMySQLConnectionEscapeString(SPMySQLConnection* self, NSString *theString, BOOL encloseInQuotes)
+{
+ typedef id (*SPMySQLConnectionEscapeStringMethodPtr)(SPMySQLConnection*, SEL, NSString *, BOOL);
+ static SPMySQLConnectionEscapeStringMethodPtr cachedMethodPointer;
+ static SEL cachedSelector;
+
+ if (!cachedSelector) cachedSelector = @selector(escapeString:includingQuotes:);
+ if (!cachedMethodPointer) cachedMethodPointer = (SPMySQLConnectionEscapeStringMethodPtr)[self methodForSelector:cachedSelector];
+
+ return cachedMethodPointer(self, cachedSelector, theString, encloseInQuotes);
+}
+
+static inline id SPMySQLConnectionEscapeData(SPMySQLConnection* self, NSData *theData, BOOL encloseInQuotes)
+{
+ typedef id (*SPMySQLConnectionEscapeDataMethodPtr)(SPMySQLConnection*, SEL, NSData *, BOOL);
+ static SPMySQLConnectionEscapeDataMethodPtr cachedMethodPointer;
+ static SEL cachedSelector;
+
+ if (!cachedSelector) cachedSelector = @selector(escapeData:includingQuotes:);
+ if (!cachedMethodPointer) cachedMethodPointer = (SPMySQLConnectionEscapeDataMethodPtr)[self methodForSelector:cachedSelector];
+
+ return cachedMethodPointer(self, cachedSelector, theData, encloseInQuotes);
+}
+
+static inline id SPMySQLConnectionQueryString(SPMySQLConnection* self, NSString *theQueryString, NSStringEncoding theEncoding, SPMySQLResultType theReturnType)
+{
+ typedef id (*SPMySQLConnectionQueryStringMethodPtr)(SPMySQLConnection*, SEL, NSString *, NSStringEncoding, SPMySQLResultType);
+ static SPMySQLConnectionQueryStringMethodPtr cachedMethodPointer;
+ static SEL cachedSelector;
+
+ if (!cachedSelector) cachedSelector = @selector(queryString:usingEncoding:withResultType:);
+ if (!cachedMethodPointer) cachedMethodPointer = (SPMySQLConnectionQueryStringMethodPtr)[self methodForSelector:cachedSelector];
+
+ return cachedMethodPointer(self, cachedSelector, theQueryString, theEncoding, theReturnType);
+}
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.m b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.m
new file mode 100644
index 00000000..9b54029c
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.m
@@ -0,0 +1,636 @@
+//
+// $Id$
+//
+// Querying & Preparation.m
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on January 14, 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 "SPMySQLConnection.h"
+#import "SPMySQL Private APIs.h"
+
+@implementation SPMySQLConnection (Querying_and_Preparation)
+
+#pragma mark -
+#pragma mark Data preparation
+
+/**
+ * See also the NSString methods mySQLTickQuotedString and mySQLBacktickQuotedString,
+ * added via an NSString category; however these methods are safer and more complete
+ * as they use the current connection encoding to quote characters.
+ */
+
+
+/**
+ * Take a string, escapes any special character, and surrounds it with single quotes
+ * for safe use within a query; correctly escapes any characters within the string
+ * using the current connection encoding.
+ */
+- (NSString *)escapeAndQuoteString:(NSString *)theString
+{
+ return SPMySQLConnectionEscapeString(self, theString, YES);
+}
+
+/**
+ * Take a string and escapes any special character for safe use within a query; correctly
+ * escapes any characters within the string using the current connection encoding.
+ * Allows control over whether to also wrap the string in single quotes.
+ */
+- (NSString *)escapeString:(NSString *)theString includingQuotes:(BOOL)includeQuotes
+{
+
+ // Return nil strings untouched
+ if (!theString) return theString;
+
+ // To correctly escape the string, an active connection is required, so verify.
+ if (state == SPMySQLDisconnected || state == SPMySQLConnecting) {
+ if ([delegate respondsToSelector:@selector(noConnectionAvailable:)]) {
+ [delegate noConnectionAvailable:self];
+ }
+ return nil;
+ }
+ if (![self _checkConnectionIfNecessary]) return nil;
+
+ // Perform a lossy conversion to bytes, using NSData to do the hard work. Preserves
+ // nul characters correctly.
+ NSData *cData = [theString dataUsingEncoding:stringEncoding allowLossyConversion:YES];
+ NSUInteger cDataLength = [cData length];
+
+ // Create a buffer for mysql_real_escape_string to place the converted string into;
+ // the max length is 2*length (if every character was quoted) + 2 (quotes/terminator).
+ // Adding quotes in this way makes the logic below *slightly* harder to follow but
+ // makes the addition of the quotes almost free, which is much nicer when building
+ // lots of strings.
+ char *escBuffer = (char *)malloc((cDataLength * 2) + 2);
+
+ // Use mysql_real_escape_string to perform the escape, starting one character in
+ NSUInteger escapedLength = mysql_real_escape_string(mySQLConnection, escBuffer+1, [cData bytes], cDataLength);
+
+ // Set up an NSData object to allow conversion back to NSString while preserving
+ // any nul characters contained in the string.
+ NSData *escapedData;
+ if (includeQuotes) {
+
+ // Add quotes if requested
+ escBuffer[0] = '\'';
+ escBuffer[escapedLength+1] = '\'';
+
+ escapedData = [NSData dataWithBytesNoCopy:escBuffer length:escapedLength+2 freeWhenDone:NO];
+ } else {
+ escapedData = [NSData dataWithBytesNoCopy:escBuffer+1 length:escapedLength freeWhenDone:NO];
+ }
+
+ // Convert to the string to return
+ NSString *escapedString = [[NSString alloc] initWithData:escapedData encoding:stringEncoding];
+
+ // Free up any memory and return
+ free(escBuffer);
+ return [escapedString autorelease];
+}
+
+/**
+ * Take NSData and hex-encodes the contents for safe transmission to a server,
+ * preserving all bytes whatever the encoding. Surrounds the hex-encoded resulting
+ * string with single quotes and precedes it with the hex-marker X for safe inclusion
+ * in a query.
+ */
+- (NSString *)escapeAndQuoteData:(NSData *)theData
+{
+ return SPMySQLConnectionEscapeData(self, theData, YES);
+}
+
+/**
+ * Takes NSData and hex-encodes the contents for safe transmission to a server,
+ * preserving all bytes whatever the encoding.
+ * Allows control over whether to also wrap the string in single quotes and a
+ * preceding X (X'...') for safe use in queries.
+ */
+- (NSString *)escapeData:(NSData *)theData includingQuotes:(BOOL)includeQuotes
+{
+
+ // Return nil datas as nil strings
+ if (!theData) return nil;
+
+ NSUInteger dataLength = [theData length];
+
+ // Create a buffer for mysql_real_escape_string to place the converted string into;
+ // the max length is 2*length (if every character was quoted) + 3 (quotes/terminator).
+ // Adding quotes in this way makes the logic below *slightly* harder to follow but
+ // makes the addition of the quotes almost free, which is much nicer when building
+ // lots of strings.
+ char *hexBuffer = (char *)malloc((dataLength * 2) + 3);
+
+ // Use mysql_hex_string to perform the escape, starting two characters in
+ NSUInteger hexLength = mysql_hex_string(hexBuffer+2, [theData bytes], dataLength);
+
+ // Set up the return NSString
+ NSString *hexString;
+ if (includeQuotes) {
+
+ // Add quotes if requested
+ hexBuffer[0] = 'X';
+ hexBuffer[1] = '\'';
+ hexBuffer[hexLength+2] = '\'';
+
+ hexString = [[NSString alloc] initWithBytes:hexBuffer length:hexLength+3 encoding:NSASCIIStringEncoding];
+ } else {
+ hexString = [[NSString alloc] initWithBytes:hexBuffer+2 length:hexLength encoding:NSASCIIStringEncoding];
+ }
+
+ // Free up any memory and return
+ free(hexBuffer);
+ return [hexString autorelease];
+}
+
+#pragma mark -
+#pragma mark Queries
+
+/**
+ * Run a query, provided as a string, on the active connection in the current connection
+ * encoding. Stores all the results before returning the complete result set.
+ */
+- (SPMySQLResult *)queryString:(NSString *)theQueryString
+{
+ return SPMySQLConnectionQueryString(self, theQueryString, stringEncoding, SPMySQLResultAsResult);
+}
+
+/**
+ * Run a query, provided as a string, on the active connection in the current connection
+ * encoding. Returns the result as a fast streaming query set, where not all the results
+ * may be available at time of return.
+ */
+- (SPMySQLFastStreamingResult *)streamingQueryString:(NSString *)theQueryString
+{
+ return SPMySQLConnectionQueryString(self, theQueryString, stringEncoding, SPMySQLResultAsFastStreamingResult);
+}
+
+/**
+ * Run a query, provided as a string, on the active connection in the current connection
+ * encoding. Returns the result as a streaming query set, where not all the results may
+ * be available at time of return.
+ * Supports a flag specifying whether streaming should be low-memory blocking (results are
+ * read from the server as the code retrives them, possibly blocking other queries on the
+ * server) or fast streaming (results are cached in the result object as fast as possible,
+ * freeing up the server even in the local rows are still being read from the result object).
+ * Will return a SPMySQLStreamingResult or SPMySQLFastStreamingResult as appropriate.
+ */
+- (id)streamingQueryString:(NSString *)theQueryString useLowMemoryBlockingStreaming:(BOOL)fullStreaming
+{
+ return SPMySQLConnectionQueryString(self, theQueryString, stringEncoding, fullStreaming?SPMySQLResultAsLowMemStreamingResult:SPMySQLResultAsFastStreamingResult);
+}
+
+/**
+ * Run a query, provided as a string, on the active connection. The query and its result
+ * set are interpreted according to the supplied encoding, which should usually match
+ * the connection encoding.
+ * The result type desired can be specified, supporting either standard or streaming
+ * result sets.
+ */
+- (id)queryString:(NSString *)theQueryString usingEncoding:(NSStringEncoding)theEncoding withResultType:(SPMySQLResultType)theReturnType
+{
+ double queryExecutionTime;
+ lastQueryWasCancelled = NO;
+ lastQueryWasCancelledUsingReconnect = NO;
+
+ // Check the connection state - if no connection is available, log an
+ // error and return.
+ if (state == SPMySQLDisconnected || state == SPMySQLConnecting) {
+ if ([delegate respondsToSelector:@selector(queryGaveError:connection:)]) {
+ [delegate queryGaveError:@"No connection available!" connection:self];
+ }
+ if ([delegate respondsToSelector:@selector(noConnectionAvailable:)]) {
+ [delegate noConnectionAvailable:self];
+ }
+ return nil;
+ }
+
+ // Check the connection if necessary, returning nil if the query couldn't be validated
+ if (![self _checkConnectionIfNecessary]) return nil;
+
+ // Determine whether a maximum query size needs to be restored from a previous query
+ if (queryActionShouldRestoreMaxQuerySize != NSNotFound) {
+ [self _restoreMaximumQuerySizeAfterQuery];
+ }
+
+ // If delegate logging is enabled, and the protocol is implemented, inform the delegate
+ if (delegateQueryLogging && delegateSupportsWillQueryString) {
+ [delegate willQueryString:theQueryString connection:self];
+ }
+
+ // Retrieve a C-style query string from the supplied NSString
+ NSUInteger cQueryStringLength;
+ const char *cQueryString = _cStringForStringWithEncoding(theQueryString, theEncoding, &cQueryStringLength);
+
+ // Check the query length against the current maximum query length. If it is
+ // larger, the query would error (and probably cause a disconnect), so if
+ // the maximum size is editable, increase it and reconnect.
+ if (cQueryStringLength > maxQuerySize) {
+ queryActionShouldRestoreMaxQuerySize = maxQuerySize;
+ if (![self _attemptMaxQuerySizeIncreaseTo:(cQueryStringLength + 1024)]) {
+ queryActionShouldRestoreMaxQuerySize = NSNotFound;
+ return nil;
+ }
+ }
+
+ // Prepare to enter a loop to run the query, allowing reattempts if appropriate
+ NSUInteger queryAttemptsAllowed = 1;
+ if (retryQueriesOnConnectionFailure) queryAttemptsAllowed++;
+ int queryStatus;
+
+ // Lock the connection while it's actively in use
+ [self _lockConnection];
+
+ while (queryAttemptsAllowed > 0) {
+
+ // While recording the overall execution time (including network lag!), run
+ // the raw query
+ uint64_t queryStartTime = mach_absolute_time();
+ queryStatus = mysql_real_query(mySQLConnection, cQueryString, cQueryStringLength);
+ queryExecutionTime = _elapsedSecondsSinceAbsoluteTime(queryStartTime);
+ lastConnectionUsedTime = mach_absolute_time();
+
+ // If the query succeeded, no need to re-attempt.
+ if (!queryStatus) {
+ break;
+
+ // If the query failed, determine whether to reattempt the query
+ } else {
+
+ // Prevent retries if the query was cancelled or not a connection error
+ if (lastQueryWasCancelled || ![SPMySQLConnection isErrorIDConnectionError:mysql_errno(mySQLConnection)]) {
+ break;
+ }
+ }
+
+ // Query has failed - check the connection
+ if (![self checkConnection]) {
+ [self _unlockConnection];
+ return nil;
+ }
+
+ queryAttemptsAllowed--;
+ }
+
+ unsigned long long theAffectedRowCount = mysql_affected_rows(mySQLConnection);
+ id theResult = nil;
+
+ // On success, if there is a query result, retrieve the result data type
+ if (!queryStatus && mysql_field_count(mySQLConnection)) {
+ MYSQL_RES *mysqlResult;
+
+ switch (theReturnType) {
+
+ // For standard result sets, retrieve all the results now, and afterwards
+ // update the affected row count.
+ case SPMySQLResultAsResult:
+ mysqlResult = mysql_store_result(mySQLConnection);
+ theResult = [[SPMySQLResult alloc] initWithMySQLResult:mysqlResult stringEncoding:theEncoding];
+ theAffectedRowCount = mysql_affected_rows(mySQLConnection);
+ break;
+
+ // For fast streaming and low memory streaming result sets, set up the result
+ case SPMySQLResultAsLowMemStreamingResult:
+ mysqlResult = mysql_use_result(mySQLConnection);
+ theResult = [[SPMySQLStreamingResult alloc] initWithMySQLResult:mysqlResult stringEncoding:theEncoding connection:self];
+ break;
+
+ case SPMySQLResultAsFastStreamingResult:
+ mysqlResult = mysql_use_result(mySQLConnection);
+ theResult = [[SPMySQLFastStreamingResult alloc] initWithMySQLResult:mysqlResult stringEncoding:theEncoding connection:self];
+ break;
+ }
+ }
+
+ // Record the error state now, as it may be affected by subsequent clean-up queries
+ NSString *theErrorMessage = [self _stringForCString:mysql_error(mySQLConnection)];
+ NSUInteger theErrorID = mysql_errno(mySQLConnection);
+
+ // If the query was cancelled, override the error state
+ if (lastQueryWasCancelled) {
+ theErrorMessage = NSLocalizedString(@"Query cancelled.", @"Query cancelled error");
+ theErrorID = 1317;
+ }
+
+ // Unlock the connection if appropriate - if not a streaming result type.
+ if (![theResult isKindOfClass:[SPMySQLStreamingResult class]]) {
+ [self _unlockConnection];
+
+ // Also perform restore if appropriate
+ if (queryActionShouldRestoreMaxQuerySize != NSNotFound) {
+ [self _restoreMaximumQuerySizeAfterQuery];
+ }
+ }
+
+ // Update error string and ID, and the rows affected
+ [self _updateLastErrorMessage:theErrorMessage];
+ [self _updateLastErrorID:theErrorID];
+ lastQueryAffectedRowCount = theAffectedRowCount;
+
+ // Store the result time on the response object
+ [theResult _setQueryExecutionTime:queryExecutionTime];
+
+ return [theResult autorelease];
+}
+
+#pragma mark -
+#pragma mark Query convenience functions
+
+/**
+ * Run a query and retrieve the entire result set as an array of dictionaries.
+ * Returns nil if there was a problem running the query or retrieving any results.
+ */
+- (NSArray *)getAllRowsFromQuery:(NSString *)theQueryString
+{
+ return [[self queryString:theQueryString] getAllRows];
+}
+
+/**
+ * Run a query and retrieve the first field of any response. Returns nil if there
+ * was a problem running the query or retrieving any results.
+ */
+- (id)getFirstFieldFromQuery:(NSString *)theQueryString
+{
+ return [[[self queryString:theQueryString] getRowAsArray] objectAtIndex:0];
+}
+
+#pragma mark -
+#pragma mark Query information
+
+/**
+ * Returns the number of rows changed, deleted, inserted, or selected by
+ * the last query.
+ */
+- (unsigned long long)rowsAffectedByLastQuery
+{
+ return lastQueryAffectedRowCount;
+}
+
+/**
+ * Returns the insert ID for the previous query which inserted a row. Note that
+ * this value persists through other SELECT/UPDATE etc queries.
+ */
+- (unsigned long long)lastInsertID
+{
+ return lastQueryInsertID;
+}
+
+#pragma mark -
+#pragma mark Retrieving connection and query error state
+
+/**
+ * Return whether the last query errored or not.
+ */
+- (BOOL)queryErrored
+{
+ return (queryErrorMessage)?YES:NO;
+}
+
+/**
+ * If the last query (or connection) triggered an error, returns the error
+ * message as a string; if the last query did not error, nil is returned.
+ */
+- (NSString *)lastErrorMessage
+{
+ return queryErrorMessage;
+}
+
+/**
+ * If the last query (or connection) triggered an error, returns the error
+ * ID; if the last query did not error, 0 is returned.
+ */
+- (NSUInteger)lastErrorID
+{
+ return queryErrorID;
+}
+
+/**
+ * Determines whether a supplied error ID can be classed as a connection error.
+ */
++ (BOOL)isErrorIDConnectionError:(NSUInteger)theErrorID
+{
+ switch (theErrorID) {
+ case 2001: // CR_SOCKET_CREATE_ERROR
+ case 2002: // CR_CONNECTION_ERROR
+ case 2003: // CR_CONN_HOST_ERROR
+ case 2004: // CR_IPSOCK_ERROR
+ case 2005: // CR_UNKNOWN_HOST
+ case 2006: // CR_SERVER_GONE_ERROR
+ case 2007: // CR_VERSION_ERROR
+ case 2009: // CR_WRONG_HOST_INFO
+ case 2012: // CR_SERVER_HANDSHAKE_ERR
+ case 2013: // CR_SERVER_LOST
+ case 2027: // CR_MALFORMED_PACKET
+ case 2032: // CR_DATA_TRUNCATED
+ case 2047: // CR_CONN_UNKNOW_PROTOCOL
+ case 2048: // CR_INVALID_CONN_HANDLE
+ case 2050: // CR_FETCH_CANCELED
+ case 2055: // CR_SERVER_LOST_EXTENDED
+ return YES;
+ }
+
+ return NO;
+}
+
+#pragma mark -
+#pragma mark Query cancellation
+
+/**
+ * Cancel the currently running query. This tries to kill the current query,
+ * and if that isn't possible - for example, on MySQL < 5 or if the current user
+ * does not have the relevant permissions - resets the connection.
+ */
+- (void)cancelCurrentQuery
+{
+
+ // If not connected, no action is required
+ if (state != SPMySQLConnected && state != SPMySQLDisconnecting) return;
+
+ // Check whether a query is actually being performed - if not, return
+ if ([self _tryLockConnection]) {
+ [self _unlockConnection];
+ return;
+ }
+
+ // Mark that the last query was cancelled to prevent query retries from occurring
+ lastQueryWasCancelled = YES;
+
+ // The query cancellation cannot occur on the connection actively running a query
+ // so set up a new connection to run the KILL command.
+ MYSQL *killerConnection = [self _makeRawMySQLConnectionWithEncoding:@"utf8" isMasterConnection:NO];
+
+
+ // If the new connection was successfully set up, use it to run a KILL command.
+ if (killerConnection) {
+ NSStringEncoding aStringEncoding = [SPMySQLConnection stringEncodingForMySQLCharset:mysql_character_set_name(killerConnection)];
+ BOOL killQuerySupported = [self serverVersionIsGreaterThanOrEqualTo:5 minorVersion:0 releaseVersion:0];
+
+ // Build the kill query
+ NSMutableString *killQuery = [NSMutableString stringWithString:@"KILL"];
+ if (killQuerySupported) [killQuery appendString:@" QUERY"];
+ [killQuery appendFormat:@" %lu", mySQLConnection->thread_id];
+
+ // Convert to a C string
+ NSUInteger killQueryCStringLength;
+ const char *killQueryCString = [SPMySQLConnection _cStringForString:killQuery usingEncoding:aStringEncoding returningLengthAs:&killQueryCStringLength];
+
+ // Run the query
+ int killQueryStatus = mysql_real_query(killerConnection, killQueryCString, killQueryCStringLength);
+
+ // Close the temporary connection
+ mysql_close(killerConnection);
+
+ // If the kill query succeeded, the active query was cancelled.
+ if (killQueryStatus == 0) {
+
+ // On MySQL < 5, the entire connection will have been reset. Ensure it's
+ // restored.
+ if (!killQuerySupported) {
+ [self checkConnection];
+ lastQueryWasCancelledUsingReconnect = YES;
+ } else {
+ lastQueryWasCancelledUsingReconnect = NO;
+ }
+
+ // Ensure the tracking bool is re-set to cover encompassed queries and return
+ lastQueryWasCancelled = YES;
+ return;
+ } else {
+ NSLog(@"SPMySQL Framework: query cancellation failed due to cancellation query error (status %d)", killQueryStatus);
+ }
+ } else {
+ NSLog(@"SPMySQL Framework: query cancellation failed because connection failed");
+ }
+
+ // A full reconnect is required at this point to force a cancellation. As the
+ // connection may have finished processing the query at this point (depending how
+ // long the connection attempt took), check whether we can skip the reconnect.
+ if ([self _tryLockConnection]) {
+ [self _unlockConnection];
+ return;
+ }
+
+ if (state == SPMySQLDisconnecting) return;
+
+ // Reset the connection with a reconnect. Unlock the connection beforehand,
+ // to allow the reconnect, but lock it again afterwards to restore the expected
+ // state (query execution process should unlock as appropriate).
+ [self _unlockConnection];
+ [self reconnect];
+ [self _lockConnection];
+
+ // Reset tracking bools to cover encompassed queries
+ lastQueryWasCancelled = YES;
+ lastQueryWasCancelledUsingReconnect = YES;
+}
+
+/**
+ * If the last query was cancelled, returns whether that query cancellation
+ * required the connection to be reset or whether the query was successfully
+ * cancelled leaving the connection intact.
+ * If the last query was not cancelled, this will return NO.
+ */
+- (BOOL)lastQueryWasCancelledUsingReconnect
+{
+ return lastQueryWasCancelledUsingReconnect;
+}
+
+@end
+
+#pragma mark -
+#pragma mark Private API
+
+@implementation SPMySQLConnection (Querying_and_Preparation_Private_API)
+
+/**
+ * Retrieves all remaining results and discards them.
+ * This is necessary to correctly process multiple result sets on the connection - as
+ * we currently don't fully support multiple result, this at least allows the connection
+ * to function after running statements with multiple result sets.
+ */
+- (void)_flushMultipleResultSets
+{
+
+ // Repeat as long as there are results
+ while (!mysql_next_result(mySQLConnection)) {
+ MYSQL_RES *eachResult = mysql_use_result(mySQLConnection);
+
+ // Ensure the result is really a result
+ if (eachResult) {
+
+ // Retrieve and discard all rows
+ while (mysql_fetch_row(eachResult));
+
+ // Free the result set
+ mysql_free_result(eachResult);
+ }
+ }
+}
+
+/**
+ * Update the MySQL error message for this connection. If an error is supplied
+ * it will be stored and returned to anything asking the instance for the last
+ * error; if no error is supplied, the connection will be used to derive (or clear)
+ * the error string.
+ */
+- (void)_updateLastErrorMessage:(NSString *)theErrorMessage
+{
+
+ // If an error message wasn't supplied, select one from the connection
+ if (!theErrorMessage) {
+ theErrorMessage = [self _stringForCString:mysql_error(mySQLConnection)];
+ }
+
+ // Clear the last error message stored on the instance
+ if (queryErrorMessage) [queryErrorMessage release], queryErrorMessage = nil;
+
+ // If we have an error message *with a length*, update the instance error message
+ if (theErrorMessage && [theErrorMessage length]) {
+ queryErrorMessage = [[NSString alloc] initWithString:theErrorMessage];
+ }
+}
+
+/**
+ * Update the MySQL error ID for this connection. If an error ID is supplied,
+ * it will be stored and returned to anything asking the instance for the last
+ * error; if an NSNotFound error ID is supplied, the connection will be used to
+ * set the error ID. Note that an error ID of 0 corresponds to no error.
+ */
+- (void)_updateLastErrorID:(NSUInteger)theErrorID
+{
+
+ // If NSNotFound was supplied as the ID, ask the connection for the last error
+ if (theErrorID == NSNotFound) {
+ queryErrorID = mysql_errno(mySQLConnection);
+
+ // Otherwise, update the error ID with the supplied ID
+ } else {
+ queryErrorID = theErrorID;
+ }
+}
+
+@end \ No newline at end of file
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Server Info.h b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Server Info.h
new file mode 100644
index 00000000..d8f5f183
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Server Info.h
@@ -0,0 +1,50 @@
+//
+// $Id$
+//
+// Server Info.h
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on January 14, 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/>
+
+@class SPMySQLResult;
+
+@interface SPMySQLConnection (Server_Info)
+
+// Server version information
+- (NSString *)serverVersionString;
+- (NSUInteger)serverMajorVersion;
+- (NSUInteger)serverMinorVersion;
+- (NSUInteger)serverReleaseVersion;
+
+// Server version comparisons
+- (BOOL)serverVersionIsGreaterThanOrEqualTo:(NSUInteger)aMajorVersion minorVersion:(NSUInteger)aMinorVersion releaseVersion:(NSUInteger)aReleaseVersion;
+
+// Server tasks & processes
+- (SPMySQLResult *)listProcesses;
+- (BOOL)killQueryOnThreadID:(unsigned long)theThreadID;
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Server Info.m b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Server Info.m
new file mode 100644
index 00000000..f695d977
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Server Info.m
@@ -0,0 +1,175 @@
+//
+// $Id$
+//
+// Server Info.m
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on January 14, 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 "Server Info.h"
+#import "SPMySQL Private APIs.h"
+
+@implementation SPMySQLConnection (Server_Info)
+
+#pragma mark -
+#pragma mark Server version information
+
+/**
+ * Return the server version string, or nil on failure.
+ */
+- (NSString *)serverVersionString
+{
+ if (serverVersionString) {
+ return [NSString stringWithString:serverVersionString];
+ }
+
+ return nil;
+}
+
+/**
+ * Return the server major version or NSNotFound on failure
+ */
+- (NSUInteger)serverMajorVersion
+{
+
+ if (serverVersionString != nil) {
+ NSString *s = [[serverVersionString componentsSeparatedByString:@"."] objectAtIndex:0];
+ return (NSUInteger)[s integerValue];
+ }
+
+ return NSNotFound;
+}
+
+/**
+ * Return the server minor version or NSNotFound on failure
+ */
+- (NSUInteger)serverMinorVersion
+{
+ if (serverVersionString != nil) {
+ NSString *s = [[serverVersionString componentsSeparatedByString:@"."] objectAtIndex:1];
+ return (NSUInteger)[s integerValue];
+ }
+
+ return NSNotFound;
+}
+
+/**
+ * Return the server release version or NSNotFound on failure
+ */
+- (NSUInteger)serverReleaseVersion
+{
+ if (serverVersionString != nil) {
+ NSString *s = [[serverVersionString componentsSeparatedByString:@"."] objectAtIndex:2];
+ return (NSUInteger)[[[s componentsSeparatedByString:@"-"] objectAtIndex:0] integerValue];
+ }
+
+ return NSNotFound;
+}
+
+#pragma mark -
+#pragma mark Server version comparisons
+
+/**
+ * Returns whether the connected server version is greater than or equal to the
+ * supplied version number. Returns NO if no connection is active.
+ */
+- (BOOL)serverVersionIsGreaterThanOrEqualTo:(NSUInteger)aMajorVersion minorVersion:(NSUInteger)aMinorVersion releaseVersion:(NSUInteger)aReleaseVersion
+{
+ if (!serverVersionString) return NO;
+
+ NSArray *serverVersionParts = [serverVersionString componentsSeparatedByString:@"."];
+
+ NSUInteger serverMajorVersion = (NSUInteger)[[serverVersionParts objectAtIndex:0] integerValue];
+ if (serverMajorVersion < aMajorVersion) return NO;
+ if (serverMajorVersion > aMajorVersion) return YES;
+
+ NSUInteger serverMinorVersion = (NSUInteger)[[serverVersionParts objectAtIndex:1] integerValue];
+ if (serverMinorVersion < aMinorVersion) return NO;
+ if (serverMinorVersion > aMinorVersion) return YES;
+
+ NSString *serverReleasePart = [serverVersionParts objectAtIndex:2];
+ NSUInteger serverReleaseVersion = (NSUInteger)[[[serverReleasePart componentsSeparatedByString:@"-"] objectAtIndex:0] integerValue];
+ if (serverReleaseVersion < aReleaseVersion) return NO;
+ return YES;
+}
+
+#pragma mark -
+#pragma mark Server tasks & processes
+
+/**
+ * Returns a result set describing the current server threads and their tasks. Note that
+ * the resulting process list defaults to the short form; run a manual SHOW FULL PROCESSLIST
+ * to retrieve tasks in non-truncated form.
+ * Returns nil on error.
+ */
+- (SPMySQLResult *)listProcesses
+{
+ if (state != SPMySQLConnected) return nil;
+
+ // Check the connection if appropriate
+ if (![self _checkConnectionIfNecessary]) return nil;
+
+ // Lock the connection before using it
+ [self _lockConnection];
+
+ // Get the process list
+ MYSQL_RES *mysqlResult = mysql_list_processes(mySQLConnection);
+
+ // Convert to SPMySQLResult
+ SPMySQLResult *theResult = [[SPMySQLResult alloc] initWithMySQLResult:mysqlResult stringEncoding:stringEncoding];
+
+ // Unlock and return
+ [self _unlockConnection];
+ return [theResult autorelease];
+}
+
+/**
+ * Kill the process with the supplied thread ID. On MySQL version 5 or later, this kills
+ * the query; on older servers this kills the entire connection. Note that the SUPER
+ * privilege is required to kill queries and processes not belonging to the currently
+ * connected user, while only PROCESS is required to see other user's processes.
+ * Returns a boolean indicating success or failure.
+ */
+- (BOOL)killQueryOnThreadID:(unsigned long)theThreadID
+{
+
+ // Note that mysql_kill has been deprecated, so use a query to perform this task.
+ NSMutableString *killQuery = [NSMutableString stringWithString:@"KILL"];
+ if ([self serverVersionIsGreaterThanOrEqualTo:5 minorVersion:0 releaseVersion:0]) {
+ [killQuery appendString:@" QUERY"];
+ }
+ [killQuery appendFormat:@" %lu", theThreadID];
+
+ // Run the query
+ [self queryString:killQuery];
+
+ // Return a value based on whether the query errored or not
+ return ![self queryErrored];
+}
+
+@end \ No newline at end of file
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection.h b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection.h
new file mode 100644
index 00000000..8f3b7f9f
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection.h
@@ -0,0 +1,181 @@
+//
+// $Id$
+//
+// SPMySQLConnection.h
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on January 8, 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/>
+
+@class SPMySQLKeepAliveTimer;
+
+@interface SPMySQLConnection : NSObject {
+
+ // Delegate
+ NSObject <SPMySQLConnectionDelegate> *delegate;
+ BOOL delegateSupportsWillQueryString;
+ BOOL delegateSupportsConnectionLost;
+ BOOL delegateQueryLogging; // Defaults to YES if protocol implemented
+
+ // Basic connection details
+ NSString *host;
+ NSString *username;
+ NSString *password;
+ NSUInteger port;
+ BOOL useSocket;
+ NSString *socketPath;
+
+ // SSL connection details
+ BOOL useSSL;
+ NSString *sslKeyFilePath;
+ NSString *sslCertificatePath;
+ NSString *sslCACertificatePath;
+
+ // MySQL connection details and state
+ struct st_mysql *mySQLConnection;
+ SPMySQLConnectionState state;
+ BOOL connectedWithSSL;
+ BOOL userTriggeredDisconnect;
+ BOOL isReconnecting;
+ uint64_t initialConnectTime;
+ unsigned long mysqlConnectionThreadId;
+
+ // Connection proxy
+ NSObject <SPMySQLConnectionProxy> *proxy;
+ SPMySQLConnectionProxyState previousProxyState;
+ BOOL proxyStateChangeNotificationsIgnored;
+
+ // Connection lock to prevent non-thread-safe query misuse
+ NSConditionLock *connectionLock;
+
+ // Currently selected database
+ NSString *database;
+
+ // Delegate connection lost decisions
+ NSUInteger reconnectionRetryAttempts;
+ SPMySQLConnectionLostDecision lastDelegateDecisionForLostConnection;
+ NSLock *delegateDecisionLock;
+
+ // Timeout and keep-alive
+ NSUInteger timeout;
+ BOOL useKeepAlive;
+ SPMySQLKeepAliveTimer *keepAliveTimer;
+ CGFloat keepAliveInterval;
+ uint64_t lastKeepAliveTime;
+ NSUInteger keepAlivePingFailures;
+ pthread_t keepAlivePingThread;
+ BOOL keepAlivePingThreadActive;
+ BOOL keepAliveLastPingSuccess;
+ BOOL keepAliveLastPingBlocked;
+
+ // Encoding details - and also a record of any previous encoding to allow
+ // switching back and forth
+ NSString *encoding;
+ NSStringEncoding stringEncoding;
+ BOOL encodingUsesLatin1Transport;
+ NSString *previousEncoding;
+ BOOL previousEncodingUsesLatin1Transport;
+
+ // Server details
+ NSString *serverVersionString;
+
+ // Error state for the last query or connection state
+ NSUInteger queryErrorID;
+ NSString *queryErrorMessage;
+
+ // Query details
+ unsigned long long lastQueryAffectedRowCount;
+ unsigned long long lastQueryInsertID;
+
+ // Query cancellation details
+ BOOL lastQueryWasCancelled;
+ BOOL lastQueryWasCancelledUsingReconnect;
+
+ // Timing details
+ uint64_t lastConnectionUsedTime;
+ double lastQueryExecutionTime;
+
+ // Maximum query size
+ NSUInteger maxQuerySize;
+ BOOL maxQuerySizeIsEditable;
+ BOOL maxQuerySizeEditabilityChecked;
+ NSUInteger queryActionShouldRestoreMaxQuerySize;
+
+ // Queries
+ BOOL retryQueriesOnConnectionFailure;
+}
+
+#pragma mark -
+#pragma mark Synthesized properties
+
+@property (readwrite, assign, nonatomic) NSObject <SPMySQLConnectionDelegate> *delegate;
+@property (readwrite, assign, nonatomic) NSObject <SPMySQLConnectionProxy> *proxy;
+
+@property (readwrite, retain) NSString *host;
+@property (readwrite, retain) NSString *username;
+@property (readwrite, retain) NSString *password;
+@property (readwrite, assign) NSUInteger port;
+@property (readwrite, assign) BOOL useSocket;
+@property (readwrite, retain) NSString *socketPath;
+
+@property (readwrite, assign) BOOL useSSL;
+@property (readwrite, retain) NSString *sslKeyFilePath;
+@property (readwrite, retain) NSString *sslCertificatePath;
+@property (readwrite, retain) NSString *sslCACertificatePath;
+
+@property (readwrite, assign) NSUInteger timeout;
+@property (readwrite, assign) BOOL useKeepAlive;
+@property (readwrite, assign) CGFloat keepAliveInterval;
+
+@property (readonly) unsigned long mysqlConnectionThreadId;
+@property (readwrite, assign) BOOL retryQueriesOnConnectionFailure;
+
+@property (readwrite, assign) BOOL delegateQueryLogging;
+
+@property (readwrite, assign) BOOL lastQueryWasCancelled;
+
+#pragma mark -
+#pragma mark Connection and disconnection
+
+- (BOOL)connect;
+- (BOOL)reconnect;
+- (void)disconnect;
+
+#pragma mark -
+#pragma mark Connection state
+
+- (BOOL)isConnected;
+- (BOOL)isConnectedViaSSL;
+- (BOOL)checkConnection;
+- (double)timeConnected;
+- (BOOL)userTriggeredDisconnect;
+
+#pragma mark -
+#pragma mark Connection utility
+
++ (NSString *)findSocketPath;
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection.m b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection.m
new file mode 100644
index 00000000..6308a3f5
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection.m
@@ -0,0 +1,834 @@
+//
+// $Id$
+//
+// SPMySQLConnection.m
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on January 8, 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 "SPMySQL Private APIs.h"
+#import "SPMySQLKeepAliveTimer.h"
+#include <mach/mach_time.h>
+#include <pthread.h>
+#include <SystemConfiguration/SCNetworkReachability.h>
+
+
+#pragma mark Class constants
+
+// The default connection options for MySQL connections
+const NSUInteger SPMySQLConnectionOptions =
+ CLIENT_COMPRESS | // Enable protocol compression - almost always a win
+ CLIENT_INTERACTIVE | // Mark ourselves as an interactive client
+ CLIENT_MULTI_RESULTS; // Multiple result support (very basic, but present)
+
+// List of permissible ciphers to use for SSL connections
+const char *SPMySQLSSLPermissibleCiphers = "DHE-RSA-AES256-SHA:AES256-SHA:DHE-RSA-AES128-SHA:AES128-SHA:AES256-RMD:AES128-RMD:DES-CBC3-RMD:DHE-RSA-AES256-RMD:DHE-RSA-AES128-RMD:DHE-RSA-DES-CBC3-RMD:RC4-SHA:RC4-MD5:DES-CBC3-SHA:DES-CBC-SHA:EDH-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC-SHA";
+
+
+@implementation SPMySQLConnection
+
+#pragma mark -
+#pragma mark Synthesized properties
+
+@synthesize delegate;
+@synthesize proxy;
+@synthesize host;
+@synthesize username;
+@synthesize password;
+@synthesize port;
+@synthesize useSocket;
+@synthesize socketPath;
+@synthesize useSSL;
+@synthesize sslKeyFilePath;
+@synthesize sslCertificatePath;
+@synthesize sslCACertificatePath;
+@synthesize timeout;
+@synthesize useKeepAlive;
+@synthesize keepAliveInterval;
+@synthesize mysqlConnectionThreadId;
+@synthesize retryQueriesOnConnectionFailure;
+@synthesize delegateQueryLogging;
+@synthesize lastQueryWasCancelled;
+
+#pragma mark -
+#pragma mark Initialisation and teardown
+
+/**
+ * Initialise the SPMySQLConnection object, setting up class defaults.
+ *
+ * Typically initialisation would be followed by setting the connection details
+ * and then calling -connect.
+ */
+- (id)init
+{
+ if ((self = [super init])) {
+ mySQLConnection = NULL;
+ state = SPMySQLDisconnected;
+ userTriggeredDisconnect = NO;
+ isReconnecting = NO;
+ mysqlConnectionThreadId = 0;
+ initialConnectTime = 0;
+
+ port = 3306;
+
+ // Default to socket connections if no other details have been provided
+ useSocket = YES;
+
+ // Start with no proxy
+ proxy = nil;
+ proxyStateChangeNotificationsIgnored = NO;
+
+ // Start with no selected database
+ database = nil;
+
+ // Set a timeout of 30 seconds, with keepalive on and acting every sixty seconds
+ timeout = 30;
+ useKeepAlive = YES;
+ keepAliveInterval = 60;
+ keepAlivePingFailures = 0;
+ lastKeepAliveTime = 0;
+ keepAlivePingThread = NULL;
+ keepAlivePingThreadActive = NO;
+ keepAliveLastPingSuccess = NO;
+ keepAliveLastPingBlocked = NO;
+
+ // Set up default encoding variables
+ encoding = [[NSString alloc] initWithString:@"utf8"];
+ stringEncoding = NSUTF8StringEncoding;
+ encodingUsesLatin1Transport = NO;
+ previousEncoding = nil;
+ previousEncodingUsesLatin1Transport = NO;
+
+ // Initialise default delegate settings
+ delegateSupportsWillQueryString = NO;
+ delegateSupportsConnectionLost = NO;
+ delegateQueryLogging = YES;
+
+ // Delegate disconnection decisions
+ reconnectionRetryAttempts = 0;
+ lastDelegateDecisionForLostConnection = SPMySQLConnectionLostDisconnect;
+ delegateDecisionLock = [[NSLock alloc] init];
+
+ // Set up the connection lock
+ connectionLock = [[NSConditionLock alloc] initWithCondition:SPMySQLConnectionIdle];
+ [connectionLock setName:@"SPMySQLConnection query lock"];
+
+ // Ensure the server detail records are initialised
+ serverVersionString = nil;
+
+ // Start with a blank error state
+ queryErrorID = 0;
+ queryErrorMessage = nil;
+
+ // Start with empty cancellation details
+ lastQueryWasCancelled = NO;
+ lastQueryWasCancelledUsingReconnect = NO;
+
+ // Empty or reset the timing variables
+ lastConnectionUsedTime = 0;
+ lastQueryExecutionTime = 0;
+
+ // Default to editable query size of 1MB
+ maxQuerySize = 1048576;
+ maxQuerySizeIsEditable = YES;
+ maxQuerySizeEditabilityChecked = NO;
+ queryActionShouldRestoreMaxQuerySize = NSNotFound;
+
+ // Default to allowing queries to be automatically retried if the connection drops
+ // while running them
+ retryQueriesOnConnectionFailure = YES;
+
+ // Start the ping keepalive timer
+ keepAliveTimer = [[SPMySQLKeepAliveTimer alloc] initWithInterval:10 target:self selector:@selector(_keepAlive)];
+ }
+
+ return self;
+}
+
+/**
+ * Object deallocation.
+ */
+- (void) dealloc
+{
+ userTriggeredDisconnect = YES;
+
+ // Unset the delegate
+ [self setDelegate:nil];
+
+ // Clear the keepalive timer
+ [keepAliveTimer invalidate];
+ [keepAliveTimer release];
+
+ // Disconnect if appropriate (which should also disconnect any proxy)
+ [self disconnect];
+
+ // Clean up the connection proxy, if any
+ if (proxy) {
+ [proxy setConnectionStateChangeSelector:NULL delegate:nil];
+ [proxy release];
+ }
+
+ // Ensure the query lock is unlocked, thereafter setting to nil in case of pending calls
+ if ([connectionLock condition] != SPMySQLConnectionIdle) {
+ [self _unlockConnection];
+ }
+ [connectionLock release], connectionLock = nil;
+
+ [encoding release];
+ if (previousEncoding) [previousEncoding release], previousEncoding = nil;
+
+ if (database) [database release], database = nil;
+ if (serverVersionString) [serverVersionString release], serverVersionString = nil;
+ if (queryErrorMessage) [queryErrorMessage release], queryErrorMessage = nil;
+ [delegateDecisionLock release];
+
+ [NSObject cancelPreviousPerformRequestsWithTarget:self];
+
+ [super dealloc];
+}
+
+#pragma mark -
+#pragma mark Connection and disconnection
+
+/**
+ * Trigger a connection to the specified host, if any, using any connection details
+ * that have been set.
+ * Returns whether the connection was successful.
+ */
+- (BOOL)connect
+{
+
+ // If a connection is already active in some form, throw an exception
+ if (state != SPMySQLDisconnected) {
+ [NSException raise:NSInternalInconsistencyException format:@"Attempted to connect a connection that is not disconnected."];
+ return NO;
+ }
+ state = SPMySQLConnecting;
+
+ // Lock the connection for safety
+ [self _lockConnection];
+
+ // Attempt the connection
+ mySQLConnection = [self _makeRawMySQLConnectionWithEncoding:encoding isMasterConnection:YES];
+
+ // If the connection failed, reset state and return
+ if (!mySQLConnection) {
+ [self _unlockConnection];
+ state = SPMySQLDisconnected;
+ return NO;
+ }
+
+ // Successfully connected - record connected state and reset tracking variables
+ state = SPMySQLConnected;
+ userTriggeredDisconnect = NO;
+ reconnectionRetryAttempts = 0;
+ initialConnectTime = mach_absolute_time();
+ mysqlConnectionThreadId = mySQLConnection->thread_id;
+ lastConnectionUsedTime = 0;
+
+ // Update SSL state
+ connectedWithSSL = NO;
+ if (useSSL) connectedWithSSL = (mysql_get_ssl_cipher(mySQLConnection))?YES:NO;
+ if (useSSL && !connectedWithSSL) {
+ if ([delegate respondsToSelector:@selector(connectionFellBackToNonSSL:)]) {
+ [delegate connectionFellBackToNonSSL:self];
+ }
+ }
+
+ // Reset keepalive variables
+ lastKeepAliveTime = 0;
+ keepAlivePingFailures = 0;
+
+ // Clear the connection error record
+ [self _updateLastErrorID:NSNotFound];
+ [self _updateLastErrorMessage:nil];
+
+ // Unlock the connection
+ [self _unlockConnection];
+
+ // Update connection variables to be in sync with the server state. As this performs
+ // a query, ensure the connection is still up afterwards (!)
+ [self _updateConnectionVariables];
+ if (state != SPMySQLConnected) return NO;
+
+ // Update the maximum query size
+ [self _updateMaxQuerySize];
+
+ return YES;
+}
+
+/**
+ * Reconnect to the currently "active" - but possibly disconnected - connection, using the
+ * stored details.
+ * Error checks extensively - if this method fails, it will ask how to proceed and loop depending
+ * on the status, not returning control until either a connection has been established or
+ * the connection and document have been closed.
+ * Runs its own autorelease pool as sometimes called in a thread following proxy changes
+ * (where the return code doesn't matter).
+ */
+- (BOOL)reconnect
+{
+ if (userTriggeredDisconnect) return NO;
+
+ NSAutoreleasePool *reconnectionPool = [[NSAutoreleasePool alloc] init];
+
+ // Check whether a reconnection attempt is already being made - if so, wait
+ // and return the status of that reconnection attempt. This improves threaded
+ // use of the connection by preventing reconnect races.
+ if (isReconnecting) {
+
+ // Loop in a panel runloop mode until the reconnection has processed; if an iteration
+ // takes less than the requested 0.1s, sleep instead.
+ while (isReconnecting) {
+ uint64_t loopIterationStart_t = mach_absolute_time();
+
+ [[NSRunLoop currentRunLoop] runMode:NSModalPanelRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
+ if (_elapsedSecondsSinceAbsoluteTime(loopIterationStart_t) < 0.1) {
+ usleep(100000 - (useconds_t)(1000000 * _elapsedSecondsSinceAbsoluteTime(loopIterationStart_t)));
+ }
+ }
+
+ [reconnectionPool drain];
+ return (state == SPMySQLConnected);
+ }
+
+ isReconnecting = YES;
+
+ // Store certain details about the connection, so that if the reconnection is successful
+ // they can be restored. This has to be treated separately from _restoreConnectionDetails
+ // as a full connection reinitialises certain values from the server.
+ NSString *preReconnectEncoding = [NSString stringWithString:encoding];
+ BOOL preReconnectEncodingUsesLatin1 = encodingUsesLatin1Transport;
+ NSString *preReconnectDatabase = nil;
+ if (database) preReconnectDatabase = [NSString stringWithString:database];
+
+ // If there is a connection proxy, temporarily disassociate the state change action
+ if (proxy) proxyStateChangeNotificationsIgnored = YES;
+
+ // Close the connection if it's active
+ [self disconnect];
+
+ // Lock the connection while waiting for network and proxy
+ [self _lockConnection];
+
+ // If no network is present, wait for a short time for one to become available
+ [self _waitForNetworkConnectionWithTimeout:10];
+
+ // If there is a proxy, attempt to reconnect it in blocking fashion
+ if (proxy) {
+ uint64_t loopIterationStart_t, proxyWaitStart_t;
+
+ // If the proxy is not yet idle after requesting a disconnect, wait for a short time
+ // to allow it to disconnect.
+ if ([proxy state] != SPMySQLProxyIdle) {
+
+ proxyWaitStart_t = mach_absolute_time();
+ while ([proxy state] != SPMySQLProxyIdle) {
+ loopIterationStart_t = mach_absolute_time();
+
+ // If the connection timeout has passed, break out of the loop
+ if (_elapsedSecondsSinceAbsoluteTime(proxyWaitStart_t) > timeout) break;
+
+ // Allow events to process for 0.25s, sleeping to completion on early return
+ [[NSRunLoop currentRunLoop] runMode:NSModalPanelRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.25]];
+ if (_elapsedSecondsSinceAbsoluteTime(loopIterationStart_t) < 0.25) {
+ usleep(250000 - (useconds_t)(1000000 * _elapsedSecondsSinceAbsoluteTime(loopIterationStart_t)));
+ }
+ }
+ }
+
+ // Request that the proxy re-establishes its connection
+ [proxy connect];
+
+ // Wait while the proxy connects
+ proxyWaitStart_t = mach_absolute_time();
+ while (1) {
+ loopIterationStart_t = mach_absolute_time();
+
+ // If the proxy has connected, record the new local port and break out of the loop
+ if ([proxy state] == SPMySQLProxyConnected) {
+ port = [proxy localPort];
+ break;
+ }
+
+ // If the proxy connection attempt time has exceeded the timeout, break of of the loop.
+ if (_elapsedSecondsSinceAbsoluteTime(proxyWaitStart_t) > (timeout + 1)) {
+ [proxy disconnect];
+ break;
+ }
+
+ // Process events for a short time, allowing dialogs to be shown but waiting for
+ // the proxy. Capture how long this interface action took, standardising the
+ // overall time.
+ [[NSRunLoop mainRunLoop] runMode:NSModalPanelRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.25]];
+ if (_elapsedSecondsSinceAbsoluteTime(loopIterationStart_t) < 0.25) {
+ usleep((useconds_t)(250000 - (1000000 * _elapsedSecondsSinceAbsoluteTime(loopIterationStart_t))));
+ }
+
+ // Extend the connection timeout by any interface time
+ if ([proxy state] == SPMySQLProxyWaitingForAuth) {
+ proxyWaitStart_t += mach_absolute_time() - loopIterationStart_t;
+ }
+ }
+
+ // Having in theory performed the proxy connect, update state
+ previousProxyState = [proxy state];
+ proxyStateChangeNotificationsIgnored = NO;
+ }
+
+ // Unlock the connection
+ [self _unlockConnection];
+
+ // If not using a proxy, or if the proxy successfully connected, trigger a connection
+ if (!proxy || [proxy state] == SPMySQLProxyConnected) {
+ [self connect];
+ }
+
+ // If the connection failed, retry the reconnection or cancel as appropriate.
+ if (state != SPMySQLConnected) {
+
+ // Default to attempting another reconnect
+ SPMySQLConnectionLostDecision connectionLostDecision = SPMySQLConnectionLostReconnect;
+
+ // If the delegate supports the decision process, ask it how to proceed
+ if (delegateSupportsConnectionLost) {
+ connectionLostDecision = [self _delegateDecisionForLostConnection];
+
+ // Otherwise default to reconnect, but only a set number of times to prevent a runaway loop
+ } else {
+ if (reconnectionRetryAttempts < 5) {
+ connectionLostDecision = SPMySQLConnectionLostReconnect;
+ } else {
+ connectionLostDecision = SPMySQLConnectionLostDisconnect;
+ }
+ reconnectionRetryAttempts++;
+ }
+
+ switch (connectionLostDecision) {
+ case SPMySQLConnectionLostDisconnect:
+ [self _updateLastErrorMessage:NSLocalizedString(@"User triggered disconnection", @"User triggered disconnection")];
+ userTriggeredDisconnect = YES;
+ isReconnecting = NO;
+ [reconnectionPool release];
+ return NO;
+ default:
+ isReconnecting = NO;
+ [reconnectionPool release];
+ return [self reconnect];
+ }
+ }
+
+ // If the connection was successfully established, restore the connection
+ // state if appropriate.
+ if (preReconnectDatabase) {
+ [self selectDatabase:preReconnectDatabase];
+ }
+ [self setEncoding:preReconnectEncoding];
+ [self setEncodingUsesLatin1Transport:preReconnectEncodingUsesLatin1];
+
+ isReconnecting = NO;
+ [reconnectionPool release];
+ return YES;
+}
+
+/**
+ * Trigger a disconnection if the connection is currently active.
+ */
+- (void)disconnect
+{
+
+ // Only continue if a connection is active
+ if (state != SPMySQLConnected && state != SPMySQLConnecting) return;
+ state = SPMySQLDisconnecting;
+
+ // If a query is active, cancel it
+ [self cancelCurrentQuery];
+
+ // Allow any pings or cancelled queries to complete, inside a time limit of ten seconds
+ uint64_t disconnectStartTime_t = mach_absolute_time();
+ do {
+ usleep(100000);
+ if (_elapsedSecondsSinceAbsoluteTime(disconnectStartTime_t) > 10) break;
+ } while (![self _tryLockConnection]);
+ [self _unlockConnection];
+ if (keepAlivePingThread != NULL) pthread_cancel(keepAlivePingThread), keepAlivePingThread = NULL;
+
+ // Close the underlying MySQL connection if it still appears to be active, and not reading
+ // or writing. While this may result in a leak of the MySQL object, it prevents crashes
+ // due to attempts to close a blocked/stuck connection.
+ if (!mySQLConnection->net.reading_or_writing && mySQLConnection->net.vio && mySQLConnection->net.buff) {
+ mysql_close(mySQLConnection);
+ }
+ mySQLConnection = NULL;
+
+ // If using a connection proxy, disconnect that too
+ if (proxy) {
+ [proxy performSelectorOnMainThread:@selector(disconnect) withObject:nil waitUntilDone:YES];
+ }
+
+ // Clear host-specific information
+ if (serverVersionString) [serverVersionString release], serverVersionString = nil;
+ if (database) [database release], database = nil;
+
+ state = SPMySQLDisconnected;
+}
+
+#pragma mark -
+#pragma mark Connection state
+
+/**
+ * Retrieve whether the connection instance is connected to the remote host.
+ * Returns NO if the connection is still in process, YES if a disconnection is
+ * being actively performed.
+ */
+- (BOOL)isConnected
+{
+ return (state == SPMySQLConnected || state == SPMySQLDisconnecting);
+}
+
+/**
+ * Returns YES if the MCPConnection is connected to a server via SSL, NO otherwise.
+ */
+- (BOOL)isConnectedViaSSL
+{
+ if (![self isConnected]) return NO;
+ return connectedWithSSL;
+}
+
+/**
+ * Checks whether the connection to the server is still active. This verifies
+ * the connection using a ping, and if the connection is found to be down attempts
+ * to quickly restore it, including the previous state.
+ */
+- (BOOL)checkConnection
+{
+
+ // If the connection is not seen as active, don't proceed
+ if (state != SPMySQLConnected) return NO;
+
+ // Similarly, if the connection is currently locked, that indicates it's in use. This
+ // could be because queries are actively being run, or that a ping is running.
+ if ([connectionLock condition] == SPMySQLConnectionBusy) {
+
+ // If a ping thread is not active queries are being performed - return success.
+ if (!keepAlivePingThreadActive) return YES;
+
+ // If a ping thread is active, wait for it to complete before checking the connection
+ while (keepAlivePingThreadActive) {
+ usleep(10000);
+ }
+ }
+
+ // Confirm whether the connection is still responding by using a ping
+ BOOL connectionVerified = [self _pingConnectionUsingLoopDelay:400];
+
+ // If the connection didn't respond, trigger a reconnect. This will automatically
+ // attempt to reconnect once, and if that fails will ask the user how to proceed - whether
+ // to keep reconnecting, or whether to disconnect.
+ if (!connectionVerified) {
+ connectionVerified = [self reconnect];
+ }
+
+ return connectionVerified;
+}
+
+/**
+ * Retrieve the time elapsed since the connection was established, in seconds.
+ * This time is retrieved in a monotonically increasing fashion and is high
+ * precision; it is used internally for query timing, and is reset on reconnections.
+ * If no connection is currently active, returns -1.
+ */
+- (double)timeConnected
+{
+ if (initialConnectTime == 0) return -1;
+
+ return _elapsedSecondsSinceAbsoluteTime(initialConnectTime);
+}
+
+/**
+ * Returns YES if the user chose to disconnect at the last "connection failure"
+ * prompt, NO otherwise. This can be used to alter behaviour in response to state
+ * changes.
+ */
+- (BOOL)userTriggeredDisconnect
+{
+ return userTriggeredDisconnect;
+}
+
+#pragma mark -
+#pragma mark General connection utilities
+
++ (NSString *)findSocketPath
+{
+ NSFileManager *fileManager = [NSFileManager defaultManager];
+
+ NSArray *possibleSocketLocations = [NSArray arrayWithObjects:
+ @"/tmp/mysql.sock", // Default
+ @"/Applications/MAMP/tmp/mysql/mysql.sock", // MAMP default location
+ @"/Applications/xampp/xamppfiles/var/mysql/mysql.sock", // XAMPP default location
+ @"/var/mysql/mysql.sock", // Mac OS X Server default
+ @"/opt/local/var/run/mysqld/mysqld.sock", // Darwinports MySQL
+ @"/opt/local/var/run/mysql4/mysqld.sock", // Darwinports MySQL 4
+ @"/opt/local/var/run/mysql5/mysqld.sock", // Darwinports MySQL 5
+ @"/usr/local/zend/mysql/tmp/mysql.sock", // Zend Server CE (see Issue #1251)
+ @"/var/run/mysqld/mysqld.sock", // As used on Debian/Gentoo
+ @"/var/tmp/mysql.sock", // As used on FreeBSD
+ @"/var/lib/mysql/mysql.sock", // As used by Fedora
+ @"/opt/local/lib/mysql/mysql.sock", // Alternate fedora
+ nil];
+
+ for (NSUInteger i = 0; i < [possibleSocketLocations count]; i++) {
+ if ([fileManager fileExistsAtPath:[possibleSocketLocations objectAtIndex:i]])
+ return [possibleSocketLocations objectAtIndex:i];
+ }
+
+ return nil;
+}
+
+@end
+
+#pragma mark -
+#pragma mark Private API
+
+@implementation SPMySQLConnection (PrivateAPI)
+
+/**
+ * Make a connection using the class connection settings, returning a MySQL
+ * connection object on success.
+ */
+- (MYSQL *)_makeRawMySQLConnectionWithEncoding:(NSString *)encodingName isMasterConnection:(BOOL)isMaster
+{
+
+ // Set up the MySQL connection object
+ MYSQL *theConnection = mysql_init(NULL);
+ if (!theConnection) return NULL;
+
+ // Disable automatic reconnection, as it's handled in-framework to preserve
+ // options, encodings and connection state.
+ my_bool falseMyBool = FALSE;
+ mysql_options(theConnection, MYSQL_OPT_RECONNECT, &falseMyBool);
+
+ // Set the connection timeout
+ mysql_options(theConnection, MYSQL_OPT_CONNECT_TIMEOUT, (const void *)&timeout);
+
+ // Set the connection encoding
+ mysql_options(theConnection, MYSQL_SET_CHARSET_NAME, [encodingName UTF8String]);
+
+ // Set up the connection variables in the format MySQL needs, from the class-wide variables
+ const char *theHost = NULL;
+ const char *theUsername = "";
+ const char *thePassword = NULL;
+ const char *theSocket = NULL;
+
+ if (host) theHost = [self _cStringForString:host];
+ if (username) theUsername = [self _cStringForString:username];
+
+ // If a password was supplied, use it; otherwise ask the delegate if appropriate
+ if (password) {
+ thePassword = [self _cStringForString:password];
+ } else if ([delegate respondsToSelector:@selector(keychainPasswordForConnection:)]) {
+ thePassword = [self _cStringForString:[delegate keychainPasswordForConnection:self]];
+ }
+
+ // If set to use a socket and a socket was supplied, use it; otherwise, search for a socket to use
+ if (useSocket) {
+ if ([socketPath length]) {
+ theSocket = [self _cStringForString:socketPath];
+ } else {
+ theSocket = [self _cStringForString:[SPMySQLConnection findSocketPath]];
+ }
+ }
+
+ // Apply SSL if appropriate
+ if (useSSL) {
+ const char *theSSLKeyFilePath = NULL;
+ const char *theSSLCertificatePath = NULL;
+ const char *theCACertificatePath = NULL;
+
+ if (sslKeyFilePath) {
+ theSSLKeyFilePath = [[sslKeyFilePath stringByExpandingTildeInPath] UTF8String];
+ }
+ if (sslCertificatePath) {
+ theSSLCertificatePath = [[sslCertificatePath stringByExpandingTildeInPath] UTF8String];
+ }
+ if (sslCACertificatePath) {
+ theCACertificatePath = [[sslCACertificatePath stringByExpandingTildeInPath] UTF8String];
+ }
+
+ mysql_ssl_set(theConnection, theSSLKeyFilePath, theSSLCertificatePath, theCACertificatePath, NULL, SPMySQLSSLPermissibleCiphers);
+ }
+
+ MYSQL *connectionStatus = mysql_real_connect(theConnection, theHost, theUsername, thePassword, NULL, (unsigned int)port, theSocket, SPMySQLConnectionOptions);
+
+ // If the connection failed, return NULL
+ if (theConnection != connectionStatus) {
+
+ // If the connection is the master connection, record the error state
+ if (isMaster) {
+ [self _updateLastErrorMessage:[self _stringForCString:mysql_error(theConnection)]];
+ [self _updateLastErrorID:mysql_errno(theConnection)];
+ }
+
+ return NULL;
+ }
+
+ // Ensure automatic reconnection is disabled for older versions
+ theConnection->reconnect = 0;
+
+ // Successful connection - return the handle
+ return theConnection;
+}
+
+/**
+ * Loop while a connection isn't available; allows blocking while the network is disconnected
+ * or still connecting (eg Airport still coming up after sleep).
+ */
+- (BOOL)_waitForNetworkConnectionWithTimeout:(double)timeoutSeconds
+{
+ BOOL hostReachable;
+ Boolean flagsValid;
+ SCNetworkReachabilityRef reachabilityTarget;
+ SCNetworkConnectionFlags reachabilityStatus;
+
+ // Set up the reachability target - the host is not important, and is not connected to.
+ reachabilityTarget = SCNetworkReachabilityCreateWithName(NULL, "dev.mysql.com");
+
+ // In a loop until success or the timeout, test reachability
+ uint64_t loopStart_t = mach_absolute_time();
+ while (1) {
+
+ // Check reachability
+ flagsValid = SCNetworkReachabilityGetFlags(reachabilityTarget, &reachabilityStatus);
+
+ hostReachable = flagsValid ? YES : NO;
+
+ // Ensure that the network is reachable
+ if (hostReachable && !(reachabilityStatus & kSCNetworkFlagsReachable)) hostReachable = NO;
+
+ // Ensure that Airport is up/connected if present
+ if (hostReachable && (reachabilityStatus & kSCNetworkFlagsConnectionRequired)) hostReachable = NO;
+
+ // If the host *is* reachable, return success
+ if (hostReachable) return YES;
+
+ // If the timeout has been exceeded, break out of the loop
+ if (_elapsedSecondsSinceAbsoluteTime(loopStart_t) >= timeoutSeconds) break;
+
+ // Sleep before the next loop iteration
+ usleep(250000);
+ }
+
+ // All checks failed - return failure
+ return NO;
+}
+
+/**
+ * Update connection variables from the server, collecting state and ensuring
+ * settings like encoding are in sync.
+ */
+- (void)_updateConnectionVariables
+{
+ if (state != SPMySQLConnected && state != SPMySQLConnecting) return;
+
+ // Retrieve all variables from the server in a single query
+ SPMySQLResult *theResult = [self queryString:@"SHOW VARIABLES"];
+ if (![theResult numberOfRows]) return;
+
+ // SHOW VARIABLES can return binary results on certain MySQL 4 versions; ensure string output
+ [theResult setReturnDataAsStrings:YES];
+
+ // Convert the result set into a variables dictionary
+ [theResult setDefaultRowReturnType:SPMySQLResultRowAsArray];
+ NSMutableDictionary *variables = [NSMutableDictionary new];
+ for (NSArray *variableRow in theResult) {
+ [variables setObject:[variableRow objectAtIndex:1] forKey:[variableRow objectAtIndex:0]];
+ }
+
+ // Copy the server version string to the instance variable
+ if (serverVersionString) [serverVersionString release], serverVersionString = nil;
+ serverVersionString = [[variables objectForKey:@"version"] retain];
+
+ // Get the connection encoding. Although a specific encoding may have been requested on
+ // connection, it may be overridden by init_connect commands or connection state changes.
+ // Default to latin1 for older server versions.
+ NSString *retrievedEncoding = @"latin1";
+ if ([variables objectForKey:@"character_set_results"]) {
+ retrievedEncoding = [variables objectForKey:@"character_set_results"];
+ } else if ([variables objectForKey:@"character_set"]) {
+ retrievedEncoding = [variables objectForKey:@"character_set"];
+ }
+
+ // Update instance variables
+ if (encoding) [encoding release];
+ encoding = [[NSString alloc] initWithString:retrievedEncoding];
+ stringEncoding = [SPMySQLConnection stringEncodingForMySQLCharset:[self _cStringForString:encoding]];
+ encodingUsesLatin1Transport = NO;
+
+ // Check the interactive timeout - if it's below five minutes, increase it to ten
+ // to imprive timeout/keepalive behaviour
+ if ([variables objectForKey:@"interactive_timeout"]) {
+ if ([[variables objectForKey:@"interactive_timeout"] integerValue] < 300) {
+ [self queryString:@"SET interactive_timeout=600"];
+ }
+ }
+
+ [variables release];
+}
+
+/**
+ * Restore the connection encoding details as necessary based on previously set
+ * details.
+ */
+- (void)_restoreConnectionVariables
+{
+ mysqlConnectionThreadId = mySQLConnection->thread_id;
+ initialConnectTime = mach_absolute_time();
+
+ [self selectDatabase:database];
+
+ [self setEncoding:encoding];
+ [self setEncodingUsesLatin1Transport:encodingUsesLatin1Transport];
+}
+
+/**
+ * If thirty seconds have passed since the last time the connection was
+ * used, check the connection.
+ * This minimises the impact of continuous additional connection checks -
+ * each of which requires a round trip to the server - but handles most
+ * network issues.
+ * Returns whether the connection is considered still valid.
+ */
+- (BOOL)_checkConnectionIfNecessary
+{
+
+ // If the connection was recently used, return success
+ if (_elapsedSecondsSinceAbsoluteTime(lastConnectionUsedTime) < 30) return YES;
+
+ // Otherwise check the connection
+ return [self checkConnection];
+}
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnectionDelegate.h b/Frameworks/SPMySQLFramework/Source/SPMySQLConnectionDelegate.h
new file mode 100644
index 00000000..97e94170
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnectionDelegate.h
@@ -0,0 +1,108 @@
+//
+// $Id$
+//
+// SPMySQLConnectionDelegate.h
+// SPMySQLFramework
+//
+// Created by Stuart Connolly (stuconnolly.com) on October 20, 2010.
+// Copyright (c) 2010 Stuart Connolly. 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 "SPMySQLConstants.h"
+
+@protocol SPMySQLConnectionDelegate <NSObject>
+@optional
+
+/**
+ * Notifies the delegate that a query will be performed.
+ *
+ * @param query The query string that will be sent to the MySQL server
+ * @param connection The connection instance performing the query
+ */
+- (void)willQueryString:(NSString *)query connection:(id)connection;
+
+/**
+ * Notifies the delegate that a query that was just performed gave
+ * an error.
+ *
+ * @param error The query error, as a string
+ * @param connection The connection instance which received the error
+ */
+- (void)queryGaveError:(NSString *)error connection:(id)connection;
+
+/**
+ * Notifies the delegate that it should display the supplied error.
+ * The connection may sometimes want to notify the user directly
+ * about certain issues, and will use this method to allow the
+ * delegate to do so.
+ *
+ * @param title The title of the message to display to the user
+ * @param message The main text of the message to display to the user
+ */
+- (void)showErrorWithTitle:(NSString *)title message:(NSString *)message;
+
+/**
+ * Requests the keychain password for the connection.
+ * When a connection is being made to a server, it is best not to
+ * set the password on the class; instead, it should be kept within
+ * the secure store, and the other connection details (user, host)
+ * can be used to look it up and supplied on demand.
+ *
+ * @param connection The connection instance to supply the password for
+ */
+- (NSString *)keychainPasswordForConnection:(id)connection;
+
+/**
+ * Notifies the delegate that no underlying connection is available,
+ * typically when the connection has been asked to perform a query
+ * or some other action for which a connection must be present.
+ * Those actions will still return false or error states as appropriate,
+ * but the delegate may wish to perform actions as a result of a total
+ * loss of connection.
+ *
+ * @param connection The connection instance which has lost the connection to the host
+ */
+- (void)noConnectionAvailable:(id)connection;
+
+/**
+ * Notifies the delegate that although a SSL connection was requested,
+ * MySQL made the connection without using SSL. This can happen because
+ * the server connected to doesn't support SSL or had it disabled, or
+ * that insufficient details were provided to make the connection over
+ * SSL.
+ */
+- (void)connectionFellBackToNonSSL:(id)connection;
+
+/**
+ * Notifies the delegate that the connection has been temporarily lost,
+ * and asks the delegate for guidance on how to proceed. If the delegate
+ * does not implement this method, reconnections will automatically be
+ * attempted - up to a small limit of attempts.
+ *
+ * @param connection The connection instance that requires a decision on how to proceed
+ */
+- (SPMySQLConnectionLostDecision)connectionLost:(id)connection;
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnectionProxy.h b/Frameworks/SPMySQLFramework/Source/SPMySQLConnectionProxy.h
new file mode 100644
index 00000000..2da553c6
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnectionProxy.h
@@ -0,0 +1,77 @@
+//
+// $Id$
+//
+// SPMySQLConnectionProxy.h
+// SPMySQLFramework
+//
+// Created by Stuart Connolly (stuconnolly.com) on July 2, 2009.
+// Copyright (c) 2009 Stuart Connolly. 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/>
+
+
+/**
+ * Connection proxy state constants.
+ */
+typedef enum {
+ SPMySQLProxyIdle = 0,
+ SPMySQLProxyConnecting = 1,
+ SPMySQLProxyWaitingForAuth = 2,
+ SPMySQLProxyConnected = 3,
+ SPMySQLProxyForwardingFailed = 4
+} SPMySQLConnectionProxyState;
+
+
+@protocol SPMySQLConnectionProxy <NSObject>
+
+/**
+ * All the methods for this protocol are required.
+ */
+
+/**
+ * Connect the proxy.
+ */
+- (void)connect;
+
+/**
+ * Disconnect the proxy.
+ */
+- (void)disconnect;
+
+/**
+ * Get the current state of the proxy.
+ */
+- (SPMySQLConnectionProxyState)state;
+
+/**
+ * Get the local port being provided by the proxy.
+ */
+- (NSUInteger)localPort;
+
+/**
+ * Sets the method the proxy should call whenever the state of the connection changes.
+ */
+- (BOOL)setConnectionStateChangeSelector:(SEL)theStateChangeSelector delegate:(id)theDelegate;
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConstants.h b/Frameworks/SPMySQLFramework/Source/SPMySQLConstants.h
new file mode 100644
index 00000000..b1689569
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConstants.h
@@ -0,0 +1,76 @@
+//
+// $Id$
+//
+// SPMySQLConstants.h
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on January 14, 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/>
+
+
+// Connection state
+typedef enum {
+ SPMySQLDisconnected = 0,
+ SPMySQLConnecting = 1,
+ SPMySQLConnected = 2,
+ SPMySQLDisconnecting = 3
+} SPMySQLConnectionState;
+
+// Connection lock state
+typedef enum {
+ SPMySQLConnectionIdle = 0,
+ SPMySQLConnectionBusy = 1
+} SPMySQLConnectionLockState;
+
+// Decision on how to handle lost connections
+// Connection check constants
+typedef enum {
+ SPMySQLConnectionLostDisconnect = 0,
+ SPMySQLConnectionLostReconnect = 1
+} SPMySQLConnectionLostDecision;
+
+// Result set row types
+typedef enum {
+ SPMySQLResultRowAsDefault = 0,
+ SPMySQLResultRowAsArray = 1,
+ SPMySQLResultRowAsDictionary = 2
+} SPMySQLResultRowType;
+
+// Result charset list
+typedef struct {
+ NSUInteger nr;
+ const char *name;
+ const char *collation;
+ NSUInteger char_minlen;
+ NSUInteger char_maxlen;
+} SPMySQLResultCharset;
+
+// Query result types
+typedef enum {
+ SPMySQLResultAsResult = 0,
+ SPMySQLResultAsFastStreamingResult = 1,
+ SPMySQLResultAsLowMemStreamingResult = 2
+} SPMySQLResultType;
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLFastStreamingResult.h b/Frameworks/SPMySQLFramework/Source/SPMySQLFastStreamingResult.h
new file mode 100644
index 00000000..a4f07cdd
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLFastStreamingResult.h
@@ -0,0 +1,47 @@
+//
+// $Id$
+//
+// SPMySQLFastStreamingResult.h
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on February 2, 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/>
+
+
+@interface SPMySQLFastStreamingResult : SPMySQLStreamingResult {
+
+ // Linked list setup
+ struct st_spmysqlstreamingrowdata *currentDataStoreEntry;
+ struct st_spmysqlstreamingrowdata *lastDataStoreEntry;
+
+ // Additional counts and memory length tracking
+ NSUInteger processedRowCount;
+
+ // Thread safety
+ pthread_mutex_t dataLock;
+}
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLFastStreamingResult.m b/Frameworks/SPMySQLFramework/Source/SPMySQLFastStreamingResult.m
new file mode 100644
index 00000000..8ba55134
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLFastStreamingResult.m
@@ -0,0 +1,405 @@
+//
+// $Id$
+//
+// SPMySQLFastStreamingResult.m
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on February 2, 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 "SPMySQLFastStreamingResult.h"
+#import "SPMySQL Private APIs.h"
+#import "SPMySQLArrayAdditions.h"
+#include <pthread.h>
+
+static id NSNullPointer;
+
+/**
+ * This type of streaming result operates in a multithreaded fashion - a worker
+ * thread is set up to download the results as fast as possible in the background,
+ * while the results are made available via blocking (and so single-thread-compatible)
+ * calls. This provides the benefit of allowing a progress bar to be shown during
+ * downloads, and threaded processing, but still has reasonable memory usage for the
+ * downloaded result - and won't block the server.
+ */
+
+typedef struct st_spmysqlstreamingrowdata {
+ char *data;
+ unsigned long *dataLengths;
+ struct st_spmysqlstreamingrowdata *nextRow;
+} SPMySQLStreamingRowData;
+
+@interface SPMySQLFastStreamingResult (Private_API)
+
+- (void) _downloadAllData;
+
+@end
+
+#pragma mark -
+
+@implementation SPMySQLFastStreamingResult
+
+#pragma mark -
+
+/**
+ * In the one-off class initialisation, cache static variables
+ */
++ (void)initialize
+{
+
+ // Cached NSNull singleton reference
+ if (!NSNullPointer) NSNullPointer = [NSNull null];
+}
+
+/**
+ * Standard init method, constructing the SPMySQLStreamingResult around a MySQL
+ * result pointer and the encoding to use when working with the data.
+ * As opposed to SPMySQLResult, defaults to returning rows as arrays, as the result
+ * sets are likely to be larger and processed in loops.
+ */
+- (id)initWithMySQLResult:(void *)theResult stringEncoding:(NSStringEncoding)theStringEncoding connection:(SPMySQLConnection *)theConnection
+{
+
+ // If no result set was passed in, return nil.
+ if (!theResult) return nil;
+
+ if ((self = [super initWithMySQLResult:theResult stringEncoding:theStringEncoding connection:theConnection])) {
+
+ // Initialise the extra streaming result counts and tracking
+ processedRowCount = 0;
+
+ // Initialise the linked list pointers
+ currentDataStoreEntry = NULL;
+ lastDataStoreEntry = NULL;
+
+ // Set up the linked list lock
+ pthread_mutex_init(&dataLock, NULL);
+
+ // Start the data download thread
+ [NSThread detachNewThreadSelector:@selector(_downloadAllData) toTarget:self withObject:nil];
+ }
+
+ return self;
+}
+
+/**
+ * Deallocate the result and ensure the parent connection is unlocked for further use.
+ */
+- (void)dealloc
+{
+
+ // Ensure all data is processed and the parent connection is unlocked
+ [self cancelResultLoad];
+
+ // Destroy the linked list lock
+ pthread_mutex_destroy(&dataLock);
+
+ // Call dealloc on super to clean up everything else, and to throw an exception if
+ // the parent connection hasn't been cleaned up correctly.
+ [super dealloc];
+}
+
+#pragma mark -
+#pragma mark Data retrieval
+
+/**
+ * Override the convenience selectors so that forwarding works correctly.
+ */
+- (id)getRow
+{
+ return SPMySQLResultGetRow(self, SPMySQLResultRowAsDefault);
+}
+- (NSArray *)getRowAsArray
+{
+ return SPMySQLResultGetRow(self, SPMySQLResultRowAsArray);
+}
+- (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
+{
+ NSUInteger copiedDataLength = 0;
+ char *theRowData;
+ unsigned long *fieldLengths;
+ id theReturnData;
+
+ // 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];
+ }
+
+ // Lock the data mutex for safe access of variables and counters
+ pthread_mutex_lock(&dataLock);
+
+ // Determine whether any data is available; if not, wait 1ms before trying again
+ while (!dataDownloaded && processedRowCount == downloadedRowCount) {
+ pthread_mutex_unlock(&dataLock);
+ usleep(1000);
+ pthread_mutex_lock(&dataLock);
+ }
+
+ // If all rows have been processed, the end of the result set has been reached; return nil.
+ if (processedRowCount == downloadedRowCount) {
+ pthread_mutex_unlock(&dataLock);
+ return nil;
+ }
+
+ // Unlock the data mutex now checks are complete
+ pthread_mutex_unlock(&dataLock);
+
+ // Get a reference to the data for the current row; this is safe to do outside the lock
+ // as the pointer won't change until markers are changed at the end of this process
+ theRowData = currentDataStoreEntry->data;
+ fieldLengths = currentDataStoreEntry->dataLengths;
+
+ // Convert each of the cells in the row in turn
+ unsigned long fieldLength;
+ id cellData;
+ char *rawCellData;
+ for (NSUInteger i = 0; i < numberOfFields; i++) {
+ fieldLength = fieldLengths[i];
+
+ // If the length of this cell is NSNotFound, it's a null reference
+ if (fieldLength == NSNotFound) {
+ cellData = nil;
+
+ // Otherwise grab a reference to that data using pointer arithmetic
+ } else {
+ rawCellData = theRowData + copiedDataLength;
+ copiedDataLength += fieldLength;
+
+ // Convert to the correct object type
+ cellData = SPMySQLResultGetObject(self, rawCellData, fieldLength, 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]];
+ }
+ }
+
+ // Get a reference to the current item
+ SPMySQLStreamingRowData *previousDataStoreEntry = currentDataStoreEntry;
+
+ // Lock the mutex before updating counters and linked lists
+ pthread_mutex_lock(&dataLock);
+
+ // Update the active-data pointer to the next item in the list (which may be NULL)
+ currentDataStoreEntry = currentDataStoreEntry->nextRow;
+ if (!currentDataStoreEntry) lastDataStoreEntry = NULL;
+
+ // Increment the processed counter and row index
+ processedRowCount++;
+ currentRowIndex++;
+ if (dataDownloaded && processedRowCount == downloadedRowCount) currentRowIndex = NSNotFound;
+
+ // Unlock the mutex
+ pthread_mutex_unlock(&dataLock);
+
+ // Free the memory for the processed row
+ free(previousDataStoreEntry->dataLengths);
+ if (previousDataStoreEntry->data != NULL) free(previousDataStoreEntry->data);
+ free(previousDataStoreEntry);
+
+ return theReturnData;
+}
+
+/*
+ * Ensure the result set is fully processed and freed without any processing
+ * This method ensures that the connection is unlocked.
+ */
+- (void)cancelResultLoad
+{
+
+ // If data has already been downloaded successfully, no further action is required
+ if (dataDownloaded && processedRowCount == downloadedRowCount) return;
+
+ // Loop until all data is fetched and freed
+ while (1) {
+
+ // Check to see whether we need to wait for the data to be available
+ // - if so, wait 1ms before checking again
+ while (!dataDownloaded && processedRowCount == downloadedRowCount) usleep(1000);
+
+ // If all rows have been processed, we're at the end of the result set - return
+ if (processedRowCount == downloadedRowCount) {
+
+ // We don't need to unlock the connection because the data loading thread
+ // has already taken care of that
+ return;
+ }
+
+ // Mark the row entry as processed without performing any actions
+ pthread_mutex_lock(&dataLock);
+ SPMySQLStreamingRowData *previousDataStoreEntry = currentDataStoreEntry;
+
+ // Update the active-data pointer to the next item in the list (which may be NULL)
+ currentDataStoreEntry = currentDataStoreEntry->nextRow;
+ if (!currentDataStoreEntry) lastDataStoreEntry = NULL;
+
+ processedRowCount++;
+ currentRowIndex++;
+ if (dataDownloaded && processedRowCount == downloadedRowCount) currentRowIndex = NSNotFound;
+
+ // Unlock the mutex
+ pthread_mutex_unlock(&dataLock);
+
+ // Free the memory for the processed row
+ free(previousDataStoreEntry->dataLengths);
+ if (previousDataStoreEntry->data != NULL) free(previousDataStoreEntry->data);
+ free(previousDataStoreEntry);
+ }
+}
+
+#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
+{
+
+ // To avoid lock issues, return one row at a time.
+ id nextRow = SPMySQLResultGetRow(self, SPMySQLResultRowAsDefault);
+
+ // If no row was available, return 0 to stop iteration.
+ if (!nextRow) return 0;
+
+ // Otherwise, add the item to the buffer and return the appropriate state.
+ stackbuf[0] = nextRow;
+
+ state->state += 1;
+ state->itemsPtr = stackbuf;
+ state->mutationsPtr = (unsigned long *)self;
+
+ return 1;
+}
+
+@end
+
+#pragma mark -
+#pragma mark Result set internals
+
+@implementation SPMySQLFastStreamingResult (Private_API)
+
+/**
+ * Used internally to download results in a background thread
+ */
+- (void)_downloadAllData
+{
+ NSAutoreleasePool *downloadPool = [[NSAutoreleasePool alloc] init];
+ MYSQL_ROW theRow;
+ unsigned long *fieldLengths;
+ NSUInteger i, dataCopiedLength, rowDataLength;
+ SPMySQLStreamingRowData *newRowStore;
+
+ size_t sizeOfStreamingRowData = sizeof(SPMySQLStreamingRowData);
+ size_t sizeOfDataLengths = (size_t)(sizeof(unsigned long) * numberOfFields);
+ size_t sizeOfChar = sizeof(char);
+
+ // Loop through the rows until the end of the data is reached - indicated via a NULL
+ while (
+ (*isConnectedPtr)(parentConnection, isConnectedSelector)
+ && (theRow = mysql_fetch_row(resultSet))
+ )
+ {
+
+ // Retrieve the lengths of the returned data
+ fieldLengths = mysql_fetch_lengths(resultSet);
+ rowDataLength = 0;
+ dataCopiedLength = 0;
+ for (i = 0; i < numberOfFields; i++) {
+ rowDataLength += fieldLengths[i];
+ }
+
+ // Initialise memory for the row and set a NULL pointer for the next item
+ newRowStore = malloc(sizeOfStreamingRowData);
+ newRowStore->nextRow = NULL;
+
+ // Set up the row data store - a char* - and copy in the data if there is any.
+ newRowStore->data = malloc(sizeOfChar * rowDataLength);
+ for (i = 0; i < numberOfFields; i++) {
+ if (theRow[i] != NULL) {
+ memcpy(newRowStore->data+dataCopiedLength, theRow[i], fieldLengths[i]);
+ dataCopiedLength += fieldLengths[i];
+ } else {
+ fieldLengths[i] = NSNotFound;
+ }
+ }
+
+ // Set up the memory for, and copy in, the field lengths
+ newRowStore->dataLengths = memcpy(malloc(sizeOfDataLengths), fieldLengths, sizeOfDataLengths);
+
+ // Lock the data mutex
+ pthread_mutex_lock(&dataLock);
+
+ // Add the newly allocated row to end of the storage linked list
+ if (lastDataStoreEntry) {
+ lastDataStoreEntry->nextRow = newRowStore;
+ }
+ lastDataStoreEntry = newRowStore;
+ if (!currentDataStoreEntry) currentDataStoreEntry = newRowStore;
+
+ // Update the downloaded row count
+ downloadedRowCount++;
+
+ // Unlock the mutex
+ pthread_mutex_unlock(&dataLock);
+ }
+
+ // Update the connection's error statuses to reflect any errors during the content download
+ [parentConnection _updateLastErrorID:NSNotFound];
+ [parentConnection _updateLastErrorMessage:nil];
+
+ // Unlock the parent connection now all data has been retrieved
+ [parentConnection _unlockConnection];
+ connectionUnlocked = YES;
+
+ dataDownloaded = YES;
+ [downloadPool drain];
+}
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLFramework_Prefix.pch b/Frameworks/SPMySQLFramework/Source/SPMySQLFramework_Prefix.pch
new file mode 100644
index 00000000..8528c29c
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLFramework_Prefix.pch
@@ -0,0 +1,11 @@
+//
+// Prefix header for all source files of the 'SPMySQLFramework' target in the 'SPMySQLFramework' project.
+//
+
+#ifdef __OBJC__
+ #import <Cocoa/Cocoa.h>
+#endif
+
+#import "mysql.h"
+#import "SPMySQL.h"
+#import "SPMySQLUtilities.h" \ No newline at end of file
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLGeometryData.h b/Frameworks/SPMySQLFramework/Source/SPMySQLGeometryData.h
new file mode 100644
index 00000000..e1313032
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLGeometryData.h
@@ -0,0 +1,53 @@
+//
+// $Id$
+//
+// SPMySQLGeometryData.h
+// sequel-pro
+//
+// Created by Hans-Jörg Bibiko on October 07, 2010
+//
+// 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/>
+
+
+@interface SPMySQLGeometryData : NSObject
+{
+ // Holds the WKB bytes coming from SQL server
+ Byte *geoBuffer;
+
+ // Holds the buffer length
+ NSUInteger bufferLength;
+
+}
+
+- (id)initWithBytes:(const void *)geoData length:(NSUInteger)length;
++ (id)dataWithBytes:(const void *)geoData length:(NSUInteger)length;
+- (NSString *)description;
+- (NSUInteger)length;
+- (NSData *)data;
+- (NSString *)wktString;
+- (NSDictionary *)coordinates;
+- (NSInteger)wkbType;
+- (NSString *)wktType;
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLGeometryData.m b/Frameworks/SPMySQLFramework/Source/SPMySQLGeometryData.m
new file mode 100644
index 00000000..3c37e403
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLGeometryData.m
@@ -0,0 +1,810 @@
+//
+// $Id$
+//
+// SPMySQLGeometryData.m
+// sequel-pro
+//
+// Created by Hans-Jörg Bibiko on October 07, 2010
+//
+// 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 "SPMySQLGeometryData.h"
+
+enum wkbType
+{
+ wkb_point = 1,
+ wkb_linestring = 2,
+ wkb_polygon = 3,
+ wkb_multipoint = 4,
+ wkb_multilinestring = 5,
+ wkb_multipolygon = 6,
+ wkb_geometrycollection = 7
+};
+
+typedef struct st_point_2d_
+{
+ double x;
+ double y;
+} st_point_2d;
+
+#define SIZEOF_STORED_UINT32 4
+#define SIZEOF_STORED_DOUBLE 8
+#define POINT_DATA_SIZE (SIZEOF_STORED_DOUBLE*2)
+#define WKB_HEADER_SIZE (1+SIZEOF_STORED_UINT32)
+#define BUFFER_START 0
+
+@implementation SPMySQLGeometryData
+
+/**
+ * Initialize the SPMySQLGeometryData object
+ */
+- (id)init
+{
+ if ((self = [super init])) {
+ geoBuffer = nil;
+ bufferLength = 0;
+ }
+ return self;
+}
+
+/**
+ * Initialize the SPMySQLGeometryData object with the WKB data
+ */
+- (id)initWithBytes:(const void *)geoData length:(NSUInteger)length
+{
+ if ((self = [self init])) {
+ bufferLength = length;
+ geoBuffer = malloc(bufferLength);
+ memcpy(geoBuffer, geoData, bufferLength);
+ }
+ return self;
+}
+
+/**
+ * Return an autorelease SPMySQLGeometryData object
+ */
++ (id)dataWithBytes:(const void *)geoData length:(NSUInteger)length
+{
+ return [[[SPMySQLGeometryData alloc] initWithBytes:geoData length:length] autorelease];
+}
+
+/**
+ * copyWithZone
+ */
+- (id)copyWithZone:(NSZone *)zone
+{
+ return [self retain];
+}
+
+/**
+ * Return the hex representation of the WKB buffer (only for convenience)
+ */
+- (NSString*)description
+{
+ return [[NSData dataWithBytes:geoBuffer length:bufferLength] description];
+}
+
+/**
+ * Return the length of the WKB buffer
+ */
+- (NSUInteger)length
+{
+ return bufferLength;
+}
+
+/**
+ * Return NSData pointer of the WKB buffer
+ */
+- (NSData *)data
+{
+ return [NSData dataWithBytes:geoBuffer length:bufferLength];
+}
+
+/**
+ * Return a human readable WKT string of the internal format (imitating the SQL function AsText()).
+ */
+- (NSString *)wktString
+{
+ char byteOrder;
+ uint32_t geoType, numberOfItems, numberOfSubItems, numberOfSubSubItems, numberOfCollectionItems;
+ int32_t srid;
+ st_point_2d aPoint;
+
+ uint32_t i, j, k, n; // Loop counter for numberOf...Items
+ uint32_t ptr = BUFFER_START; // pointer to geoBuffer while parsing
+
+ NSMutableString *wkt = [NSMutableString string];
+
+ if (bufferLength < WKB_HEADER_SIZE)
+ return @"";
+
+ memcpy(&srid, &geoBuffer[0], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32;
+
+ byteOrder = (char)geoBuffer[ptr];
+
+ if (byteOrder != 0x1)
+ return @"Byte order not yet supported";
+
+ ptr++;
+ geoType = geoBuffer[ptr];
+ ptr += SIZEOF_STORED_UINT32;
+
+ switch (geoType) {
+
+ case wkb_point:
+ memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE);
+ return [NSString stringWithFormat:@"POINT(%.16g %.16g)%@", aPoint.x, aPoint.y, (srid) ? [NSString stringWithFormat:@",%d",srid]: @""];
+ break;
+
+ case wkb_linestring:
+ [wkt setString:@"LINESTRING("];
+ memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32;
+ for (i=0; i < numberOfItems; i++) {
+ memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE);
+ [wkt appendFormat:@"%.16g %.16g%@", aPoint.x, aPoint.y, (i < numberOfItems-1) ? @"," : @""];
+ ptr += POINT_DATA_SIZE;
+ }
+ [wkt appendFormat:@")%@", (srid) ? [NSString stringWithFormat:@",%d",srid]: @""];
+ return wkt;
+ break;
+
+ case wkb_polygon:
+ [wkt setString:@"POLYGON("];
+ memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32;
+ for (i=0; i < numberOfItems; i++) {
+ memcpy(&numberOfSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32;
+ [wkt appendString:@"("];
+ for (j=0; j < numberOfSubItems; j++) {
+ memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE);
+ [wkt appendFormat:@"%.16g %.16g%@", aPoint.x, aPoint.y, (j < numberOfSubItems-1) ? @"," : @""];
+ ptr += POINT_DATA_SIZE;
+ }
+ [wkt appendFormat:@")%@", (i < numberOfItems-1) ? @"," : @""];
+ }
+ [wkt appendFormat:@")%@", (srid) ? [NSString stringWithFormat:@",%d",srid]: @""];
+ return wkt;
+ break;
+
+ case wkb_multipoint:
+ [wkt setString:@"MULTIPOINT("];
+ memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32+WKB_HEADER_SIZE;
+ for (i=0; i < numberOfItems; i++) {
+ memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE);
+ [wkt appendFormat:@"%.16g %.16g%@", aPoint.x, aPoint.y, (i < numberOfItems-1) ? @"," : @""];
+ ptr += POINT_DATA_SIZE+WKB_HEADER_SIZE;
+ }
+ [wkt appendFormat:@")%@", (srid) ? [NSString stringWithFormat:@",%d",srid]: @""];
+ return wkt;
+ break;
+
+ case wkb_multilinestring:
+ [wkt setString:@"MULTILINESTRING("];
+ memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32+WKB_HEADER_SIZE;
+ for (i=0; i < numberOfItems; i++) {
+ memcpy(&numberOfSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32;
+ [wkt appendString:@"("];
+ for (j=0; j < numberOfSubItems; j++) {
+ memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE);
+ [wkt appendFormat:@"%.16g %.16g%@", aPoint.x, aPoint.y, (j < numberOfSubItems-1) ? @"," : @""];
+ ptr += POINT_DATA_SIZE;
+ }
+ ptr += WKB_HEADER_SIZE;
+ [wkt appendFormat:@")%@", (i < numberOfItems-1) ? @"," : @""];
+ }
+ [wkt appendFormat:@")%@", (srid) ? [NSString stringWithFormat:@",%d",srid]: @""];
+ return wkt;
+ break;
+
+ case wkb_multipolygon:
+ [wkt setString:@"MULTIPOLYGON("];
+ memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32+WKB_HEADER_SIZE;
+ for (i=0; i < numberOfItems; i++) {
+ memcpy(&numberOfSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32;
+ [wkt appendString:@"("];
+ for (j=0; j < numberOfSubItems; j++) {
+ memcpy(&numberOfSubSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32;
+ [wkt appendString:@"("];
+ for (k=0; k < numberOfSubSubItems; k++) {
+ memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE);
+ [wkt appendFormat:@"%.16g %.16g%@", aPoint.x, aPoint.y, (k < numberOfSubSubItems-1) ? @"," : @""];
+ ptr += POINT_DATA_SIZE;
+ }
+ [wkt appendFormat:@")%@", (j < numberOfSubItems-1) ? @"," : @""];
+ }
+ ptr += WKB_HEADER_SIZE;
+ [wkt appendFormat:@")%@", (i < numberOfItems-1) ? @"," : @""];
+ }
+ [wkt appendFormat:@")%@", (srid) ? [NSString stringWithFormat:@",%d",srid]: @""];
+ return wkt;
+ break;
+
+ case wkb_geometrycollection:
+ [wkt setString:@"GEOMETRYCOLLECTION("];
+ numberOfCollectionItems = geoBuffer[ptr];
+ ptr += SIZEOF_STORED_UINT32;
+
+ for (n=0; n < numberOfCollectionItems; n++) {
+
+ byteOrder = (char)geoBuffer[ptr];
+
+ if(byteOrder != 0x1)
+ return @"Byte order not yet supported";
+
+ ptr++;
+ geoType = geoBuffer[ptr];
+ ptr += SIZEOF_STORED_UINT32;
+
+ switch(geoType) {
+
+ case wkb_point:
+ memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE);
+ [wkt appendFormat:@"POINT(%.16g %.16g)", aPoint.x, aPoint.y];
+ ptr += POINT_DATA_SIZE;
+ break;
+
+ case wkb_linestring:
+ [wkt appendString:@"LINESTRING("];
+ memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32;
+ for (i=0; i < numberOfItems; i++) {
+ memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE);
+ [wkt appendFormat:@"%.16g %.16g%@", aPoint.x, aPoint.y, (i < numberOfItems-1) ? @"," : @""];
+ ptr += POINT_DATA_SIZE;
+ }
+ [wkt appendString:@")"];
+ break;
+
+ case wkb_polygon:
+ [wkt appendString:@"POLYGON("];
+ memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32;
+ for (i=0; i < numberOfItems; i++) {
+ memcpy(&numberOfSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32;
+ [wkt appendString:@"("];
+ for (j=0; j < numberOfSubItems; j++) {
+ memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE);
+ [wkt appendFormat:@"%.16g %.16g%@", aPoint.x, aPoint.y, (j < numberOfSubItems-1) ? @"," : @""];
+ ptr += POINT_DATA_SIZE;
+ }
+ [wkt appendFormat:@")%@", (i < numberOfItems-1) ? @"," : @""];
+ }
+ [wkt appendString:@")"];
+ break;
+
+ case wkb_multipoint:
+ [wkt appendString:@"MULTIPOINT("];
+ memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32+WKB_HEADER_SIZE;
+ for (i=0; i < numberOfItems; i++) {
+ memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE);
+ [wkt appendFormat:@"%.16g %.16g%@", aPoint.x, aPoint.y, (i < numberOfItems-1) ? @"," : @""];
+ ptr += POINT_DATA_SIZE+WKB_HEADER_SIZE;
+ }
+ ptr -= WKB_HEADER_SIZE;
+ [wkt appendString:@")"];
+ break;
+
+ case wkb_multilinestring:
+ [wkt appendString:@"MULTILINESTRING("];
+ memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32+WKB_HEADER_SIZE;
+ for (i=0; i < numberOfItems; i++) {
+ memcpy(&numberOfSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32;
+ [wkt appendString:@"("];
+ for (j=0; j < numberOfSubItems; j++) {
+ memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE);
+ [wkt appendFormat:@"%.16g %.16g%@", aPoint.x, aPoint.y, (j < numberOfSubItems-1) ? @"," : @""];
+ ptr += POINT_DATA_SIZE;
+ }
+ ptr += WKB_HEADER_SIZE;
+ [wkt appendFormat:@")%@", (i < numberOfItems-1) ? @"," : @""];
+ }
+ ptr -= WKB_HEADER_SIZE;
+ [wkt appendString:@")"];
+ break;
+
+ case wkb_multipolygon:
+ [wkt appendString:@"MULTIPOLYGON("];
+ memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32+WKB_HEADER_SIZE;
+ for (i=0; i < numberOfItems; i++) {
+ memcpy(&numberOfSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32;
+ [wkt appendString:@"("];
+ for (j=0; j < numberOfSubItems; j++) {
+ memcpy(&numberOfSubSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32;
+ [wkt appendString:@"("];
+ for (k=0; k < numberOfSubSubItems; k++) {
+ memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE);
+ [wkt appendFormat:@"%.16g %.16g%@", aPoint.x, aPoint.y, (k < numberOfSubSubItems-1) ? @"," : @""];
+ ptr += POINT_DATA_SIZE;
+ }
+ [wkt appendFormat:@")%@", (j < numberOfSubItems-1) ? @"," : @""];
+ }
+ ptr += WKB_HEADER_SIZE;
+ [wkt appendFormat:@")%@", (i < numberOfItems-1) ? @"," : @""];
+ }
+ ptr -= WKB_HEADER_SIZE;
+ [wkt appendString:@")"];
+ break;
+
+ default:
+ return @"Error geometrycollection type parsing";
+ }
+ [wkt appendString:(n < numberOfCollectionItems-1) ? @"," : @""];
+ }
+ [wkt appendFormat:@")%@", (srid) ? [NSString stringWithFormat:@",%d",srid]: @""];
+ return wkt;
+ break;
+
+ default:
+ return @"Error geometry type parsing";
+ }
+
+ return @"Error while parsing";
+}
+
+/**
+ * Return a dictionary of coordinates, bbox, etc. to be able to draw the given geometry.
+ *
+ * @return A dictionary having the following keys: "bbox" as NSArray of NSNumbers of x_min x_max y_min y_max, "coordinates" as NSArray containing the
+ * the to be drawn points as NSPoint strings, "type" as NSString
+ */
+- (NSDictionary *)coordinates
+{
+ char byteOrder;
+ uint32_t geoType, numberOfItems, numberOfSubItems, numberOfSubSubItems, numberOfCollectionItems;
+ int32_t srid;
+ st_point_2d aPoint;
+
+ uint32_t i, j, k, n; // Loop counter for numberOf...Items
+ uint32_t ptr = BUFFER_START; // pointer to geoBuffer while parsing
+
+ double x_min = DBL_MAX;
+ double x_max = -DBL_MAX;
+ double y_min = DBL_MAX;
+ double y_max = -DBL_MAX;
+
+ NSMutableArray *coordinates = [NSMutableArray array];
+ NSMutableArray *subcoordinates = [NSMutableArray array];
+ NSMutableArray *pointcoordinates = [NSMutableArray array];
+ NSMutableArray *linecoordinates = [NSMutableArray array];
+ NSMutableArray *linesubcoordinates = [NSMutableArray array];
+ NSMutableArray *polygoncoordinates = [NSMutableArray array];
+ NSMutableArray *polygonsubcoordinates = [NSMutableArray array];
+
+ if (bufferLength < WKB_HEADER_SIZE)
+ return nil;
+
+ memcpy(&srid, &geoBuffer[0], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32;
+
+ byteOrder = (char)geoBuffer[ptr];
+
+ if (byteOrder != 0x1)
+ return nil;
+
+ ptr++;
+ geoType = geoBuffer[ptr];
+ ptr += SIZEOF_STORED_UINT32;
+
+ switch(geoType) {
+
+ case wkb_point:
+ memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE);
+ x_min = aPoint.x;
+ x_max = aPoint.x;
+ y_min = aPoint.y;
+ y_max = aPoint.y;
+ [coordinates addObject:NSStringFromPoint(NSMakePoint((CGFloat)aPoint.x, (CGFloat)aPoint.y))];
+ return [NSDictionary dictionaryWithObjectsAndKeys:
+ [NSArray arrayWithObjects:
+ [NSNumber numberWithDouble:x_min],
+ [NSNumber numberWithDouble:x_max],
+ [NSNumber numberWithDouble:y_min],
+ [NSNumber numberWithDouble:y_max],
+ nil], @"bbox",
+ coordinates, @"coordinates",
+ [NSNumber numberWithInt:srid], @"srid",
+ @"POINT", @"type",
+ nil];
+ break;
+
+ case wkb_linestring:
+ memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32;
+ for (i=0; i < numberOfItems; i++) {
+ memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE);
+ x_min = (aPoint.x < x_min) ? aPoint.x : x_min;
+ x_max = (aPoint.x > x_max) ? aPoint.x : x_max;
+ y_min = (aPoint.y < y_min) ? aPoint.y : y_min;
+ y_max = (aPoint.y > y_max) ? aPoint.y : y_max;
+ [coordinates addObject:NSStringFromPoint(NSMakePoint((CGFloat)aPoint.x, (CGFloat)aPoint.y))];
+ ptr += POINT_DATA_SIZE;
+ }
+ return [NSDictionary dictionaryWithObjectsAndKeys:
+ [NSArray arrayWithObjects:
+ [NSNumber numberWithDouble:x_min],
+ [NSNumber numberWithDouble:x_max],
+ [NSNumber numberWithDouble:y_min],
+ [NSNumber numberWithDouble:y_max],
+ nil], @"bbox",
+ [NSArray arrayWithObjects:coordinates,nil], @"coordinates",
+ @"LINESTRING", @"type",
+ nil];
+ break;
+
+ case wkb_polygon:
+ memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32;
+ for (i=0; i < numberOfItems; i++) {
+ memcpy(&numberOfSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32;
+ for (j=0; j < numberOfSubItems; j++) {
+ memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE);
+ x_min = (aPoint.x < x_min) ? aPoint.x : x_min;
+ x_max = (aPoint.x > x_max) ? aPoint.x : x_max;
+ y_min = (aPoint.y < y_min) ? aPoint.y : y_min;
+ y_max = (aPoint.y > y_max) ? aPoint.y : y_max;
+ [subcoordinates addObject:NSStringFromPoint(NSMakePoint((CGFloat)aPoint.x, (CGFloat)aPoint.y))];
+ ptr += POINT_DATA_SIZE;
+ }
+ [coordinates addObject:[[subcoordinates copy] autorelease]];
+ [subcoordinates removeAllObjects];
+ }
+ return [NSDictionary dictionaryWithObjectsAndKeys:
+ [NSArray arrayWithObjects:
+ [NSNumber numberWithDouble:x_min],
+ [NSNumber numberWithDouble:x_max],
+ [NSNumber numberWithDouble:y_min],
+ [NSNumber numberWithDouble:y_max],
+ nil], @"bbox",
+ coordinates, @"coordinates",
+ [NSNumber numberWithInt:srid], @"srid",
+ @"POLYGON", @"type",
+ nil];
+ break;
+
+ case wkb_multipoint:
+ memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32+WKB_HEADER_SIZE;
+ for (i=0; i < numberOfItems; i++) {
+ memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE);
+ x_min = (aPoint.x < x_min) ? aPoint.x : x_min;
+ x_max = (aPoint.x > x_max) ? aPoint.x : x_max;
+ y_min = (aPoint.y < y_min) ? aPoint.y : y_min;
+ y_max = (aPoint.y > y_max) ? aPoint.y : y_max;
+ [coordinates addObject:NSStringFromPoint(NSMakePoint((CGFloat)aPoint.x, (CGFloat)aPoint.y))];
+ ptr += POINT_DATA_SIZE+WKB_HEADER_SIZE;
+ }
+ return [NSDictionary dictionaryWithObjectsAndKeys:
+ [NSArray arrayWithObjects:
+ [NSNumber numberWithDouble:x_min],
+ [NSNumber numberWithDouble:x_max],
+ [NSNumber numberWithDouble:y_min],
+ [NSNumber numberWithDouble:y_max],
+ nil], @"bbox",
+ coordinates, @"coordinates",
+ [NSNumber numberWithInt:srid], @"srid",
+ @"MULTIPOINT", @"type",
+ nil];
+ break;
+
+ case wkb_multilinestring:
+ memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32+WKB_HEADER_SIZE;
+ for (i=0; i < numberOfItems; i++) {
+ memcpy(&numberOfSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32;
+ for (j=0; j < numberOfSubItems; j++) {
+ memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE);
+ x_min = (aPoint.x < x_min) ? aPoint.x : x_min;
+ x_max = (aPoint.x > x_max) ? aPoint.x : x_max;
+ y_min = (aPoint.y < y_min) ? aPoint.y : y_min;
+ y_max = (aPoint.y > y_max) ? aPoint.y : y_max;
+ [subcoordinates addObject:NSStringFromPoint(NSMakePoint((CGFloat)aPoint.x, (CGFloat)aPoint.y))];
+ ptr += POINT_DATA_SIZE;
+ }
+ ptr += WKB_HEADER_SIZE;
+ [coordinates addObject:[[subcoordinates copy] autorelease]];
+ [subcoordinates removeAllObjects];
+ }
+ return [NSDictionary dictionaryWithObjectsAndKeys:
+ [NSArray arrayWithObjects:
+ [NSNumber numberWithDouble:x_min],
+ [NSNumber numberWithDouble:x_max],
+ [NSNumber numberWithDouble:y_min],
+ [NSNumber numberWithDouble:y_max],
+ nil], @"bbox",
+ coordinates, @"coordinates",
+ [NSNumber numberWithInt:srid], @"srid",
+ @"MULTILINESTRING", @"type",
+ nil];
+ break;
+
+ case wkb_multipolygon:
+ memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32+WKB_HEADER_SIZE;
+ for (i=0; i < numberOfItems; i++) {
+ memcpy(&numberOfSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32;
+ for (j=0; j < numberOfSubItems; j++) {
+ memcpy(&numberOfSubSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32;
+ for (k=0; k < numberOfSubSubItems; k++) {
+ memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE);
+ x_min = (aPoint.x < x_min) ? aPoint.x : x_min;
+ x_max = (aPoint.x > x_max) ? aPoint.x : x_max;
+ y_min = (aPoint.y < y_min) ? aPoint.y : y_min;
+ y_max = (aPoint.y > y_max) ? aPoint.y : y_max;
+ [subcoordinates addObject:NSStringFromPoint(NSMakePoint((CGFloat)aPoint.x, (CGFloat)aPoint.y))];
+ ptr += POINT_DATA_SIZE;
+ }
+ [coordinates addObject:[[subcoordinates copy] autorelease]];
+ [subcoordinates removeAllObjects];
+ }
+ ptr += WKB_HEADER_SIZE;
+ }
+ return [NSDictionary dictionaryWithObjectsAndKeys:
+ [NSArray arrayWithObjects:
+ [NSNumber numberWithDouble:x_min],
+ [NSNumber numberWithDouble:x_max],
+ [NSNumber numberWithDouble:y_min],
+ [NSNumber numberWithDouble:y_max],
+ nil], @"bbox",
+ coordinates, @"coordinates",
+ [NSNumber numberWithInt:srid], @"srid",
+ @"MULTIPOLYGON", @"type",
+ nil];
+ break;
+
+ case wkb_geometrycollection:
+ numberOfCollectionItems = geoBuffer[ptr];
+ ptr += SIZEOF_STORED_UINT32;
+
+ for (n=0; n < numberOfCollectionItems; n++) {
+
+ byteOrder = (char)geoBuffer[ptr];
+
+ if (byteOrder != 0x1)
+ return nil;
+
+ ptr++;
+ geoType = geoBuffer[ptr];
+ ptr += SIZEOF_STORED_UINT32;
+
+ switch(geoType) {
+
+ case wkb_point:
+ memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE);
+ x_min = (aPoint.x < x_min) ? aPoint.x : x_min;
+ x_max = (aPoint.x > x_max) ? aPoint.x : x_max;
+ y_min = (aPoint.y < y_min) ? aPoint.y : y_min;
+ y_max = (aPoint.y > y_max) ? aPoint.y : y_max;
+ [pointcoordinates addObject:NSStringFromPoint(NSMakePoint((CGFloat)aPoint.x, (CGFloat)aPoint.y))];
+ ptr += POINT_DATA_SIZE;
+ break;
+
+ case wkb_linestring:
+ memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32;
+ for (i=0; i < numberOfItems; i++) {
+ memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE);
+ x_min = (aPoint.x < x_min) ? aPoint.x : x_min;
+ x_max = (aPoint.x > x_max) ? aPoint.x : x_max;
+ y_min = (aPoint.y < y_min) ? aPoint.y : y_min;
+ y_max = (aPoint.y > y_max) ? aPoint.y : y_max;
+ [linesubcoordinates addObject:NSStringFromPoint(NSMakePoint((CGFloat)aPoint.x, (CGFloat)aPoint.y))];
+ ptr += POINT_DATA_SIZE;
+ }
+ [linecoordinates addObject:[[linesubcoordinates copy] autorelease]];
+ [linesubcoordinates removeAllObjects];
+ break;
+
+ case wkb_polygon:
+ memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32;
+ for (i=0; i < numberOfItems; i++) {
+ memcpy(&numberOfSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32;
+ for (j=0; j < numberOfSubItems; j++) {
+ memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE);
+ x_min = (aPoint.x < x_min) ? aPoint.x : x_min;
+ x_max = (aPoint.x > x_max) ? aPoint.x : x_max;
+ y_min = (aPoint.y < y_min) ? aPoint.y : y_min;
+ y_max = (aPoint.y > y_max) ? aPoint.y : y_max;
+ [polygonsubcoordinates addObject:NSStringFromPoint(NSMakePoint((CGFloat)aPoint.x, (CGFloat)aPoint.y))];
+ ptr += POINT_DATA_SIZE;
+ }
+ [polygoncoordinates addObject:[[polygonsubcoordinates copy] autorelease]];
+ [polygonsubcoordinates removeAllObjects];
+ }
+ break;
+
+ case wkb_multipoint:
+ memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32+WKB_HEADER_SIZE;
+ for (i=0; i < numberOfItems; i++) {
+ memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE);
+ x_min = (aPoint.x < x_min) ? aPoint.x : x_min;
+ x_max = (aPoint.x > x_max) ? aPoint.x : x_max;
+ y_min = (aPoint.y < y_min) ? aPoint.y : y_min;
+ y_max = (aPoint.y > y_max) ? aPoint.y : y_max;
+ [pointcoordinates addObject:NSStringFromPoint(NSMakePoint((CGFloat)aPoint.x, (CGFloat)aPoint.y))];
+ ptr += POINT_DATA_SIZE+WKB_HEADER_SIZE;
+ }
+ ptr -= WKB_HEADER_SIZE;
+ break;
+
+ case wkb_multilinestring:
+ memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32+WKB_HEADER_SIZE;
+ for (i=0; i < numberOfItems; i++) {
+ memcpy(&numberOfSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32;
+ for (j=0; j < numberOfSubItems; j++) {
+ memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE);
+ x_min = (aPoint.x < x_min) ? aPoint.x : x_min;
+ x_max = (aPoint.x > x_max) ? aPoint.x : x_max;
+ y_min = (aPoint.y < y_min) ? aPoint.y : y_min;
+ y_max = (aPoint.y > y_max) ? aPoint.y : y_max;
+ [linesubcoordinates addObject:NSStringFromPoint(NSMakePoint((CGFloat)aPoint.x, (CGFloat)aPoint.y))];
+ ptr += POINT_DATA_SIZE;
+ }
+ [linecoordinates addObject:[[linesubcoordinates copy] autorelease]];
+ [linesubcoordinates removeAllObjects];
+ ptr += WKB_HEADER_SIZE;
+ }
+ ptr -= WKB_HEADER_SIZE;
+ break;
+
+ case wkb_multipolygon:
+ memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32+WKB_HEADER_SIZE;
+ for (i=0; i < numberOfItems; i++) {
+ memcpy(&numberOfSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32;
+ for (j=0; j < numberOfSubItems; j++) {
+ memcpy(&numberOfSubSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32);
+ ptr += SIZEOF_STORED_UINT32;
+ for (k=0; k < numberOfSubSubItems; k++) {
+ memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE);
+ x_min = (aPoint.x < x_min) ? aPoint.x : x_min;
+ x_max = (aPoint.x > x_max) ? aPoint.x : x_max;
+ y_min = (aPoint.y < y_min) ? aPoint.y : y_min;
+ y_max = (aPoint.y > y_max) ? aPoint.y : y_max;
+ [polygonsubcoordinates addObject:NSStringFromPoint(NSMakePoint((CGFloat)aPoint.x, (CGFloat)aPoint.y))];
+ ptr += POINT_DATA_SIZE;
+ }
+ [polygoncoordinates addObject:[[polygonsubcoordinates copy] autorelease]];
+ [polygonsubcoordinates removeAllObjects];
+ }
+ ptr += WKB_HEADER_SIZE;
+ }
+ ptr -= WKB_HEADER_SIZE;
+ break;
+
+ default:
+ return nil;
+ }
+ }
+ return [NSDictionary dictionaryWithObjectsAndKeys:
+ [NSArray arrayWithObjects:
+ [NSNumber numberWithDouble:x_min],
+ [NSNumber numberWithDouble:x_max],
+ [NSNumber numberWithDouble:y_min],
+ [NSNumber numberWithDouble:y_max],
+ nil], @"bbox",
+ [NSArray arrayWithObjects:pointcoordinates, linecoordinates, polygoncoordinates, nil], @"coordinates",
+ @"GEOMETRYCOLLECTION", @"type",
+ nil];
+ break;
+
+ default:
+ return nil;
+ }
+
+ return nil;
+}
+
+/**
+ * Return the WKB type of the geoBuffer ie if buffer represents a POINT, LINESTRING, etc.
+ * according to stored wkbType in header file. It returns -1 if an error occurred.
+ */
+- (NSInteger)wkbType
+{
+ char byteOrder;
+ SInt32 geoType;
+
+ NSUInteger ptr = BUFFER_START; // pointer to geoBuffer while parsing
+
+ if (bufferLength < WKB_HEADER_SIZE)
+ return -1;
+
+ byteOrder = (char)geoBuffer[ptr];
+
+ if (byteOrder != 0x1)
+ return -1;
+
+ ptr++;
+ geoType = geoBuffer[ptr];
+
+ if (geoType > 0 && geoType < 8)
+ return geoType;
+ else
+ return -1;
+
+}
+
+/**
+ * Return the WKT type of the geoBuffer ie if buffer represents a POINT, LINESTRING, etc.
+ * according to stored wkbType in header file. It returns nil if an error occurred.
+ */
+- (NSString *)wktType
+{
+ switch ([self wkbType])
+ {
+ case wkb_point:
+ return @"POINT";
+ case wkb_linestring:
+ return @"LINESTRING";
+ case wkb_polygon:
+ return @"POLYGON";
+ case wkb_multipoint:
+ return @"MULTIPOINT";
+ case wkb_multilinestring:
+ return @"MULTILINESTRING";
+ case wkb_multipolygon:
+ return @"MULTIPOLYGON";
+ case wkb_geometrycollection:
+ return @"GEOMETRYCOLLECTION";
+ default:
+ return nil;
+ }
+ return nil;
+}
+
+/**
+ * dealloc
+ */
+- (void)dealloc
+{
+ if (geoBuffer && bufferLength) free(geoBuffer);
+ [super dealloc];
+}
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLKeepAliveTimer.h b/Frameworks/SPMySQLFramework/Source/SPMySQLKeepAliveTimer.h
new file mode 100644
index 00000000..ebe46bed
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLKeepAliveTimer.h
@@ -0,0 +1,45 @@
+//
+// $Id$
+//
+// SPMySQLKeepAliveTimer.h
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on March 5, 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/>
+
+
+@interface SPMySQLKeepAliveTimer : NSObject {
+ id timerTarget;
+ SEL timerSelector;
+ NSTimeInterval timerRepeatInterval;
+
+ NSTimer *wrappedTimer;
+}
+
+- (id)initWithInterval:(NSTimeInterval)anInterval target:(id)aTarget selector:(SEL)aSelector;
+- (void)invalidate;
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLKeepAliveTimer.m b/Frameworks/SPMySQLFramework/Source/SPMySQLKeepAliveTimer.m
new file mode 100644
index 00000000..f9164aff
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLKeepAliveTimer.m
@@ -0,0 +1,127 @@
+//
+// $Id$
+//
+// SPMySQLKeepAliveTimer.m
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on March 5, 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 "SPMySQLKeepAliveTimer.h"
+#import "SPMySQL Private APIs.h"
+
+@interface SPMySQLKeepAliveTimer (Private_API)
+
+- (void)_initKeepAliveTimer;
+- (void)_forwardPing;
+
+@end
+
+#pragma mark -
+
+@implementation SPMySQLKeepAliveTimer
+
+/**
+ * Prevent SPMySQLKeepAliveTimer from being init'd normally.
+ */
+- (id)init
+{
+ [NSException raise:NSInternalInconsistencyException format:@"SPMySQLKeepAliveTimers should not be init'd directly; use initWithInterval:target:selector: instead."];
+ return nil;
+}
+
+/**
+ * Initialise the SPMySQLKeepAliveTimer. This also sets up the contained timer,
+ * which has to be wrapped in this class to prevent retain cycles preventing the
+ * parent connection from being released.
+ *
+ * After initialisation, the delegate should be set to ensure that the timer events
+ * are received.
+ */
+- (id)initWithInterval:(NSTimeInterval)anInterval target:(id)aTarget selector:(SEL)aSelector
+{
+ if ((self = [super init])) {
+ wrappedTimer = nil;
+
+ // Keep a weak reference to the target
+ timerTarget = aTarget;
+ timerSelector = aSelector;
+ timerRepeatInterval = anInterval;
+
+ // Ensure the timer is set up on the main thread
+ if ([NSThread isMainThread]) {
+ [self _initKeepAliveTimer];
+ } else {
+ [self performSelectorOnMainThread:@selector(_initKeepAliveTimer) withObject:nil waitUntilDone:YES];
+ }
+ }
+
+ return self;
+}
+
+/**
+ * Invalidate the wrapped timer, which also releases the reference to the timer
+ * target (this object), breaking retain loops.
+ */
+- (void)invalidate
+{
+ if ([NSThread isMainThread]) {
+ [wrappedTimer invalidate];
+ } else {
+ [wrappedTimer performSelectorOnMainThread:@selector(invalidate) withObject:nil waitUntilDone:YES];
+ }
+}
+
+- (void)dealloc
+{
+ [wrappedTimer dealloc];
+ [super dealloc];
+}
+
+@end
+
+@implementation SPMySQLKeepAliveTimer (Private_API)
+
+/**
+ * Set up the timer to tickle the target. This must be set up on the main thread
+ * to ensure the timer events keep firing.
+ */
+- (void)_initKeepAliveTimer
+{
+ wrappedTimer = [[NSTimer scheduledTimerWithTimeInterval:timerRepeatInterval target:self selector:@selector(_forwardPing) userInfo:nil repeats:YES] retain];
+}
+
+/**
+ * Forward the NSTimer-fired ping to the target object. Performing this forwarding
+ * breaks the retain cycle.
+ */
+- (void)_forwardPing
+{
+ [timerTarget performSelector:timerSelector];
+}
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Convenience Methods.h b/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Convenience Methods.h
new file mode 100644
index 00000000..7d870a93
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Convenience Methods.h
@@ -0,0 +1,38 @@
+//
+// $Id$
+//
+// Convenience Methods.h
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on February 20, 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/>
+
+
+@interface SPMySQLResult (Convenience_Methods)
+
+- (NSArray *)getAllRows;
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Convenience Methods.m b/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Convenience Methods.m
new file mode 100644
index 00000000..2b049264
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Convenience Methods.m
@@ -0,0 +1,71 @@
+//
+// $Id$
+//
+// Convenience Methods.h
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on February 20, 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 "Convenience Methods.h"
+
+@implementation SPMySQLResult (Convenience_Methods)
+
+/**
+ * Iterates over the result set, retrieving all the rows, and returns them
+ * as an array.
+ * The rows are in the default format for this instance, as controlled via
+ * -setDefaultRowReturnType:.
+ * Returns nil if there are no rows to return.
+ */
+- (NSArray *)getAllRows
+{
+ unsigned long long previousSeekPosition = currentRowIndex;
+
+ NSMutableArray *rowsToReturn;
+
+ // If the number of rows is known, pre-set the size; otherwise just create an array
+ if (numberOfRows != NSNotFound) {
+ rowsToReturn = [[NSMutableArray alloc] initWithCapacity:(NSUInteger)numberOfRows];
+ } else {
+ rowsToReturn = [[NSMutableArray alloc] init];
+ }
+
+ // Loop through the rows in the instance-specified return format
+ for (id eachRow in self) {
+ [rowsToReturn addObject:eachRow];
+ }
+
+ // Seek to the previous position if appropriate
+ if (previousSeekPosition) [self seekToRow:previousSeekPosition];
+
+ // Instead of empty arrays, return nil if there are no rows.
+ if (![rowsToReturn count]) return nil;
+
+ return rowsToReturn;
+}
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Field Definitions.h b/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Field Definitions.h
new file mode 100644
index 00000000..20e1ddc9
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Field Definitions.h
@@ -0,0 +1,38 @@
+//
+// $Id$
+//
+// Field Definitions.h
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on February 2, 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/>
+
+
+@interface SPMySQLResult (Field_Definitions)
+
+- (NSArray *)fieldDefinitions;
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Field Definitions.m b/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Field Definitions.m
new file mode 100644
index 00000000..59e75d2f
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Field Definitions.m
@@ -0,0 +1,557 @@
+//
+// $Id$
+//
+// Field Definitions.m
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on February 2, 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 "Field Definitions.h"
+
+@interface SPMySQLResult (Field_Definitions_Private_API)
+
+- (NSUInteger)_findCharsetMaxByteLengthPerCharForMySQLNumber:(NSUInteger)charsetnr;
+- (NSString *)_charsetNameForMySQLNumber:(NSUInteger)charsetnr;
+- (NSString *)_charsetCollationForMySQLNumber:(NSUInteger)charsetnr;
+- (NSString *)_mysqlTypeToStringForType:(NSUInteger)type withCharsetNr:(NSUInteger)charsetnr withFlags:(NSUInteger)flags withLength:(unsigned long long)length;
+- (NSString *)_mysqlTypeToGroupForType:(NSUInteger)type withCharsetNr:(NSUInteger)charsetnr withFlags:(NSUInteger)flags;
+
+@end
+
+// Import a private declaration from the SPMySQLResult file for use
+@interface SPMySQLResult (Private_API)
+
+- (NSString *)_stringWithBytes:(const void *)bytes length:(NSUInteger)length;
+
+@end
+
+#define MAGIC_BINARY_CHARSET_NR 63
+
+const SPMySQLResultCharset SPMySQLCharsetMap[] =
+{
+ {1, "big5","big5_chinese_ci", 1, 2},
+ {3, "dec8", "dec8_swedisch_ci", 1, 1},
+ {4, "cp850", "cp850_general_ci", 1, 1},
+ {6, "hp8", "hp8_english_ci", 1, 1},
+ {7, "koi8r", "koi8r_general_ci", 1, 1},
+ {8, "latin1", "latin1_swedish_ci", 1, 1},
+ {9, "latin2", "latin2_general_ci", 1, 1},
+ {10, "swe7", "swe7_swedish_ci", 1, 1},
+ {11, "ascii", "ascii_general_ci", 1, 1},
+ {12, "ujis", "ujis_japanese_ci", 1, 3},
+ {13, "sjis", "sjis_japanese_ci", 1, 2},
+ {16, "hebrew", "hebrew_general_ci", 1, 1},
+ {18, "tis620", "tis620_thai_ci", 1, 1},
+ {19, "euckr", "euckr_korean_ci", 1, 2},
+ {22, "koi8u", "koi8u_general_ci", 1, 1},
+ {24, "gb2312", "gb2312_chinese_ci", 1, 2},
+ {25, "greek", "greek_general_ci", 1, 1},
+ {26, "cp1250", "cp1250_general_ci", 1, 1},
+ {28, "gbk", "gbk_chinese_ci", 1, 2},
+ {30, "latin5", "latin5_turkish_ci", 1, 1},
+ {32, "armscii8", "armscii8_general_ci", 1, 1},
+ {33, "utf8", "utf8_general_ci", 1, 3},
+ {35, "ucs2", "ucs2_general_ci", 2, 2},
+ {36, "cp866", "cp866_general_ci", 1, 1},
+ {37, "keybcs2", "keybcs2_general_ci", 1, 1},
+ {38, "macce", "macce_general_ci", 1, 1},
+ {39, "macroman", "macroman_general_ci", 1, 1},
+ {40, "cp852", "cp852_general_ci", 1, 1},
+ {41, "latin7", "latin7_general_ci", 1, 1},
+ {51, "cp1251", "cp1251_general_ci", 1, 1},
+ {57, "cp1256", "cp1256_general_ci", 1, 1},
+ {59, "cp1257", "cp1257_general_ci", 1, 1},
+ {63, "binary", "binary", 1, 1},
+ {92, "geostd8", "geostd8_general_ci", 1, 1},
+ {95, "cp932", "cp932_japanese_ci", 1, 2},
+ {97, "eucjpms", "eucjpms_japanese_ci", 1, 3},
+ {2, "latin2", "latin2_czech_cs", 1, 1},
+ {5, "latin1", "latin1_german_ci", 1, 1},
+ {14, "cp1251", "cp1251_bulgarian_ci", 1, 1},
+ {15, "latin1", "latin1_danish_ci", 1, 1},
+ {17, "filename", "filename", 1, 5},
+ {20, "latin7", "latin7_estonian_cs", 1, 1},
+ {21, "latin2", "latin2_hungarian_ci", 1, 1},
+ {23, "cp1251", "cp1251_ukrainian_ci", 1, 1},
+ {27, "latin2", "latin2_croatian_ci", 1, 1},
+ {29, "cp1257", "cp1257_lithunian_ci", 1, 1},
+ {31, "latin1", "latin1_german2_ci", 1, 1},
+ {34, "cp1250", "cp1250_czech_cs", 1, 1},
+ {42, "latin7", "latin7_general_cs", 1, 1},
+ {43, "macce", "macce_bin", 1, 1},
+ {44, "cp1250", "cp1250_croatian_ci", 1, 1},
+ {45, "utf8", "utf8_general_ci", 1, 1},
+ {46, "utf8", "utf8_bin", 1, 1},
+ {47, "latin1", "latin1_bin", 1, 1},
+ {48, "latin1", "latin1_general_ci", 1, 1},
+ {49, "latin1", "latin1_general_cs", 1, 1},
+ {50, "cp1251", "cp1251_bin", 1, 1},
+ {52, "cp1251", "cp1251_general_cs", 1, 1},
+ {53, "macroman", "macroman_bin", 1, 1},
+ {58, "cp1257", "cp1257_bin", 1, 1},
+ {60, "armascii8", "armascii8_bin", 1, 1},
+ {65, "ascii", "ascii_bin", 1, 1},
+ {66, "cp1250", "cp1250_bin", 1, 1},
+ {67, "cp1256", "cp1256_bin", 1, 1},
+ {68, "cp866", "cp866_bin", 1, 1},
+ {69, "dec8", "dec8_bin", 1, 1},
+ {70, "greek", "greek_bin", 1, 1},
+ {71, "hebew", "hebrew_bin", 1, 1},
+ {72, "hp8", "hp8_bin", 1, 1},
+ {73, "keybcs2", "keybcs2_bin", 1, 1},
+ {74, "koi8r", "koi8r_bin", 1, 1},
+ {75, "koi8u", "koi8u_bin", 1, 1},
+ {77, "latin2", "latin2_bin", 1, 1},
+ {78, "latin5", "latin5_bin", 1, 1},
+ {79, "latin7", "latin7_bin", 1, 1},
+ {80, "cp850", "cp850_bin", 1, 1},
+ {81, "cp852", "cp852_bin", 1, 1},
+ {82, "swe7", "swe7_bin", 1, 1},
+ {93, "geostd8", "geostd8_bin", 1, 1},
+ {83, "utf8", "utf8_bin", 1, 3},
+ {84, "big5", "big5_bin", 1, 2},
+ {85, "euckr", "euckr_bin", 1, 2},
+ {86, "gb2312", "gb2312_bin", 1, 2},
+ {87, "gbk", "gbk_bin", 1, 2},
+ {88, "sjis", "sjis_bin", 1, 2},
+ {89, "tis620", "tis620_bin", 1, 1},
+ {90, "ucs2", "ucs2_bin", 2, 2},
+ {91, "ujis", "ujis_bin", 1, 3},
+ {94, "latin1", "latin1_spanish_ci", 1, 1},
+ {96, "cp932", "cp932_bin", 1, 2},
+ {99, "cp1250", "cp1250_polish_ci", 1, 1},
+ {98, "eucjpms", "eucjpms_bin", 1, 3},
+ {128, "ucs2", "ucs2_unicode_ci", 2, 2},
+ {129, "ucs2", "ucs2_icelandic_ci", 2, 2},
+ {130, "ucs2", "ucs2_latvian_ci", 2, 2},
+ {131, "ucs2", "ucs2_romanian_ci", 2, 2},
+ {132, "ucs2", "ucs2_slovenian_ci", 2, 2},
+ {133, "ucs2", "ucs2_polish_ci", 2, 2},
+ {134, "ucs2", "ucs2_estonian_ci", 2, 2},
+ {135, "ucs2", "ucs2_spanish_ci", 2, 2},
+ {136, "ucs2", "ucs2_swedish_ci", 2, 2},
+ {137, "ucs2", "ucs2_turkish_ci", 2, 2},
+ {138, "ucs2", "ucs2_czech_ci", 2, 2},
+ {139, "ucs2", "ucs2_danish_ci", 2, 2},
+ {140, "ucs2", "ucs2_lithunian_ci", 2, 2},
+ {141, "ucs2", "ucs2_slovak_ci", 2, 2},
+ {142, "ucs2", "ucs2_spanish2_ci", 2, 2},
+ {143, "ucs2", "ucs2_roman_ci", 2, 2},
+ {144, "ucs2", "ucs2_persian_ci", 2, 2},
+ {145, "ucs2", "ucs2_esperanto_ci", 2, 2},
+ {146, "ucs2", "ucs2_hungarian_ci", 2, 2},
+ {147, "ucs2", "ucs2_sinhala_ci", 2, 2},
+ {192, "utf8mb3", "utf8mb3_general_ci", 1, 3},
+ {193, "utf8mb3", "utf8mb3_icelandic_ci", 1, 3},
+ {194, "utf8mb3", "utf8mb3_latvian_ci", 1, 3},
+ {195, "utf8mb3", "utf8mb3_romanian_ci", 1, 3},
+ {196, "utf8mb3", "utf8mb3_slovenian_ci", 1, 3},
+ {197, "utf8mb3", "utf8mb3_polish_ci", 1, 3},
+ {198, "utf8mb3", "utf8mb3_estonian_ci", 1, 3},
+ {119, "utf8mb3", "utf8mb3_spanish_ci", 1, 3},
+ {200, "utf8mb3", "utf8mb3_swedish_ci", 1, 3},
+ {201, "utf8mb3", "utf8mb3_turkish_ci", 1, 3},
+ {202, "utf8mb3", "utf8mb3_czech_ci", 1, 3},
+ {203, "utf8mb3", "utf8mb3_danish_ci", 1, 3},
+ {204, "utf8mb3", "utf8mb3_lithunian_ci", 1, 3},
+ {205, "utf8mb3", "utf8mb3_slovak_ci", 1, 3},
+ {206, "utf8mb3", "utf8mb3_spanish2_ci", 1, 3},
+ {207, "utf8mb3", "utf8mb3_roman_ci", 1, 3},
+ {208, "utf8mb3", "utf8mb3_persian_ci", 1, 3},
+ {209, "utf8mb3", "utf8mb3_esperanto_ci", 1, 3},
+ {210, "utf8mb3", "utf8mb3_hungarian_ci", 1, 3},
+ {211, "utf8mb3", "utf8mb3_sinhala_ci", 1, 3},
+ {224, "utf8", "utf8_unicode_ci", 1, 3},
+ {225, "utf8", "utf8_icelandic_ci", 1, 3},
+ {226, "utf8", "utf8_latvian_ci", 1, 3},
+ {227, "utf8", "utf8_romanian_ci", 1, 3},
+ {228, "utf8", "utf8_slovenian_ci", 1, 3},
+ {229, "utf8", "utf8_polish_ci", 1, 3},
+ {230, "utf8", "utf8_estonian_ci", 1, 3},
+ {231, "utf8", "utf8_spanish_ci", 1, 3},
+ {232, "utf8", "utf8_swedish_ci", 1, 3},
+ {233, "utf8", "utf8_turkish_ci", 1, 3},
+ {234, "utf8", "utf8_czech_ci", 1, 3},
+ {235, "utf8", "utf8_danish_ci", 1, 3},
+ {236, "utf8", "utf8_lithuanian_ci", 1, 3},
+ {237, "utf8", "utf8_slovak_ci", 1, 3},
+ {238, "utf8", "utf8_spanish2_ci", 1, 3},
+ {239, "utf8", "utf8_roman_ci", 1, 3},
+ {240, "utf8", "utf8_persian_ci", 1, 3},
+ {241, "utf8", "utf8_esperanto_ci", 1, 3},
+ {242, "utf8", "utf8_hungarian_ci", 1, 3},
+ {243, "utf8", "utf8_sinhala_ci", 1, 3},
+ {254, "utf8mb3", "utf8mb3_general_cs", 1, 3},
+ {0, NULL, NULL, 0, 0}
+};
+
+#pragma mark -
+
+@implementation SPMySQLResult (Field_Definitions)
+
+/**
+ * Return an array of NSDictionaries, each containing information about one of
+ * the columns in the result set.
+ * MySQL returns non-valid details as empty strings - these are converted to
+ * unset entries in the dictionary.
+ */
+- (NSArray *)fieldDefinitions
+{
+ NSUInteger i;
+ NSMutableArray *theFieldDefinitions = [NSMutableArray array];
+ NSMutableDictionary *eachField;
+ MYSQL_FIELD mysqlField;
+
+ for (i = 0; i < numberOfFields; i++) {
+ eachField = [NSMutableDictionary dictionary];
+ mysqlField = fieldDefinitions[i];
+
+ // Record the original column position within the result set
+ [eachField setObject:[NSString stringWithFormat:@"%llu", (unsigned long long)i] forKey:@"datacolumnindex"];
+
+ // Record the column name, or alias if one is being used
+ [eachField setObject:[self _stringWithBytes:mysqlField.name length:mysqlField.name_length] forKey:@"name"];
+
+ // Record the original column name if using an alias
+ [eachField setObject:[self _stringWithBytes:mysqlField.org_name length:mysqlField.org_name_length] forKey:@"org_name"];
+
+ // If the column had an underlying table, record the table name, respecting aliases
+ if (mysqlField.table_length) {
+ [eachField setObject:[self _stringWithBytes:mysqlField.table length:mysqlField.table_length] forKey:@"table"];
+ }
+
+ // If the column had an underlying table, record the original table name, ignoring aliases
+ if (mysqlField.org_table_length) {
+ [eachField setObject:[self _stringWithBytes:mysqlField.org_table length:mysqlField.org_table_length] forKey:@"org_table"];
+ }
+
+ // If the column had an underlying database, record the database name
+ if (mysqlField.db_length) {
+ [eachField setObject:[self _stringWithBytes:mysqlField.db length:mysqlField.db_length] forKey:@"db"];
+ }
+
+ // Width of column (minimum real length in bytes)
+ [eachField setObject:[NSNumber numberWithUnsignedLongLong:mysqlField.length] forKey:@"byte_length"];
+
+ // Width of column (as in create)
+ // TODO: Discuss the logic of this with Hans-Jörg Bibiko; is this related to max_byte_length?
+ [eachField setObject:[NSNumber numberWithUnsignedLongLong:(mysqlField.length/[self _findCharsetMaxByteLengthPerCharForMySQLNumber:mysqlField.charsetnr])] forKey:@"char_length"];
+
+ // Max width (bytes) for selected set. Note that this will be 0 for streaming results.
+ [eachField setObject:[NSNumber numberWithUnsignedLongLong:mysqlField.max_length] forKey:@"max_byte_length"];
+
+ // Bit-flags that describe the field, in entirety and split out
+ [eachField setObject:[NSNumber numberWithUnsignedInt:mysqlField.flags] forKey:@"flags"];
+ [eachField setObject:[NSNumber numberWithBool:(mysqlField.flags & NOT_NULL_FLAG) ? YES : NO] forKey:@"null"];
+ [eachField setObject:[NSNumber numberWithBool:(mysqlField.flags & PRI_KEY_FLAG) ? YES : NO] forKey:@"PRI_KEY_FLAG"];
+ [eachField setObject:[NSNumber numberWithBool:(mysqlField.flags & UNIQUE_KEY_FLAG) ? YES : NO] forKey:@"UNIQUE_KEY_FLAG"];
+ [eachField setObject:[NSNumber numberWithBool:(mysqlField.flags & MULTIPLE_KEY_FLAG) ? YES : NO] forKey:@"MULTIPLE_KEY_FLAG"];
+ [eachField setObject:[NSNumber numberWithBool:(mysqlField.flags & BLOB_FLAG) ? YES : NO] forKey:@"BLOB_FLAG"];
+ [eachField setObject:[NSNumber numberWithBool:(mysqlField.flags & UNSIGNED_FLAG) ? YES : NO] forKey:@"UNSIGNED_FLAG"];
+ [eachField setObject:[NSNumber numberWithBool:(mysqlField.flags & ZEROFILL_FLAG) ? YES : NO] forKey:@"ZEROFILL_FLAG"];
+ [eachField setObject:[NSNumber numberWithBool:(mysqlField.flags & BINARY_FLAG) ? YES : NO] forKey:@"BINARY_FLAG"];
+ [eachField setObject:[NSNumber numberWithBool:(mysqlField.flags & ENUM_FLAG) ? YES : NO] forKey:@"ENUM_FLAG"];
+ [eachField setObject:[NSNumber numberWithBool:(mysqlField.flags & AUTO_INCREMENT_FLAG) ? YES : NO] forKey:@"AUTO_INCREMENT_FLAG"];
+ [eachField setObject:[NSNumber numberWithBool:(mysqlField.flags & SET_FLAG) ? YES : NO] forKey:@"SET_FLAG"];
+ [eachField setObject:[NSNumber numberWithBool:(mysqlField.flags & NUM_FLAG) ? YES : NO] forKey:@"NUM_FLAG"];
+ [eachField setObject:[NSNumber numberWithBool:(mysqlField.flags & PART_KEY_FLAG) ? YES : NO] forKey:@"PART_KEY_FLAG"];
+
+ // For numeric fields, record the number of decimals
+ [eachField setObject:[NSNumber numberWithUnsignedInteger:mysqlField.decimals] forKey:@"decimals"];
+
+ // Character set details
+ [eachField setObject:[NSNumber numberWithUnsignedInteger:mysqlField.charsetnr] forKey:@"charsetnr"];
+ [eachField setObject:[self _charsetNameForMySQLNumber:mysqlField.charsetnr] forKey:@"charset_name"];
+ [eachField setObject:[self _charsetCollationForMySQLNumber:mysqlField.charsetnr] forKey:@"charset_collation"];
+
+ /* Table type */
+ [eachField setObject:[self _mysqlTypeToStringForType:mysqlField.type
+ withCharsetNr:mysqlField.charsetnr
+ withFlags:mysqlField.flags
+ withLength:mysqlField.length
+ ] forKey:@"type"];
+
+ /* Table type group*/
+ [eachField setObject:[self _mysqlTypeToGroupForType:mysqlField.type
+ withCharsetNr:mysqlField.charsetnr
+ withFlags:mysqlField.flags
+ ] forKey:@"typegrouping"];
+
+ [theFieldDefinitions addObject:eachField];
+ }
+
+ return theFieldDefinitions;
+}
+
+@end
+
+#pragma mark -
+#pragma mark Field defintion internals
+
+@implementation SPMySQLResult (Field_Definitions_Private_API)
+
+/**
+ * Return the maximum byte length to store a char by using
+ * a specific mysql_charsetnr
+ */
+- (NSUInteger)_findCharsetMaxByteLengthPerCharForMySQLNumber:(NSUInteger)charsetnr
+{
+ const SPMySQLResultCharset *c = SPMySQLCharsetMap;
+
+ do {
+ if (c->nr == charsetnr)
+ return c->char_maxlen;
+ ++c;
+ } while (c[0].nr != 0);
+
+ return 1;
+}
+
+/**
+ * Convert a mysql_charsetnr into a charset name as string
+ */
+- (NSString *)_charsetNameForMySQLNumber:(NSUInteger)charsetnr
+{
+ const SPMySQLResultCharset *c = SPMySQLCharsetMap;
+
+ do {
+ if (c->nr == charsetnr)
+ return [NSString stringWithCString:c->name encoding:stringEncoding];
+ ++c;
+ } while (c[0].nr != 0);
+
+ return @"UNKNOWN";
+}
+
+/**
+ * Convert a mysql_charsetnr into a collation name as string
+ */
+- (NSString *)_charsetCollationForMySQLNumber:(NSUInteger)charsetnr
+{
+ const SPMySQLResultCharset *c = SPMySQLCharsetMap;
+
+ do {
+ if (c->nr == charsetnr)
+ return [NSString stringWithCString:c->collation encoding:stringEncoding];
+ ++c;
+ } while (c[0].nr != 0);
+
+ return @"UNKNOWN";
+}
+
+/**
+ * Convert a mysql_type to a string
+ */
+- (NSString *)_mysqlTypeToStringForType:(NSUInteger)type withCharsetNr:(NSUInteger)charsetnr withFlags:(NSUInteger)flags withLength:(unsigned long long)length
+{
+
+ switch (type) {
+
+ case FIELD_TYPE_BIT:
+ return @"BIT";
+
+ case MYSQL_TYPE_DECIMAL:
+ case MYSQL_TYPE_NEWDECIMAL:
+ return @"DECIMAL";
+
+ case MYSQL_TYPE_TINY:
+ return @"TINYINT";
+
+ case MYSQL_TYPE_SHORT:
+ return @"SMALLINT";
+
+ case MYSQL_TYPE_LONG:
+ return @"INT";
+
+ case MYSQL_TYPE_FLOAT:
+ return @"FLOAT";
+
+ case MYSQL_TYPE_DOUBLE:
+ return @"DOUBLE";
+
+ case MYSQL_TYPE_NULL:
+ return @"NULL";
+
+ case MYSQL_TYPE_TIMESTAMP:
+ return @"TIMESTAMP";
+
+ case MYSQL_TYPE_LONGLONG:
+ return @"BIGINT";
+
+ case MYSQL_TYPE_INT24:
+ return @"MEDIUMINT";
+
+ case MYSQL_TYPE_DATE:
+ return @"DATE";
+
+ case MYSQL_TYPE_TIME:
+ return @"TIME";
+
+ case MYSQL_TYPE_DATETIME:
+ return @"DATETIME";
+
+ case MYSQL_TYPE_TINY_BLOB:// should no appear over the wire
+ case MYSQL_TYPE_MEDIUM_BLOB:// should no appear over the wire
+ case MYSQL_TYPE_LONG_BLOB:// should no appear over the wire
+ case MYSQL_TYPE_BLOB:
+ {
+ BOOL isBlob = (charsetnr == MAGIC_BINARY_CHARSET_NR);
+ switch (length/[self _findCharsetMaxByteLengthPerCharForMySQLNumber:charsetnr]) {
+ case 255: return isBlob? @"TINYBLOB":@"TINYTEXT";
+ case 65535: return isBlob? @"BLOB":@"TEXT";
+ case 16777215: return isBlob? @"MEDIUMBLOB":@"MEDIUMTEXT";
+ case 4294967295: return isBlob? @"LONGBLOB":@"LONGTEXT";
+ default:
+ switch (length) {
+ case 255: return isBlob? @"TINYBLOB":@"TINYTEXT";
+ case 65535: return isBlob? @"BLOB":@"TEXT";
+ case 16777215: return isBlob? @"MEDIUMBLOB":@"MEDIUMTEXT";
+ case 4294967295: return isBlob? @"LONGBLOB":@"LONGTEXT";
+ default:
+ return @"UNKNOWN";
+ }
+ }
+ }
+
+ case MYSQL_TYPE_VAR_STRING:
+ if (flags & ENUM_FLAG) {
+ return @"ENUM";
+ }
+ if (flags & SET_FLAG) {
+ return @"SET";
+ }
+ if (charsetnr == MAGIC_BINARY_CHARSET_NR) {
+ return @"VARBINARY";
+ }
+ return @"VARCHAR";
+
+ case MYSQL_TYPE_STRING:
+ if (flags & ENUM_FLAG) {
+ return @"ENUM";
+ }
+ if (flags & SET_FLAG) {
+ return @"SET";
+ }
+ if ((flags & BINARY_FLAG) && charsetnr == MAGIC_BINARY_CHARSET_NR) {
+ return @"BINARY";
+ }
+ return @"CHAR";
+
+ case MYSQL_TYPE_ENUM:
+ /* This should never happen */
+ return @"ENUM";
+
+ case MYSQL_TYPE_YEAR:
+ return @"YEAR";
+
+ case MYSQL_TYPE_SET:
+ /* This should never happen */
+ return @"SET";
+
+ case MYSQL_TYPE_GEOMETRY:
+ return @"GEOMETRY";
+
+ default:
+ return @"UNKNOWN";
+ }
+}
+
+/**
+ * Merge mysql_types into type groups
+ */
+- (NSString *)_mysqlTypeToGroupForType:(NSUInteger)type withCharsetNr:(NSUInteger)charsetnr withFlags:(NSUInteger)flags
+{
+ switch(type){
+
+ case FIELD_TYPE_BIT:
+ return @"bit";
+
+ case MYSQL_TYPE_TINY:
+ case MYSQL_TYPE_SHORT:
+ case MYSQL_TYPE_LONG:
+ case MYSQL_TYPE_LONGLONG:
+ case MYSQL_TYPE_INT24:
+ return @"integer";
+
+ case MYSQL_TYPE_FLOAT:
+ case MYSQL_TYPE_DOUBLE:
+ case MYSQL_TYPE_DECIMAL:
+ case MYSQL_TYPE_NEWDECIMAL:
+ return @"float";
+
+ case MYSQL_TYPE_YEAR:
+ case MYSQL_TYPE_DATETIME:
+ case MYSQL_TYPE_TIME:
+ case MYSQL_TYPE_DATE:
+ case MYSQL_TYPE_TIMESTAMP:
+ return @"date";
+
+ case MYSQL_TYPE_VAR_STRING:
+ if (flags & ENUM_FLAG) {
+ return @"enum";
+ }
+ if (flags & SET_FLAG) {
+ return @"enum";
+ }
+ if (charsetnr == MAGIC_BINARY_CHARSET_NR) {
+ return @"binary";
+ }
+ return @"string";
+
+ case MYSQL_TYPE_STRING:
+ if (flags & ENUM_FLAG) {
+ return @"enum";
+ }
+ if (flags & SET_FLAG) {
+ return @"enum";
+ }
+ if ((flags & BINARY_FLAG) && charsetnr == MAGIC_BINARY_CHARSET_NR) {
+ return @"binary";
+ }
+ return @"string";
+
+ case MYSQL_TYPE_TINY_BLOB: // should no appear over the wire
+ case MYSQL_TYPE_MEDIUM_BLOB: // should no appear over the wire
+ case MYSQL_TYPE_LONG_BLOB: // should no appear over the wire
+ case MYSQL_TYPE_BLOB:
+ {
+ if (charsetnr == MAGIC_BINARY_CHARSET_NR) {
+ return @"blobdata";
+ } else {
+ return @"textdata";
+ }
+ }
+
+ case MYSQL_TYPE_GEOMETRY:
+ return @"geometry";
+
+ default:
+ return @"blobdata";
+ }
+}
+
+@end \ No newline at end of file
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLResult.h b/Frameworks/SPMySQLFramework/Source/SPMySQLResult.h
new file mode 100644
index 00000000..29518b5d
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLResult.h
@@ -0,0 +1,121 @@
+//
+// $Id$
+//
+// SPMySQLResult.h
+// 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/>
+
+
+typedef enum {
+ SPMySQLResultFieldAsUnhandled = 0,
+ SPMySQLResultFieldAsString = 1,
+ SPMySQLResultFieldAsStringOrBlob = 2,
+ SPMySQLResultFieldAsBlob = 3,
+ SPMySQLResultFieldAsBit = 4,
+ SPMySQLResultFieldAsGeometry = 5,
+ SPMySQLResultFieldAsNull = 6
+} SPMySQLResultFieldProcessor;
+
+@interface SPMySQLResult : NSObject <NSFastEnumeration> {
+
+ // Wrapped MySQL result set and its encoding
+ struct st_mysql_res *resultSet;
+ NSStringEncoding stringEncoding;
+
+ // Number of fields in the result set, and the field names and information
+ NSUInteger numberOfFields;
+ struct st_mysql_field *fieldDefinitions;
+ unsigned int *fieldTypes;
+ NSString **fieldNames;
+
+ // Number of rows in the result set and an internal data position counter
+ unsigned long long numberOfRows;
+ unsigned long long currentRowIndex;
+
+ // How long it took to execute the query that produced this result
+ double queryExecutionTime;
+
+ // The target result set type for fast enumeration and unspecified row retrieval
+ SPMySQLResultRowType defaultRowReturnType;
+
+ // Whether all data should be returned as strings - useful for working with some older server types
+ BOOL returnDataAsStrings;
+}
+
+// Master init method
+- (id)initWithMySQLResult:(void *)theResult stringEncoding:(NSStringEncoding)theStringEncoding;
+
+// Result set information
+- (NSUInteger)numberOfFields;
+- (unsigned long long)numberOfRows;
+- (double)queryExecutionTime;
+
+// Column information
+- (NSArray *)fieldNames;
+
+// Data retrieval (note that fast enumeration is also supported, using instance-default format)
+- (void)seekToRow:(unsigned long long)targetRow;
+- (id)getRow;
+- (NSArray *)getRowAsArray;
+- (NSDictionary *)getRowAsDictionary;
+- (id)getRowAsType:(SPMySQLResultRowType)theType;
+
+// Data conversion
++ (NSString *)bitStringWithBytes:(const char *)bytes length:(NSUInteger)length padToLength:(NSUInteger)padLength;
+
+#pragma mark -
+#pragma mark Synthesized properties
+
+/**
+ * Set whether the result should return data types as strings. This may be useful
+ * for queries where the result may be returned in either string or data form, but
+ * will be converted to string for display and use anyway.
+ * Note that certain MySQL versions also return data types for strings - eg SHOW
+ * commands like SHOW CREATE TABLE or SHOW VARIABLES, and this conversion can be
+ * necessary there.
+ */
+@property (readwrite, assign) BOOL returnDataAsStrings;
+
+@property (readwrite, assign) SPMySQLResultRowType defaultRowReturnType;
+
+@end
+
+/**
+ * Set up a static function to allow fast calling with cached selectors
+ */
+static inline id SPMySQLResultGetRow(SPMySQLResult* self, SPMySQLResultRowType rowType)
+{
+ typedef id (*SPMySQLResultGetRowMethodPtr)(SPMySQLResult*, SEL, SPMySQLResultRowType);
+ static SPMySQLResultGetRowMethodPtr cachedMethodPointer;
+ static SEL cachedSelector;
+
+ if (!cachedSelector) cachedSelector = @selector(getRowAsType:);
+ if (!cachedMethodPointer) cachedMethodPointer = (SPMySQLResultGetRowMethodPtr)[self methodForSelector:cachedSelector];
+
+ return cachedMethodPointer(self, cachedSelector, rowType);
+}
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
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResult.h b/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResult.h
new file mode 100644
index 00000000..5abb85db
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResult.h
@@ -0,0 +1,54 @@
+//
+// $Id$
+//
+// SPMySQLStreamingResult.h
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on February 18, 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/>
+
+@interface SPMySQLStreamingResult : SPMySQLResult {
+
+ // Keep a link to the parent connection for locking purposes
+ SPMySQLConnection *parentConnection;
+
+ // Streaming result information and tracking
+ BOOL connectionUnlocked;
+ BOOL dataDownloaded;
+
+ // Counts and memory length tracking
+ NSUInteger downloadedRowCount;
+
+ IMP isConnectedPtr;
+ SEL isConnectedSelector;
+}
+
+- (id)initWithMySQLResult:(void *)theResult stringEncoding:(NSStringEncoding)theStringEncoding connection:(SPMySQLConnection *)theConnection;
+
+// Allow result fetching to be cancelled
+- (void)cancelResultLoad;
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResult.m b/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResult.m
new file mode 100644
index 00000000..b19e5356
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResult.m
@@ -0,0 +1,246 @@
+//
+// $Id$
+//
+// SPMySQLStreamingResult.m
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on February 18, 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 "SPMySQLStreamingResult.h"
+#import "SPMySQL Private APIs.h"
+
+
+/**
+ * This type of streaming result allows each row to be accessed on-demand; this can
+ * be dangerous as it means a SELECT will tie up the server for longer, as for MyISAM
+ * tables updates (and subsequent reads) must block while a SELECT is still running.
+ * However this can be useful for certain processes such as working with very large
+ * tables to keep memory usage low.
+ */
+
+@implementation SPMySQLStreamingResult
+
+#pragma mark -
+
+/**
+ * Prevent SPMySQLStreamingResults from being init'd as SPMySQLResults.
+ */
+- (id)initWithMySQLResult:(void *)theResult stringEncoding:(NSStringEncoding)theStringEncoding
+{
+ [NSException raise:NSInternalInconsistencyException format:@"SPMySQLFullStreamingResults should not be init'd as SPMySQLResults; use initWithMySQLResult:stringEncoding:connection:withFullStreaming: instead."];
+ return nil;
+}
+
+/**
+ * Standard init method, constructing the SPMySQLStreamingResult around a MySQL
+ * result pointer and the encoding to use when working with the data.
+ * As opposed to SPMySQLResult, defaults to returning rows as arrays, as the result
+ * sets are likely to be larger and processed in loops.
+ */
+- (id)initWithMySQLResult:(void *)theResult stringEncoding:(NSStringEncoding)theStringEncoding connection:(SPMySQLConnection *)theConnection
+{
+
+ // If no result set was passed in, return nil.
+ if (!theResult) return nil;
+
+ if ((self = [super initWithMySQLResult:theResult stringEncoding:theStringEncoding])) {
+ parentConnection = theConnection;
+ numberOfRows = NSNotFound;
+
+ // Start with no rows downloaded
+ downloadedRowCount = 0;
+ dataDownloaded = NO;
+ connectionUnlocked = NO;
+
+ // Cache the isConnected selector and pointer for fast connection checks
+ isConnectedSelector = @selector(isConnected);
+ isConnectedPtr = [parentConnection methodForSelector:isConnectedSelector];
+
+ // Default to returning rows as arrays
+ defaultRowReturnType = SPMySQLResultRowAsArray;
+ }
+
+ return self;
+}
+
+/**
+ * Deallocate the result and ensure the parent connection is unlocked for further use.
+ */
+- (void)dealloc
+{
+
+ // Ensure all data is processed and the parent connection is unlocked
+ [self cancelResultLoad];
+
+ // Throw an exception if in invalid state
+ if (!connectionUnlocked) {
+ [parentConnection _unlockConnection];
+ [NSException raise:NSInternalInconsistencyException format:@"Parent connection remains locked after SPMySQLStreamingResult use"];
+ }
+
+ [super dealloc];
+}
+
+#pragma mark -
+#pragma mark Result set information
+
+/**
+ * Override the return of the number of rows in the data set. If this is used before the
+ * data is fully downloaded, the number of results is still unknown (the server may still
+ * be seeking/matching), so return NSNotFound; otherwise the number of rows is returned.
+ */
+- (unsigned long long)numberOfRows
+{
+ if (!dataDownloaded) return NSNotFound;
+
+ return downloadedRowCount;
+}
+
+#pragma mark -
+#pragma mark Data retrieval
+
+/**
+ * Override seeking behaviour: seeking cannot be used in streaming result sets.
+ */
+- (void)seekToRow:(unsigned long long)targetRow
+{
+ [NSException raise:NSInternalInconsistencyException format:@"Seeking is not supported in streaming SPMySQL result sets."];
+}
+
+/**
+ * Override the convenience selectors so that forwarding works correctly.
+ */
+- (id)getRow
+{
+ return SPMySQLResultGetRow(self, SPMySQLResultRowAsDefault);
+}
+- (NSArray *)getRowAsArray
+{
+ return SPMySQLResultGetRow(self, SPMySQLResultRowAsArray);
+}
+- (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
+{
+ id theRow = nil;
+
+ // Ensure that the connection is still up before performing a row fetch
+ if ((*isConnectedPtr)(parentConnection, isConnectedSelector)) {
+
+ // The core of result fetching in streaming mode is still based around mysql_fetch_row,
+ // so use the super to perform normal processing.
+ theRow = [super getRowAsType:theType];
+ }
+
+ // If no row was returned, the end of the result set has been reached. Clear markers,
+ // unlock the parent connection, and return nil.
+ if (!theRow) {
+ dataDownloaded = YES;
+ [parentConnection _unlockConnection];
+ connectionUnlocked = YES;
+ return nil;
+ }
+
+ // Otherwise increment the data downloaded counter and return the row
+ downloadedRowCount++;
+
+ return theRow;
+}
+
+/*
+ * Ensure the result set is fully processed and freed without any processing
+ * This method ensures that the connection is unlocked.
+ */
+- (void)cancelResultLoad
+{
+
+ // If data has already been downloaded successfully, no further action is required
+ if (dataDownloaded) return;
+
+ MYSQL_ROW theRow;
+
+ // Loop through all the rows and ensure the rows are fetched.
+ while (1) {
+ theRow = mysql_fetch_row(resultSet);
+
+ // If no data was returned, we're at the end of the result set - return.
+ if (theRow == NULL) {
+ dataDownloaded = YES;
+ if (!connectionUnlocked) {
+ [parentConnection _unlockConnection];
+ connectionUnlocked = YES;
+ }
+ return;
+ }
+
+ downloadedRowCount++;
+ }
+}
+
+#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. Full streaming mode - return one row at a time.
+ */
+- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len
+{
+
+ // If all rows have been retrieved, return 0 to stop iteration.
+ if (dataDownloaded) return 0;
+
+ // If the MySQL row pointer does not match the requested state, throw an exception
+ if (state->state != currentRowIndex) {
+ [NSException raise:NSRangeException format:@"SPMySQLStreamingResult results can only be accessed linearly"];
+ }
+
+ // In full streaming mode return one row at a time. Retrieve the row.
+ id theRow = SPMySQLResultGetRow(self, SPMySQLResultRowAsDefault);
+
+ // If nil was returned the end of the result resource has been reached
+ if (!theRow) return 0;
+
+ // Add the row to the result stack and update state
+ stackbuf[0] = theRow;
+ state->state += 1;
+ state->itemsPtr = stackbuf;
+ state->mutationsPtr = (unsigned long *)self;
+
+ return 1;
+}
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLStringAdditions.h b/Frameworks/SPMySQLFramework/Source/SPMySQLStringAdditions.h
new file mode 100644
index 00000000..b1f9ccd0
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLStringAdditions.h
@@ -0,0 +1,39 @@
+//
+// $Id$
+//
+// SPMySQLStringAdditions.h
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on February 8, 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/>
+
+
+@interface NSString (SPMySQLStringAdditions)
+
+- (NSString *)mySQLBacktickQuotedString;
+- (NSString *)mySQLTickQuotedString;
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLStringAdditions.m b/Frameworks/SPMySQLFramework/Source/SPMySQLStringAdditions.m
new file mode 100644
index 00000000..1ea966de
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLStringAdditions.m
@@ -0,0 +1,57 @@
+//
+// $Id$
+//
+// SPMySQLStringAdditions.h
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on February 8, 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 "SPMySQLStringAdditions.h"
+
+@implementation NSString (SPMySQLStringAdditions)
+
+/**
+ * Returns the string quoted with backticks as required for MySQL identifiers
+ * eg.: tablename => `tablename`
+ * my`table => `my``table`
+ */
+- (NSString *)mySQLBacktickQuotedString
+{
+ return [NSString stringWithFormat: @"`%@`", [self stringByReplacingOccurrencesOfString:@"`" withString:@"``"]];
+}
+
+/**
+ * Returns the string quoted with ticks as required for MySQL identifiers
+ * eg.: tablename => 'tablename'
+ * my'table => 'my''table'
+ */
+- (NSString *)mySQLTickQuotedString
+{
+ return [NSString stringWithFormat: @"'%@'", [self stringByReplacingOccurrencesOfString:@"'" withString:@"''"]];
+}
+
+@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLUtilities.h b/Frameworks/SPMySQLFramework/Source/SPMySQLUtilities.h
new file mode 100644
index 00000000..0a8c19b0
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLUtilities.h
@@ -0,0 +1,45 @@
+//
+// $Id$
+//
+// Locking.h
+// SPMySQLFramework
+//
+// Created by Rowan Beentje (rowan.beent.je) on February 6, 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/>
+
+#include <mach/mach_time.h>
+
+/**
+ * Define a project function to make it easier to use mach_absolute_time()
+ * to track monotonically increasing time.
+ */
+static double _elapsedSecondsSinceAbsoluteTime(uint64_t comparisonTime)
+{
+ uint64_t elapsedTime_t = mach_absolute_time() - comparisonTime;
+ Nanoseconds elapsedTime = AbsoluteToNanoseconds(*(AbsoluteTime *)&(elapsedTime_t));
+
+ return (((double)UnsignedWideToUInt64(elapsedTime)) * 1e-9);
+} \ No newline at end of file