diff options
Diffstat (limited to 'Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories')
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 |