aboutsummaryrefslogtreecommitdiffstats
path: root/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories
diff options
context:
space:
mode:
Diffstat (limited to 'Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories')
-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/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.h58
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Ping & KeepAlive.m227
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.h103
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.m622
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Server Info.h50
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Server Info.m175
18 files changed, 2716 insertions, 0 deletions
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/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..665e7697
--- /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 CFStringConvertEncodingToNSStringEncoding(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..3788c653
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Ping & KeepAlive.h
@@ -0,0 +1,58 @@
+//
+// $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)
+
+// Setup functions
+- (void)_initKeepAlivePingTimer;
+
+// Keepalive ping initialisation
+- (void)_keepAlive:(NSTimer *)theTimer;
+- (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..9e25edcb
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Ping & KeepAlive.m
@@ -0,0 +1,227 @@
+//
+// $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 Setup functions
+
+/**
+ * Set up the keepalive timer; this should be called on the main
+ * thread, to ensure the timer isn't descheduled when child threads
+ * terminate.
+ */
+- (void)_initKeepAlivePingTimer
+{
+ keepAliveTimer = [[NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(_keepAlive:) userInfo:nil repeats:YES] retain];
+}
+
+#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:(NSTimer *)theTimer
+{
+
+ // 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..ff55f796
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.h
@@ -0,0 +1,103 @@
+//
+// $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 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)lastQueryWasCancelled;
+- (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);
+} \ No newline at end of file
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..4134880c
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.m
@@ -0,0 +1,622 @@
+//
+// $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
+ [self _updateLastErrorMessage:theErrorMessage];
+ [self _updateLastErrorID:theErrorID];
+
+ // Store the result time on the response object
+ [theResult _setQueryExecutionTime:queryExecutionTime];
+
+ return [theResult autorelease];
+}
+
+#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;
+}
+
+/**
+ * Returns whether the last query was cancelled using cancelCurrentQuery.
+ */
+- (BOOL)lastQueryWasCancelled
+{
+ return lastQueryWasCancelled;
+}
+
+/**
+ * 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