diff options
Diffstat (limited to 'Frameworks/SPMySQLFramework/Source')
39 files changed, 7111 insertions, 0 deletions
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQL Private APIs.h b/Frameworks/SPMySQLFramework/Source/SPMySQL Private APIs.h new file mode 100644 index 00000000..5772eb72 --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQL Private APIs.h @@ -0,0 +1,110 @@ +// +// $Id$ +// +// SPMySQLConnection_PrivateAPI.h +// SPMySQLFramework +// +// Created by Rowan Beentje (rowan.beent.je) on February 12, 2012 +// Copyright (c) 2012 Rowan Beentje. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at <http://code.google.com/p/sequel-pro/> + +/** + * A collection of Private APIs from the various categories, to simplify + * inclusion across the categories. + */ + +#import "Ping & KeepAlive.h" +#import "Locking.h" +#import "Conversion.h" + + +@interface SPMySQLConnection (PrivateAPI) + +- (MYSQL *)_makeRawMySQLConnectionWithEncoding:(NSString *)encodingName isMasterConnection:(BOOL)isMaster; +- (BOOL)_waitForNetworkConnectionWithTimeout:(double)timeoutSeconds; +- (void)_updateConnectionVariables; +- (void)_restoreConnectionVariables; +- (BOOL)_checkConnectionIfNecessary; + +@end + + +@interface SPMySQLConnection (Delegate_and_Proxy_Private_API) + +- (void)_proxyStateChange:(NSObject <SPMySQLConnectionProxy> *)aProxy; +- (SPMySQLConnectionLostDecision)_delegateDecisionForLostConnection; + +@end + + +@interface SPMySQLConnection (Databases_and_Tables_Private_API) + +- (BOOL)_storeAndAlterEncodingToUTF8IfRequired; + +@end + + +@interface SPMySQLConnection (Max_Packet_Size_Private_API) + +- (void)_updateMaxQuerySize; +- (void)_updateMaxQuerySizeEditability; +- (BOOL)_attemptMaxQuerySizeIncreaseTo:(NSUInteger)targetSize; +- (void)_restoreMaximumQuerySizeAfterQuery; + +@end + + +@interface SPMySQLConnection (Querying_and_Preparation_Private_API) + +- (void)_flushMultipleResultSets; +- (void)_updateLastErrorMessage:(NSString *)theErrorMessage; +- (void)_updateLastErrorID:(NSUInteger)theErrorID; + +@end + + +// SPMySQLResult Private API +@interface SPMySQLResult (Private_API) + +- (NSString *)_stringWithBytes:(const void *)bytes length:(NSUInteger)length; +- (void)_setQueryExecutionTime:(double)theExecutionTime; +- (id)_getObjectFromBytes:(char *)bytes ofLength:(NSUInteger)length fieldType:(unsigned int)fieldType fieldDefinitionIndex:(NSUInteger)fieldIndex; + +@end + +/** + * Set up a static function to allow fast calling of SPMySQLResult data conversion with cached selectors + */ +static inline id SPMySQLResultGetObject(SPMySQLResult* self, char* bytes, NSUInteger length, unsigned int fieldType, NSUInteger fieldIndex) +{ + typedef id (*SPMySQLResultGetObjectMethodPtr)(SPMySQLResult*, SEL, char*, NSUInteger, unsigned int, NSUInteger); + static SPMySQLResultGetObjectMethodPtr cachedMethodPointer; + static SEL cachedSelector; + + if (!cachedSelector) cachedSelector = @selector(_getObjectFromBytes:ofLength:fieldType:fieldDefinitionIndex:); + if (!cachedMethodPointer) cachedMethodPointer = (SPMySQLResultGetObjectMethodPtr)[self methodForSelector:cachedSelector]; + + return cachedMethodPointer(self, cachedSelector, bytes, length, fieldType, fieldIndex); +}
\ No newline at end of file diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQL.h b/Frameworks/SPMySQLFramework/Source/SPMySQL.h new file mode 100644 index 00000000..a9150f46 --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQL.h @@ -0,0 +1,62 @@ +// +// $Id$ +// +// SPMySQL.h +// SPMySQLFramework +// +// Created by Rowan Beentje (rowan.beent.je) on January 22, 2012 +// Copyright (c) 2012 Rowan Beentje. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at <http://code.google.com/p/sequel-pro/> + +@class SPMySQLConnection, SPMySQLResult, SPMySQLStreamingResult, SPMySQLFastStreamingResult; + +// Global include file for the framework. +// Constants +#import "SPMySQLConstants.h" + +// Required category additions +#import "SPMySQLStringAdditions.h" + +// MySQL Connection Delegate and Proxy protocols +#import "SPMySQLConnectionDelegate.h" +#import "SPMySQLConnectionProxy.h" + +// MySQL Connection class and public categories +#import "SPMySQLConnection.h" +#import "Delegate & Proxy.h" +#import "Databases & Tables.h" +#import "Max Packet Size.h" +#import "Querying & Preparation.h" +#import "Encoding.h" +#import "Server Info.h" + +// MySQL result set, streaming subclasses of same, and associated categories +#import "SPMySQLResult.h" +#import "SPMySQLStreamingResult.h" +#import "SPMySQLFastStreamingResult.h" +#import "Field Definitions.h" + +// Result data objects +#import "SPMySQLGeometryData.h"
\ No newline at end of file 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 diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection.h b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection.h new file mode 100644 index 00000000..db6dc0af --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection.h @@ -0,0 +1,177 @@ +// +// $Id$ +// +// SPMySQLConnection.h +// SPMySQLFramework +// +// Created by Rowan Beentje (rowan.beent.je) on January 8, 2012 +// Copyright (c) 2012 Rowan Beentje. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at <http://code.google.com/p/sequel-pro/> + +@interface SPMySQLConnection : NSObject { + + // Delegate + NSObject <SPMySQLConnectionDelegate> *delegate; + BOOL delegateSupportsWillQueryString; + BOOL delegateSupportsConnectionLost; + BOOL delegateQueryLogging; // Defaults to YES if protocol implemented + + // Basic connection details + NSString *host; + NSString *username; + NSString *password; + NSUInteger port; + BOOL useSocket; + NSString *socketPath; + + // SSL connection details + BOOL useSSL; + NSString *sslKeyFilePath; + NSString *sslCertificatePath; + NSString *sslCACertificatePath; + + // MySQL connection details and state + struct st_mysql *mySQLConnection; + SPMySQLConnectionState state; + BOOL connectedWithSSL; + BOOL userTriggeredDisconnect; + BOOL isReconnecting; + uint64_t initialConnectTime; + unsigned long mysqlConnectionThreadId; + + // Connection proxy + NSObject <SPMySQLConnectionProxy> *proxy; + SPMySQLConnectionProxyState previousProxyState; + BOOL proxyStateChangeNotificationsIgnored; + + // Connection lock to prevent non-thread-safe query misuse + NSConditionLock *connectionLock; + + // Currently selected database + NSString *database; + + // Delegate connection lost decisions + NSUInteger reconnectionRetryAttempts; + SPMySQLConnectionLostDecision lastDelegateDecisionForLostConnection; + NSLock *delegateDecisionLock; + + // Timeout and keep-alive + NSUInteger timeout; + BOOL useKeepAlive; + NSTimer *keepAliveTimer; + CGFloat keepAliveInterval; + uint64_t lastKeepAliveTime; + NSUInteger keepAlivePingFailures; + pthread_t keepAlivePingThread; + BOOL keepAlivePingThreadActive; + BOOL keepAliveLastPingSuccess; + BOOL keepAliveLastPingBlocked; + + // Encoding details - and also a record of any previous encoding to allow + // switching back and forth + NSString *encoding; + NSStringEncoding stringEncoding; + BOOL encodingUsesLatin1Transport; + NSString *previousEncoding; + BOOL previousEncodingUsesLatin1Transport; + + // Server details + NSString *serverVersionString; + + // Error state for the last query or connection state + NSUInteger queryErrorID; + NSString *queryErrorMessage; + + // Query details + unsigned long long lastQueryAffectedRowCount; + unsigned long long lastQueryInsertID; + + // Query cancellation details + BOOL lastQueryWasCancelled; + BOOL lastQueryWasCancelledUsingReconnect; + + // Timing details + uint64_t lastConnectionUsedTime; + double lastQueryExecutionTime; + + // Maximum query size + NSUInteger maxQuerySize; + BOOL maxQuerySizeIsEditable; + BOOL maxQuerySizeEditabilityChecked; + NSUInteger queryActionShouldRestoreMaxQuerySize; + + // Queries + BOOL retryQueriesOnConnectionFailure; +} + +#pragma mark - +#pragma mark Synthesized properties + +@property (readwrite, assign, nonatomic) NSObject <SPMySQLConnectionDelegate> *delegate; +@property (readwrite, assign, nonatomic) NSObject <SPMySQLConnectionProxy> *proxy; + +@property (readwrite, retain) NSString *host; +@property (readwrite, retain) NSString *username; +@property (readwrite, retain) NSString *password; +@property (readwrite, assign) NSUInteger port; +@property (readwrite, assign) BOOL useSocket; +@property (readwrite, retain) NSString *socketPath; + +@property (readwrite, assign) BOOL useSSL; +@property (readwrite, retain) NSString *sslKeyFilePath; +@property (readwrite, retain) NSString *sslCertificatePath; +@property (readwrite, retain) NSString *sslCACertificatePath; + +@property (readwrite, assign) NSUInteger timeout; +@property (readwrite, assign) BOOL useKeepAlive; +@property (readwrite, assign) CGFloat keepAliveInterval; + +@property (readonly) unsigned long mysqlConnectionThreadId; +@property (readwrite, assign) BOOL retryQueriesOnConnectionFailure; + +@property (readwrite, assign) BOOL delegateQueryLogging; + +#pragma mark - +#pragma mark Connection and disconnection + +- (BOOL)connect; +- (BOOL)reconnect; +- (void)disconnect; + +#pragma mark - +#pragma mark Connection state + +- (BOOL)isConnected; +- (BOOL)isConnectedViaSSL; +- (BOOL)checkConnection; +- (double)timeConnected; +- (BOOL)userTriggeredDisconnect; + +#pragma mark - +#pragma mark Connection utility + ++ (NSString *)findSocketPath; + +@end
\ No newline at end of file diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection.m b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection.m new file mode 100644 index 00000000..4968266d --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection.m @@ -0,0 +1,834 @@ +// +// $Id$ +// +// SPMySQLConnection.m +// SPMySQLFramework +// +// Created by Rowan Beentje (rowan.beent.je) on January 8, 2012 +// Copyright (c) 2012 Rowan Beentje. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPMySQL Private APIs.h" +#include <mach/mach_time.h> +#include <pthread.h> +#include <SystemConfiguration/SCNetworkReachability.h> + + +#pragma mark Class constants + +// The default connection options for MySQL connections +const NSUInteger SPMySQLConnectionOptions = + CLIENT_COMPRESS | // Enable protocol compression - almost always a win + CLIENT_INTERACTIVE | // Mark ourselves as an interactive client + CLIENT_MULTI_RESULTS; // Multiple result support (very basic, but present) + +// List of permissible ciphers to use for SSL connections +const char *SPMySQLSSLPermissibleCiphers = "DHE-RSA-AES256-SHA:AES256-SHA:DHE-RSA-AES128-SHA:AES128-SHA:AES256-RMD:AES128-RMD:DES-CBC3-RMD:DHE-RSA-AES256-RMD:DHE-RSA-AES128-RMD:DHE-RSA-DES-CBC3-RMD:RC4-SHA:RC4-MD5:DES-CBC3-SHA:DES-CBC-SHA:EDH-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC-SHA"; + + +@implementation SPMySQLConnection + +#pragma mark - +#pragma mark Synthesized properties + +@synthesize delegate; +@synthesize proxy; +@synthesize host; +@synthesize username; +@synthesize password; +@synthesize port; +@synthesize useSocket; +@synthesize socketPath; +@synthesize useSSL; +@synthesize sslKeyFilePath; +@synthesize sslCertificatePath; +@synthesize sslCACertificatePath; +@synthesize timeout; +@synthesize useKeepAlive; +@synthesize keepAliveInterval; +@synthesize mysqlConnectionThreadId; +@synthesize retryQueriesOnConnectionFailure; +@synthesize delegateQueryLogging; + +#pragma mark - +#pragma mark Initialisation and teardown + +/** + * Initialise the SPMySQLConnection object, setting up class defaults. + * + * Typically initialisation would be followed by setting the connection details + * and then calling -connect. + */ +- (id)init +{ + if ((self = [super init])) { + mySQLConnection = NULL; + state = SPMySQLDisconnected; + userTriggeredDisconnect = NO; + isReconnecting = NO; + mysqlConnectionThreadId = 0; + initialConnectTime = 0; + + port = 3306; + + // Default to socket connections if no other details have been provided + useSocket = YES; + + // Start with no proxy + proxy = nil; + proxyStateChangeNotificationsIgnored = NO; + + // Start with no selected database + database = nil; + + // Set a timeout of 30 seconds, with keepalive on and acting every sixty seconds + timeout = 30; + useKeepAlive = YES; + keepAliveInterval = 60; + keepAlivePingFailures = 0; + lastKeepAliveTime = 0; + keepAlivePingThread = NULL; + keepAlivePingThreadActive = NO; + keepAliveLastPingSuccess = NO; + keepAliveLastPingBlocked = NO; + + // Set up default encoding variables + encoding = [[NSString alloc] initWithString:@"utf8"]; + stringEncoding = NSUTF8StringEncoding; + encodingUsesLatin1Transport = NO; + previousEncoding = nil; + previousEncodingUsesLatin1Transport = NO; + + // Initialise default delegate settings + delegateSupportsWillQueryString = NO; + delegateSupportsConnectionLost = NO; + delegateQueryLogging = YES; + + // Delegate disconnection decisions + reconnectionRetryAttempts = 0; + lastDelegateDecisionForLostConnection = SPMySQLConnectionLostDisconnect; + delegateDecisionLock = [[NSLock alloc] init]; + + // Set up the connection lock + connectionLock = [[NSConditionLock alloc] initWithCondition:SPMySQLConnectionIdle]; + [connectionLock setName:@"SPMySQLConnection query lock"]; + + // Ensure the server detail records are initialised + serverVersionString = nil; + + // Start with a blank error state + queryErrorID = 0; + queryErrorMessage = nil; + + // Start with empty cancellation details + lastQueryWasCancelled = NO; + lastQueryWasCancelledUsingReconnect = NO; + + // Empty or reset the timing variables + lastConnectionUsedTime = 0; + lastQueryExecutionTime = 0; + + // Default to editable query size of 1MB + maxQuerySize = 1048576; + maxQuerySizeIsEditable = YES; + maxQuerySizeEditabilityChecked = NO; + queryActionShouldRestoreMaxQuerySize = NSNotFound; + + // Default to allowing queries to be automatically retried if the connection drops + // while running them + retryQueriesOnConnectionFailure = YES; + + // Start the ping keepalive timer + if ([NSThread isMainThread]) { + [self _initKeepAlivePingTimer]; + } else { + [self performSelectorOnMainThread:@selector(_initKeepAlivePingTimer) withObject:nil waitUntilDone:YES]; + } + } + + return self; +} + +/** + * Object deallocation. + */ +- (void) dealloc +{ + userTriggeredDisconnect = YES; + + // Unset the delegate + [self setDelegate:nil]; + + // Disconnect if appropriate (which should also disconnect any proxy) + [self disconnect]; + + // Clean up the connection proxy, if any + if (proxy) { + [proxy setConnectionStateChangeSelector:NULL delegate:nil]; + [proxy release]; + } + + // Ensure the query lock is unlocked, thereafter setting to nil in case of pending calls + if ([connectionLock condition] != SPMySQLConnectionIdle) { + [self _unlockConnection]; + } + [connectionLock release], connectionLock = nil; + + [encoding dealloc]; + if (previousEncoding) [previousEncoding release], previousEncoding = nil; + + if (database) [database release], database = nil; + if (serverVersionString) [serverVersionString release], serverVersionString = nil; + if (queryErrorMessage) [queryErrorMessage release], queryErrorMessage = nil; + [keepAliveTimer invalidate]; + [keepAliveTimer release]; + [delegateDecisionLock release]; + + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + + [super dealloc]; +} + +#pragma mark - +#pragma mark Connection and disconnection + +/** + * Trigger a connection to the specified host, if any, using any connection details + * that have been set. + * Returns whether the connection was successful. + */ +- (BOOL)connect +{ + + // If a connection is already active in some form, throw an exception + if (state != SPMySQLDisconnected) { + [NSException raise:NSInternalInconsistencyException format:@"Attempted to connect a connection that is not disconnected."]; + return NO; + } + state = SPMySQLConnecting; + + // Lock the connection for safety + [self _lockConnection]; + + // Attempt the connection + mySQLConnection = [self _makeRawMySQLConnectionWithEncoding:encoding isMasterConnection:YES]; + + // If the connection failed, reset state and return + if (!mySQLConnection) { + [self _unlockConnection]; + state = SPMySQLDisconnected; + return NO; + } + + // Successfully connected - record connected state and reset tracking variables + state = SPMySQLConnected; + userTriggeredDisconnect = NO; + reconnectionRetryAttempts = 0; + initialConnectTime = mach_absolute_time(); + mysqlConnectionThreadId = mySQLConnection->thread_id; + lastConnectionUsedTime = 0; + + // Update SSL state + connectedWithSSL = NO; + if (useSSL) connectedWithSSL = (mysql_get_ssl_cipher(mySQLConnection))?YES:NO; + if (useSSL && !connectedWithSSL) { + if ([delegate respondsToSelector:@selector(connectionFellBackToNonSSL:)]) { + [delegate connectionFellBackToNonSSL:self]; + } + } + + // Reset keepalive variables + lastKeepAliveTime = 0; + keepAlivePingFailures = 0; + + // Clear the connection error record + [self _updateLastErrorID:NSNotFound]; + [self _updateLastErrorMessage:nil]; + + // Unlock the connection + [self _unlockConnection]; + + // Update connection variables to be in sync with the server state. As this performs + // a query, ensure the connection is still up afterwards (!) + [self _updateConnectionVariables]; + if (state != SPMySQLConnected) return NO; + + // Update the maximum query size + [self _updateMaxQuerySize]; + + return YES; +} + +/** + * Reconnect to the currently "active" - but possibly disconnected - connection, using the + * stored details. + * Error checks extensively - if this method fails, it will ask how to proceed and loop depending + * on the status, not returning control until either a connection has been established or + * the connection and document have been closed. + * Runs its own autorelease pool as sometimes called in a thread following proxy changes + * (where the return code doesn't matter). + */ +- (BOOL)reconnect +{ + if (userTriggeredDisconnect) return NO; + + NSAutoreleasePool *reconnectionPool = [[NSAutoreleasePool alloc] init]; + + // Check whether a reconnection attempt is already being made - if so, wait + // and return the status of that reconnection attempt. This improves threaded + // use of the connection by preventing reconnect races. + if (isReconnecting) { + + // Loop in a panel runloop mode until the reconnection has processed; if an iteration + // takes less than the requested 0.1s, sleep instead. + while (isReconnecting) { + uint64_t loopIterationStart_t = mach_absolute_time(); + + [[NSRunLoop currentRunLoop] runMode:NSModalPanelRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + if (_elapsedSecondsSinceAbsoluteTime(loopIterationStart_t) < 0.1) { + usleep(100000 - (useconds_t)(1000000 * _elapsedSecondsSinceAbsoluteTime(loopIterationStart_t))); + } + } + + [reconnectionPool drain]; + return (state == SPMySQLConnected); + } + + isReconnecting = YES; + + // Store certain details about the connection, so that if the reconnection is successful + // they can be restored. This has to be treated separately from _restoreConnectionDetails + // as a full connection reinitialises certain values from the server. + NSString *preReconnectEncoding = [NSString stringWithString:encoding]; + BOOL preReconnectEncodingUsesLatin1 = encodingUsesLatin1Transport; + NSString *preReconnectDatabase = nil; + if (database) preReconnectDatabase = [NSString stringWithString:database]; + + // If there is a connection proxy, temporarily disassociate the state change action + if (proxy) proxyStateChangeNotificationsIgnored = YES; + + // Close the connection if it's active + [self disconnect]; + + // Lock the connection while waiting for network and proxy + [self _lockConnection]; + + // If no network is present, wait for a short time for one to become available + [self _waitForNetworkConnectionWithTimeout:10]; + + // If there is a proxy, attempt to reconnect it in blocking fashion + if (proxy) { + uint64_t loopIterationStart_t, proxyWaitStart_t; + + // If the proxy is not yet idle after requesting a disconnect, wait for a short time + // to allow it to disconnect. + if ([proxy state] != SPMySQLProxyIdle) { + + proxyWaitStart_t = mach_absolute_time(); + while ([proxy state] != SPMySQLProxyIdle) { + loopIterationStart_t = mach_absolute_time(); + + // If the connection timeout has passed, break out of the loop + if (_elapsedSecondsSinceAbsoluteTime(proxyWaitStart_t) > timeout) break; + + // Allow events to process for 0.25s, sleeping to completion on early return + [[NSRunLoop currentRunLoop] runMode:NSModalPanelRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.25]]; + if (_elapsedSecondsSinceAbsoluteTime(loopIterationStart_t) < 0.25) { + usleep(250000 - (useconds_t)(1000000 * _elapsedSecondsSinceAbsoluteTime(loopIterationStart_t))); + } + } + } + + // Request that the proxy re-establishes its connection + [proxy connect]; + + // Wait while the proxy connects + proxyWaitStart_t = mach_absolute_time(); + while (1) { + loopIterationStart_t = mach_absolute_time(); + + // If the proxy has connected, record the new local port and break out of the loop + if ([proxy state] == SPMySQLProxyConnected) { + port = [proxy localPort]; + break; + } + + // If the proxy connection attempt time has exceeded the timeout, break of of the loop. + if (_elapsedSecondsSinceAbsoluteTime(proxyWaitStart_t) > (timeout + 1)) { + [proxy disconnect]; + break; + } + + // Process events for a short time, allowing dialogs to be shown but waiting for + // the proxy. Capture how long this interface action took, standardising the + // overall time. + [[NSRunLoop mainRunLoop] runMode:NSModalPanelRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.25]]; + if (_elapsedSecondsSinceAbsoluteTime(loopIterationStart_t) < 0.25) { + usleep((useconds_t)(250000 - (1000000 * _elapsedSecondsSinceAbsoluteTime(loopIterationStart_t)))); + } + + // Extend the connection timeout by any interface time + if ([proxy state] == SPMySQLProxyWaitingForAuth) { + proxyWaitStart_t += mach_absolute_time() - loopIterationStart_t; + } + } + + // Having in theory performed the proxy connect, update state + previousProxyState = [proxy state]; + proxyStateChangeNotificationsIgnored = NO; + } + + // Unlock the connection + [self _unlockConnection]; + + // If not using a proxy, or if the proxy successfully connected, trigger a connection + if (!proxy || [proxy state] == SPMySQLProxyConnected) { + [self connect]; + } + + // If the connection failed, retry the reconnection or cancel as appropriate. + if (state != SPMySQLConnected) { + + // Default to attempting another reconnect + SPMySQLConnectionLostDecision connectionLostDecision = SPMySQLConnectionLostReconnect; + + // If the delegate supports the decision process, ask it how to proceed + if (delegateSupportsConnectionLost) { + connectionLostDecision = [self _delegateDecisionForLostConnection]; + + // Otherwise default to reconnect, but only a set number of times to prevent a runaway loop + } else { + if (reconnectionRetryAttempts < 5) { + connectionLostDecision = SPMySQLConnectionLostReconnect; + } else { + connectionLostDecision = SPMySQLConnectionLostDisconnect; + } + reconnectionRetryAttempts++; + } + + switch (connectionLostDecision) { + case SPMySQLConnectionLostDisconnect: + [self _updateLastErrorMessage:NSLocalizedString(@"User triggered disconnection", @"User triggered disconnection")]; + userTriggeredDisconnect = YES; + isReconnecting = NO; + [reconnectionPool release]; + return NO; + default: + isReconnecting = NO; + [reconnectionPool release]; + return [self reconnect]; + } + } + + // If the connection was successfully established, restore the connection + // state if appropriate. + if (preReconnectDatabase) { + [self selectDatabase:preReconnectDatabase]; + } + [self setEncoding:preReconnectEncoding]; + [self setEncodingUsesLatin1Transport:preReconnectEncodingUsesLatin1]; + + isReconnecting = NO; + [reconnectionPool release]; + return YES; +} + +/** + * Trigger a disconnection if the connection is currently active. + */ +- (void)disconnect +{ + + // Only continue if a connection is active + if (state != SPMySQLConnected && state != SPMySQLConnecting) return; + state = SPMySQLDisconnecting; + + // If a query is active, cancel it + [self cancelCurrentQuery]; + + // Allow any pings or cancelled queries to complete, inside a time limit of ten seconds + uint64_t disconnectStartTime_t = mach_absolute_time(); + do { + usleep(100000); + if (_elapsedSecondsSinceAbsoluteTime(disconnectStartTime_t) > 10) break; + } while (![self _tryLockConnection]); + [self _unlockConnection]; + if (keepAlivePingThread != NULL) pthread_cancel(keepAlivePingThread), keepAlivePingThread = NULL; + + // Close the underlying MySQL connection if it still appears to be active, and not reading + // or writing. While this may result in a leak of the MySQL object, it prevents crashes + // due to attempts to close a blocked/stuck connection. + if (!mySQLConnection->net.reading_or_writing && mySQLConnection->net.vio && mySQLConnection->net.buff) { + mysql_close(mySQLConnection); + } + mySQLConnection = NULL; + + // If using a connection proxy, disconnect that too + if (proxy) { + [proxy performSelectorOnMainThread:@selector(disconnect) withObject:nil waitUntilDone:YES]; + } + + // Clear host-specific information + if (serverVersionString) [serverVersionString release], serverVersionString = nil; + if (database) [database release], database = nil; + + state = SPMySQLDisconnected; +} + +#pragma mark - +#pragma mark Connection state + +/** + * Retrieve whether the connection instance is connected to the remote host. + * Returns NO if the connection is still in process, YES if a disconnection is + * being actively performed. + */ +- (BOOL)isConnected +{ + return (state == SPMySQLConnected || state == SPMySQLDisconnecting); +} + +/** + * Returns YES if the MCPConnection is connected to a server via SSL, NO otherwise. + */ +- (BOOL)isConnectedViaSSL +{ + if (![self isConnected]) return NO; + return connectedWithSSL; +} + +/** + * Checks whether the connection to the server is still active. This verifies + * the connection using a ping, and if the connection is found to be down attempts + * to quickly restore it, including the previous state. + */ +- (BOOL)checkConnection +{ + + // If the connection is not seen as active, don't proceed + if (state != SPMySQLConnected) return NO; + + // Similarly, if the connection is currently locked, that indicates it's in use. This + // could be because queries are actively being run, or that a ping is running. + if ([connectionLock condition] == SPMySQLConnectionBusy) { + + // If a ping thread is not active queries are being performed - return success. + if (!keepAlivePingThreadActive) return YES; + + // If a ping thread is active, wait for it to complete before checking the connection + while (keepAlivePingThreadActive) { + usleep(10000); + } + } + + // Confirm whether the connection is still responding by using a ping + BOOL connectionVerified = [self _pingConnectionUsingLoopDelay:400]; + + // If the connection didn't respond, trigger a reconnect. This will automatically + // attempt to reconnect once, and if that fails will ask the user how to proceed - whether + // to keep reconnecting, or whether to disconnect. + if (!connectionVerified) { + connectionVerified = [self reconnect]; + } + + return connectionVerified; +} + +/** + * Retrieve the time elapsed since the connection was established, in seconds. + * This time is retrieved in a monotonically increasing fashion and is high + * precision; it is used internally for query timing, and is reset on reconnections. + * If no connection is currently active, returns -1. + */ +- (double)timeConnected +{ + if (initialConnectTime == 0) return -1; + + return _elapsedSecondsSinceAbsoluteTime(initialConnectTime); +} + +/** + * Returns YES if the user chose to disconnect at the last "connection failure" + * prompt, NO otherwise. This can be used to alter behaviour in response to state + * changes. + */ +- (BOOL)userTriggeredDisconnect +{ + return userTriggeredDisconnect; +} + +#pragma mark - +#pragma mark General connection utilities + ++ (NSString *)findSocketPath +{ + NSFileManager *fileManager = [NSFileManager defaultManager]; + + NSArray *possibleSocketLocations = [NSArray arrayWithObjects: + @"/tmp/mysql.sock", // Default + @"/Applications/MAMP/tmp/mysql/mysql.sock", // MAMP default location + @"/Applications/xampp/xamppfiles/var/mysql/mysql.sock", // XAMPP default location + @"/var/mysql/mysql.sock", // Mac OS X Server default + @"/opt/local/var/run/mysqld/mysqld.sock", // Darwinports MySQL + @"/opt/local/var/run/mysql4/mysqld.sock", // Darwinports MySQL 4 + @"/opt/local/var/run/mysql5/mysqld.sock", // Darwinports MySQL 5 + @"/usr/local/zend/mysql/tmp/mysql.sock", // Zend Server CE (see Issue #1251) + @"/var/run/mysqld/mysqld.sock", // As used on Debian/Gentoo + @"/var/tmp/mysql.sock", // As used on FreeBSD + @"/var/lib/mysql/mysql.sock", // As used by Fedora + @"/opt/local/lib/mysql/mysql.sock", // Alternate fedora + nil]; + + for (NSUInteger i = 0; i < [possibleSocketLocations count]; i++) { + if ([fileManager fileExistsAtPath:[possibleSocketLocations objectAtIndex:i]]) + return [possibleSocketLocations objectAtIndex:i]; + } + + return nil; +} + +@end + +#pragma mark - +#pragma mark Private API + +@implementation SPMySQLConnection (PrivateAPI) + +/** + * Make a connection using the class connection settings, returning a MySQL + * connection object on success. + */ +- (MYSQL *)_makeRawMySQLConnectionWithEncoding:(NSString *)encodingName isMasterConnection:(BOOL)isMaster +{ + + // Set up the MySQL connection object + MYSQL *theConnection = mysql_init(NULL); + if (!theConnection) return NULL; + + // Disable automatic reconnection, as it's handled in-framework to preserve + // options, encodings and connection state. + my_bool falseMyBool = FALSE; + mysql_options(theConnection, MYSQL_OPT_RECONNECT, &falseMyBool); + + // Set the connection timeout + mysql_options(theConnection, MYSQL_OPT_CONNECT_TIMEOUT, (const void *)&timeout); + + // Set the connection encoding + mysql_options(theConnection, MYSQL_SET_CHARSET_NAME, [encodingName UTF8String]); + + // Set up the connection variables in the format MySQL needs, from the class-wide variables + const char *theHost = NULL; + const char *theUsername = ""; + const char *thePassword = NULL; + const char *theSocket = NULL; + + if (host) theHost = [self _cStringForString:host]; + if (username) theUsername = [self _cStringForString:username]; + + // If a password was supplied, use it; otherwise ask the delegate if appropriate + if (password) { + thePassword = [self _cStringForString:password]; + } else if ([delegate respondsToSelector:@selector(keychainPasswordForConnection:)]) { + thePassword = [self _cStringForString:[delegate keychainPasswordForConnection:self]]; + } + + // If set to use a socket and a socket was supplied, use it; otherwise, search for a socket to use + if (useSocket) { + if (socketPath) { + theSocket = [self _cStringForString:socketPath]; + } else { + theSocket = [self _cStringForString:[SPMySQLConnection findSocketPath]]; + } + } + + // Apply SSL if appropriate + if (useSSL) { + const char *theSSLKeyFilePath = NULL; + const char *theSSLCertificatePath = NULL; + const char *theCACertificatePath = NULL; + + if (sslKeyFilePath) { + theSSLKeyFilePath = [[sslKeyFilePath stringByExpandingTildeInPath] UTF8String]; + } + if (sslCertificatePath) { + theSSLCertificatePath = [[sslCertificatePath stringByExpandingTildeInPath] UTF8String]; + } + if (sslCACertificatePath) { + theCACertificatePath = [[sslCACertificatePath stringByExpandingTildeInPath] UTF8String]; + } + + mysql_ssl_set(theConnection, theSSLKeyFilePath, theSSLCertificatePath, theCACertificatePath, NULL, SPMySQLSSLPermissibleCiphers); + } + + MYSQL *connectionStatus = mysql_real_connect(theConnection, theHost, theUsername, thePassword, NULL, (unsigned int)port, theSocket, SPMySQLConnectionOptions); + + // If the connection failed, return NULL + if (theConnection != connectionStatus) { + + // If the connection is the master connection, record the error state + if (isMaster) { + [self _updateLastErrorMessage:[self _stringForCString:mysql_error(theConnection)]]; + [self _updateLastErrorID:mysql_errno(theConnection)]; + } + + return NULL; + } + + // Ensure automatic reconnection is disabled for older versions + theConnection->reconnect = 0; + + // Successful connection - return the handle + return theConnection; +} + +/** + * Loop while a connection isn't available; allows blocking while the network is disconnected + * or still connecting (eg Airport still coming up after sleep). + */ +- (BOOL)_waitForNetworkConnectionWithTimeout:(double)timeoutSeconds +{ + BOOL hostReachable; + Boolean flagsValid; + SCNetworkReachabilityRef reachabilityTarget; + SCNetworkConnectionFlags reachabilityStatus; + + // Set up the reachability target - the host is not important, and is not connected to. + reachabilityTarget = SCNetworkReachabilityCreateWithName(NULL, "dev.mysql.com"); + + // In a loop until success or the timeout, test reachability + uint64_t loopStart_t = mach_absolute_time(); + while (1) { + + // Check reachability + flagsValid = SCNetworkReachabilityGetFlags(reachabilityTarget, &reachabilityStatus); + + hostReachable = flagsValid ? YES : NO; + + // Ensure that the network is reachable + if (hostReachable && !(reachabilityStatus & kSCNetworkFlagsReachable)) hostReachable = NO; + + // Ensure that Airport is up/connected if present + if (hostReachable && (reachabilityStatus & kSCNetworkFlagsConnectionRequired)) hostReachable = NO; + + // If the host *is* reachable, return success + if (hostReachable) return YES; + + // If the timeout has been exceeded, break out of the loop + if (_elapsedSecondsSinceAbsoluteTime(loopStart_t) >= timeoutSeconds) break; + + // Sleep before the next loop iteration + usleep(250000); + } + + // All checks failed - return failure + return NO; +} + +/** + * Update connection variables from the server, collecting state and ensuring + * settings like encoding are in sync. + */ +- (void)_updateConnectionVariables +{ + if (state != SPMySQLConnected && state != SPMySQLConnecting) return; + + // Retrieve all variables from the server in a single query + SPMySQLResult *theResult = [self queryString:@"SHOW VARIABLES"]; + if (![theResult numberOfRows]) return; + + // SHOW VARIABLES can return binary results on certain MySQL 4 versions; ensure string output + [theResult setReturnDataAsStrings:YES]; + + // Convert the result set into a variables dictionary + [theResult setDefaultRowReturnType:SPMySQLResultRowAsArray]; + NSMutableDictionary *variables = [NSMutableDictionary new]; + for (NSArray *variableRow in theResult) { + [variables setObject:[variableRow objectAtIndex:1] forKey:[variableRow objectAtIndex:0]]; + } + + // Copy the server version string to the instance variable + if (serverVersionString) [serverVersionString release], serverVersionString = nil; + serverVersionString = [[variables objectForKey:@"version"] retain]; + + // Get the connection encoding. Although a specific encoding may have been requested on + // connection, it may be overridden by init_connect commands or connection state changes. + // Default to latin1 for older server versions. + NSString *retrievedEncoding = @"latin1"; + if ([variables objectForKey:@"character_set_results"]) { + retrievedEncoding = [variables objectForKey:@"character_set_results"]; + } else if ([variables objectForKey:@"character_set"]) { + retrievedEncoding = [variables objectForKey:@"character_set"]; + } + + // Update instance variables + if (encoding) [encoding release]; + encoding = [[NSString alloc] initWithString:retrievedEncoding]; + stringEncoding = [SPMySQLConnection stringEncodingForMySQLCharset:[self _cStringForString:encoding]]; + encodingUsesLatin1Transport = NO; + + // Check the interactive timeout - if it's below five minutes, increase it to ten + // to imprive timeout/keepalive behaviour + if ([variables objectForKey:@"interactive_timeout"]) { + if ([[variables objectForKey:@"interactive_timeout"] integerValue] < 300) { + [self queryString:@"SET interactive_timeout=600"]; + } + } + + [variables release]; +} + +/** + * Restore the connection encoding details as necessary based on previously set + * details. + */ +- (void)_restoreConnectionVariables +{ + mysqlConnectionThreadId = mySQLConnection->thread_id; + initialConnectTime = mach_absolute_time(); + + [self selectDatabase:database]; + + [self setEncoding:encoding]; + [self setEncodingUsesLatin1Transport:encodingUsesLatin1Transport]; +} + +/** + * If thirty seconds have passed since the last time the connection was + * used, check the connection. + * This minimises the impact of continuous additional connection checks - + * each of which requires a round trip to the server - but handles most + * network issues. + * Returns whether the connection is considered still valid. + */ +- (BOOL)_checkConnectionIfNecessary +{ + + // If the connection was recently used, return success + if (_elapsedSecondsSinceAbsoluteTime(lastConnectionUsedTime) < 30) return YES; + + // Otherwise check the connection + return [self checkConnection]; +} +@end diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnectionDelegate.h b/Frameworks/SPMySQLFramework/Source/SPMySQLConnectionDelegate.h new file mode 100644 index 00000000..f1a1f911 --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnectionDelegate.h @@ -0,0 +1,106 @@ +// +// $Id$ +// +// SPMySQLConnectionDelegate.h +// SPMySQLFramework +// +// Created by Stuart Connolly (stuconnolly.com) on October 20, 2010. +// Copyright (c) 2010 Stuart Connolly. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at <http://code.google.com/p/sequel-pro/> + +@protocol SPMySQLConnectionDelegate <NSObject> +@optional + +/** + * Notifies the delegate that a query will be performed. + * + * @param query The query string that will be sent to the MySQL server + * @param connection The connection instance performing the query + */ +- (void)willQueryString:(NSString *)query connection:(id)connection; + +/** + * Notifies the delegate that a query that was just performed gave + * an error. + * + * @param error The query error, as a string + * @param connection The connection instance which received the error + */ +- (void)queryGaveError:(NSString *)error connection:(id)connection; + +/** + * Notifies the delegate that it should display the supplied error. + * The connection may sometimes want to notify the user directly + * about certain issues, and will use this method to allow the + * delegate to do so. + * + * @param title The title of the message to display to the user + * @param message The main text of the message to display to the user + */ +- (void)showErrorWithTitle:(NSString *)title message:(NSString *)message; + +/** + * Requests the keychain password for the connection. + * When a connection is being made to a server, it is best not to + * set the password on the class; instead, it should be kept within + * the secure store, and the other connection details (user, host) + * can be used to look it up and supplied on demand. + * + * @param connection The connection instance to supply the password for + */ +- (NSString *)keychainPasswordForConnection:(id)connection; + +/** + * Notifies the delegate that no underlying connection is available, + * typically when the connection has been asked to perform a query + * or some other action for which a connection must be present. + * Those actions will still return false or error states as appropriate, + * but the delegate may wish to perform actions as a result of a total + * loss of connection. + * + * @param connection The connection instance which has lost the connection to the host + */ +- (void)noConnectionAvailable:(id)connection; + +/** + * Notifies the delegate that although a SSL connection was requested, + * MySQL made the connection without using SSL. This can happen because + * the server connected to doesn't support SSL or had it disabled, or + * that insufficient details were provided to make the connection over + * SSL. + */ +- (void)connectionFellBackToNonSSL:(id)connection; + +/** + * Notifies the delegate that the connection has been temporarily lost, + * and asks the delegate for guidance on how to proceed. If the delegate + * does not implement this method, reconnections will automatically be + * attempted - up to a small limit of attempts. + * + * @param connection The connection instance that requires a decision on how to proceed + */ +- (SPMySQLConnectionLostDecision)connectionLost:(id)connection; + +@end diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnectionProxy.h b/Frameworks/SPMySQLFramework/Source/SPMySQLConnectionProxy.h new file mode 100644 index 00000000..afd7c95a --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnectionProxy.h @@ -0,0 +1,77 @@ +// +// $Id$ +// +// SPMySQLConnectionProxy.h +// SPMySQLFramework +// +// Created by Stuart Connolly (stuconnolly.com) on July 2, 2009. +// Copyright (c) 2009 Stuart Connolly. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at <http://code.google.com/p/sequel-pro/> + + +/** + * Connection proxy state constants. + */ +typedef enum { + SPMySQLProxyIdle = 0, + SPMySQLProxyConnecting = 1, + SPMySQLProxyWaitingForAuth = 2, + SPMySQLProxyConnected = 3, + SPMySQLProxyForwardingFailed = 4 +} SPMySQLConnectionProxyState; + + +@protocol SPMySQLConnectionProxy <NSObject> + +/** + * All the methods for this protocol are required. + */ + +/** + * Connect the proxy. + */ +- (void)connect; + +/** + * Disconnect the proxy. + */ +- (void)disconnect; + +/** + * Get the current state of the proxy. + */ +- (SPMySQLConnectionProxyState)state; + +/** + * Get the local port being provided by the proxy. + */ +- (NSUInteger)localPort; + +/** + * Sets the method the proxy should call whenever the state of the connection changes. + */ +- (BOOL)setConnectionStateChangeSelector:(SEL)theStateChangeSelector delegate:(id)theDelegate; + +@end
\ No newline at end of file diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConstants.h b/Frameworks/SPMySQLFramework/Source/SPMySQLConstants.h new file mode 100644 index 00000000..6fb3d279 --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConstants.h @@ -0,0 +1,76 @@ +// +// $Id$ +// +// SPMySQLConstants.h +// SPMySQLFramework +// +// Created by Rowan Beentje (rowan.beent.je) on January 14, 2012 +// Copyright (c) 2012 Rowan Beentje. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at <http://code.google.com/p/sequel-pro/> + + +// Connection state +typedef enum { + SPMySQLDisconnected = 0, + SPMySQLConnecting = 1, + SPMySQLConnected = 2, + SPMySQLDisconnecting = 3 +} SPMySQLConnectionState; + +// Connection lock state +typedef enum { + SPMySQLConnectionIdle = 0, + SPMySQLConnectionBusy = 1 +} SPMySQLConnectionLockState; + +// Decision on how to handle lost connections +// Connection check constants +typedef enum { + SPMySQLConnectionLostDisconnect = 0, + SPMySQLConnectionLostReconnect = 1 +} SPMySQLConnectionLostDecision; + +// Result set row types +typedef enum { + SPMySQLResultRowAsDefault = 0, + SPMySQLResultRowAsArray = 1, + SPMySQLResultRowAsDictionary = 2 +} SPMySQLResultRowType; + +// Result charset list +typedef struct { + NSUInteger nr; + const char *name; + const char *collation; + NSUInteger char_minlen; + NSUInteger char_maxlen; +} SPMySQLResultCharset; + +// Query result types +typedef enum { + SPMySQLResultAsResult = 0, + SPMySQLResultAsFastStreamingResult = 1, + SPMySQLResultAsLowMemStreamingResult = 2 +} SPMySQLResultType;
\ No newline at end of file diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLFastStreamingResult.h b/Frameworks/SPMySQLFramework/Source/SPMySQLFastStreamingResult.h new file mode 100644 index 00000000..a4f07cdd --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLFastStreamingResult.h @@ -0,0 +1,47 @@ +// +// $Id$ +// +// SPMySQLFastStreamingResult.h +// SPMySQLFramework +// +// Created by Rowan Beentje (rowan.beent.je) on February 2, 2012 +// Copyright (c) 2012 Rowan Beentje. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at <http://code.google.com/p/sequel-pro/> + + +@interface SPMySQLFastStreamingResult : SPMySQLStreamingResult { + + // Linked list setup + struct st_spmysqlstreamingrowdata *currentDataStoreEntry; + struct st_spmysqlstreamingrowdata *lastDataStoreEntry; + + // Additional counts and memory length tracking + NSUInteger processedRowCount; + + // Thread safety + pthread_mutex_t dataLock; +} + +@end diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLFastStreamingResult.m b/Frameworks/SPMySQLFramework/Source/SPMySQLFastStreamingResult.m new file mode 100644 index 00000000..f084dee0 --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLFastStreamingResult.m @@ -0,0 +1,417 @@ +// +// $Id$ +// +// SPMySQLFastStreamingResult.m +// SPMySQLFramework +// +// Created by Rowan Beentje (rowan.beent.je) on February 2, 2012 +// Copyright (c) 2012 Rowan Beentje. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPMySQLFastStreamingResult.h" +#import "SPMySQL Private APIs.h" +#include <pthread.h> + +/** + * This type of streaming result operates in a multithreaded fashion - a worker + * thread is set up to download the results as fast as possible in the background, + * while the results are made available via blocking (and so single-thread-compatible) + * calls. This provides the benefit of allowing a progress bar to be shown during + * downloads, and threaded processing, but still has reasonable memory usage for the + * downloaded result - and won't block the server. + */ + +typedef struct st_spmysqlstreamingrowdata { + char *data; + NSUInteger *dataLengths; + struct st_spmysqlstreamingrowdata *nextRow; +} SPMySQLStreamingRowData; + +@interface SPMySQLFastStreamingResult (Private_API) + +- (void) _downloadAllData; + +@end + +#pragma mark - + +@implementation SPMySQLFastStreamingResult + +#pragma mark - + +/** + * Standard init method, constructing the SPMySQLStreamingResult around a MySQL + * result pointer and the encoding to use when working with the data. + * As opposed to SPMySQLResult, defaults to returning rows as arrays, as the result + * sets are likely to be larger and processed in loops. + */ +- (id)initWithMySQLResult:(void *)theResult stringEncoding:(NSStringEncoding)theStringEncoding connection:(SPMySQLConnection *)theConnection +{ + + // If no result set was passed in, return nil. + if (!theResult) return nil; + + if ((self = [super initWithMySQLResult:theResult stringEncoding:theStringEncoding connection:theConnection])) { + + // Initialise the extra streaming result counts and tracking + processedRowCount = 0; + + // Initialise the linked list pointers + currentDataStoreEntry = NULL; + lastDataStoreEntry = NULL; + + // Set up the linked list lock + pthread_mutex_init(&dataLock, NULL); + + // Start the data download thread + [NSThread detachNewThreadSelector:@selector(_downloadAllData) toTarget:self withObject:nil]; + } + + return self; +} + +/** + * Deallocate the result and ensure the parent connection is unlocked for further use. + */ +- (void)dealloc +{ + + // Ensure all data is processed and the parent connection is unlocked + [self cancelResultLoad]; + + // Destroy the linked list lock + pthread_mutex_destroy(&dataLock); + + // Call dealloc on super to clean up everything else, and to throw an exception if + // the parent connection hasn't been cleaned up correctly. + [super dealloc]; +} + +#pragma mark - +#pragma mark Data retrieval + +/** + * Override the convenience selectors so that forwarding works correctly. + */ +- (id)getRow +{ + return SPMySQLResultGetRow(self, SPMySQLResultRowAsDefault); +} +- (NSArray *)getRowAsArray +{ + return SPMySQLResultGetRow(self, SPMySQLResultRowAsArray); +} +- (NSDictionary *)getRowAsDictionary +{ + return SPMySQLResultGetRow(self, SPMySQLResultRowAsDictionary); +} + +/** + * Retrieve the next row in the result set, using the internal pointer, in the specified + * return format. + * If there are no rows remaining in the current iteration, returns nil. + */ +- (id)getRowAsType:(SPMySQLResultRowType)theType +{ + NSUInteger copiedDataLength = 0; + char *theRowData; + NSUInteger *fieldLengths; + id theReturnData; + + // Lock the data mutex for safe access of variables and counters + pthread_mutex_lock(&dataLock); + + // Determine whether any data is available; if not, wait 1ms before trying again + while (!dataDownloaded && processedRowCount == downloadedRowCount) { + pthread_mutex_unlock(&dataLock); + usleep(1000); + pthread_mutex_lock(&dataLock); + } + + // If all rows have been processed, the end of the result set has been reached; return nil. + if (processedRowCount == downloadedRowCount) { + pthread_mutex_unlock(&dataLock); + return nil; + } + + // Unlock the data mutex now checks are complete + pthread_mutex_unlock(&dataLock); + + // Get a reference to the data for the current row; this is safe to do outside the lock + // as the pointer won't change until markers are changed at the end of this process + theRowData = currentDataStoreEntry->data; + fieldLengths = currentDataStoreEntry->dataLengths; + + // If the target type was unspecified, use the instance default + if (theType == SPMySQLResultRowAsDefault) theType = defaultRowReturnType; + + // Set up the return data as appropriate + if (theType == SPMySQLResultRowAsArray) { + theReturnData = [NSMutableArray arrayWithCapacity:numberOfFields]; + } else { + theReturnData = [NSMutableDictionary dictionaryWithCapacity:numberOfFields]; + } + + // Convert each of the cells in the row in turn + for (NSUInteger i = 0; i < numberOfFields; i++) { + char *rawCellData; + NSUInteger fieldLength = fieldLengths[i]; + + // If the length of this cell is NSNotFound, it's a null reference + if (fieldLength == NSNotFound) { + rawCellData = NULL; + + // Otherwise grab a reference to that data using pointer arithmetic + } else { + rawCellData = theRowData + copiedDataLength; + copiedDataLength += fieldLength; + } + + // Convert to the correct object type + id cellData = SPMySQLResultGetObject(self, rawCellData, fieldLength, fieldTypes[i], i); + + // If object creation failed, display a null + if (!cellData) cellData = [NSNull null]; + + // Add to the result array/dictionary + if (theType == SPMySQLResultRowAsArray) { + [(NSMutableArray *)theReturnData addObject:cellData]; + } else { + [(NSMutableDictionary *)theReturnData setObject:cellData forKey:fieldNames[i]]; + } + } + + // Get a reference to the current item + SPMySQLStreamingRowData *previousDataStoreEntry = currentDataStoreEntry; + + // Lock the mutex before updating counters and linked lists + pthread_mutex_lock(&dataLock); + + // Update the active-data pointer to the next item in the list (which may be NULL) + currentDataStoreEntry = currentDataStoreEntry->nextRow; + + // Increment the processed counter and row index + processedRowCount++; + currentRowIndex++; + if (dataDownloaded && processedRowCount == downloadedRowCount) currentRowIndex = NSNotFound; + + // Unlock the mutex + pthread_mutex_unlock(&dataLock); + + // Free the memory for the processed row + previousDataStoreEntry->nextRow = NULL; + free(previousDataStoreEntry->dataLengths); + if (previousDataStoreEntry->data != NULL) free(previousDataStoreEntry->data); + free(previousDataStoreEntry); + + return theReturnData; +} + +/* + * Ensure the result set is fully processed and freed without any processing + * This method ensures that the connection is unlocked. + */ +- (void)cancelResultLoad +{ + + // If data has already been downloaded successfully, no further action is required + if (dataDownloaded && processedRowCount == downloadedRowCount) return; + + // Loop until all data is fetched and freed + while (1) { + + // Check to see whether we need to wait for the data to be available + // - if so, wait 1ms before checking again + while (!dataDownloaded && processedRowCount == downloadedRowCount) usleep(1000); + + // If all rows have been processed, we're at the end of the result set - return + if (processedRowCount == downloadedRowCount) { + + // We don't need to unlock the connection because the data loading thread + // has already taken care of that + return; + } + + // Mark the row entry as processed without performing any actions + pthread_mutex_lock(&dataLock); + SPMySQLStreamingRowData *previousDataStoreEntry = currentDataStoreEntry; + + // Update the active-data pointer to the next item in the list (which may be NULL) + currentDataStoreEntry = currentDataStoreEntry->nextRow; + + processedRowCount++; + currentRowIndex++; + if (dataDownloaded && processedRowCount == downloadedRowCount) currentRowIndex = NSNotFound; + + // Unlock the mutex + pthread_mutex_unlock(&dataLock); + + // Free the memory for the processed row + previousDataStoreEntry->nextRow = NULL; + free(previousDataStoreEntry->dataLengths); + if (previousDataStoreEntry->data != NULL) free(previousDataStoreEntry->data); + free(previousDataStoreEntry); + } +} + +#pragma mark - +#pragma mark Data retrieval for fast enumeration + +/** + * Implement the fast enumeration endpoint. Rows for fast enumeration are retrieved in + * the instance default, as specified in setDefaultRowReturnType: or defaulting to + * NSDictionary. + */ +- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len +{ + + // If all rows have already been processed, return 0 to stop iteration. + if (dataDownloaded && processedRowCount == downloadedRowCount) return 0; + + // If the MySQL row pointer does not match the requested state, throw an exception + if (state->state != currentRowIndex) { + [NSException raise:NSRangeException format:@"SPMySQLFastStreamingResult results can only be accessed linearly"]; + } + + // Determine how many objects to return. Default to 128 items, or the number of items requested + NSUInteger itemsToReturn = 128; + if (len < 128) itemsToReturn = len; + + // If there are fewer items available in the downloaded-but-processed queue, limit to that + if (downloadedRowCount - processedRowCount < itemsToReturn) { + itemsToReturn = downloadedRowCount - processedRowCount; + } + + // If no rows are available to be processed, wait for a single item to be readied. + if (!itemsToReturn) itemsToReturn = 1; + + // Retrieve rows and add them to the result stack + NSUInteger i, itemsRetrieved = 0; + id eachRow; + for (i = 0; i < itemsToReturn; i++) { + eachRow = SPMySQLResultGetRow(self, SPMySQLResultRowAsDefault); + + // If nil was returned the end of the result resource has been reached + if (!eachRow) { + if (!itemsRetrieved) return 0; + break; + } + + stackbuf[i] = eachRow; + itemsRetrieved++; + } + + state->state += itemsRetrieved; + state->itemsPtr = stackbuf; + state->mutationsPtr = (unsigned long *)self; + + return itemsRetrieved; +} + +@end + +#pragma mark - +#pragma mark Result set internals + +@implementation SPMySQLFastStreamingResult (Private_API) + +/** + * Used internally to download results in a background thread + */ +- (void)_downloadAllData +{ + NSAutoreleasePool *downloadPool = [[NSAutoreleasePool alloc] init]; + MYSQL_ROW theRow; + unsigned long *fieldLengths; + NSUInteger i, dataCopiedLength, rowDataLength; + SPMySQLStreamingRowData *newRowStore; + + size_t sizeOfStreamingRowData = sizeof(SPMySQLStreamingRowData); + size_t sizeOfDataLengths = (size_t)(sizeof(NSUInteger) * numberOfFields); + size_t sizeOfChar = sizeof(char); + + // Loop through the rows until the end of the data is reached - indicated via a NULL + while ( + (*isConnectedPtr)(parentConnection, isConnectedSelector) + && (theRow = mysql_fetch_row(resultSet)) + ) + { + + // Retrieve the lengths of the returned data + fieldLengths = mysql_fetch_lengths(resultSet); + rowDataLength = 0; + dataCopiedLength = 0; + for (i = 0; i < numberOfFields; i++) { + rowDataLength += fieldLengths[i]; + } + + // Initialise memory for the row and set a NULL pointer for the next item + newRowStore = malloc(sizeOfStreamingRowData); + newRowStore->nextRow = NULL; + + // Set up the row data store - a char* - and copy in the data if there is any. + newRowStore->data = malloc(sizeOfChar * rowDataLength); + for (i = 0; i < numberOfFields; i++) { + if (theRow[i] != NULL) { + memcpy(newRowStore->data+dataCopiedLength, theRow[i], fieldLengths[i]); + dataCopiedLength += fieldLengths[i]; + } else { + fieldLengths[i] = NSNotFound; + } + } + + // Set up the memory for, and copy in, the field lengths + newRowStore->dataLengths = memcpy(malloc(sizeOfDataLengths), fieldLengths, sizeOfDataLengths); + + // Lock the data mutex + pthread_mutex_lock(&dataLock); + + // Add the newly allocated row to end of the storage linked list + if (lastDataStoreEntry) { + lastDataStoreEntry->nextRow = newRowStore; + } + lastDataStoreEntry = newRowStore; + if (!currentDataStoreEntry) currentDataStoreEntry = newRowStore; + + // Update the downloaded row count + downloadedRowCount++; + + // Unlock the mutex + pthread_mutex_unlock(&dataLock); + } + + // Update the connection's error statuses to reflect any errors during the content download + [parentConnection _updateLastErrorID:NSNotFound]; + [parentConnection _updateLastErrorMessage:nil]; + + // Unlock the parent connection now all data has been retrieved + [parentConnection _unlockConnection]; + connectionUnlocked = YES; + + dataDownloaded = YES; + [downloadPool drain]; +} + +@end diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLFramework_Prefix.pch b/Frameworks/SPMySQLFramework/Source/SPMySQLFramework_Prefix.pch new file mode 100644 index 00000000..8528c29c --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLFramework_Prefix.pch @@ -0,0 +1,11 @@ +// +// Prefix header for all source files of the 'SPMySQLFramework' target in the 'SPMySQLFramework' project. +// + +#ifdef __OBJC__ + #import <Cocoa/Cocoa.h> +#endif + +#import "mysql.h" +#import "SPMySQL.h" +#import "SPMySQLUtilities.h"
\ No newline at end of file diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLGeometryData.h b/Frameworks/SPMySQLFramework/Source/SPMySQLGeometryData.h new file mode 100644 index 00000000..e1313032 --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLGeometryData.h @@ -0,0 +1,53 @@ +// +// $Id$ +// +// SPMySQLGeometryData.h +// sequel-pro +// +// Created by Hans-Jörg Bibiko on October 07, 2010 +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at <http://code.google.com/p/sequel-pro/> + + +@interface SPMySQLGeometryData : NSObject +{ + // Holds the WKB bytes coming from SQL server + Byte *geoBuffer; + + // Holds the buffer length + NSUInteger bufferLength; + +} + +- (id)initWithBytes:(const void *)geoData length:(NSUInteger)length; ++ (id)dataWithBytes:(const void *)geoData length:(NSUInteger)length; +- (NSString *)description; +- (NSUInteger)length; +- (NSData *)data; +- (NSString *)wktString; +- (NSDictionary *)coordinates; +- (NSInteger)wkbType; +- (NSString *)wktType; + +@end diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLGeometryData.m b/Frameworks/SPMySQLFramework/Source/SPMySQLGeometryData.m new file mode 100644 index 00000000..3c37e403 --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLGeometryData.m @@ -0,0 +1,810 @@ +// +// $Id$ +// +// SPMySQLGeometryData.m +// sequel-pro +// +// Created by Hans-Jörg Bibiko on October 07, 2010 +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPMySQLGeometryData.h" + +enum wkbType +{ + wkb_point = 1, + wkb_linestring = 2, + wkb_polygon = 3, + wkb_multipoint = 4, + wkb_multilinestring = 5, + wkb_multipolygon = 6, + wkb_geometrycollection = 7 +}; + +typedef struct st_point_2d_ +{ + double x; + double y; +} st_point_2d; + +#define SIZEOF_STORED_UINT32 4 +#define SIZEOF_STORED_DOUBLE 8 +#define POINT_DATA_SIZE (SIZEOF_STORED_DOUBLE*2) +#define WKB_HEADER_SIZE (1+SIZEOF_STORED_UINT32) +#define BUFFER_START 0 + +@implementation SPMySQLGeometryData + +/** + * Initialize the SPMySQLGeometryData object + */ +- (id)init +{ + if ((self = [super init])) { + geoBuffer = nil; + bufferLength = 0; + } + return self; +} + +/** + * Initialize the SPMySQLGeometryData object with the WKB data + */ +- (id)initWithBytes:(const void *)geoData length:(NSUInteger)length +{ + if ((self = [self init])) { + bufferLength = length; + geoBuffer = malloc(bufferLength); + memcpy(geoBuffer, geoData, bufferLength); + } + return self; +} + +/** + * Return an autorelease SPMySQLGeometryData object + */ ++ (id)dataWithBytes:(const void *)geoData length:(NSUInteger)length +{ + return [[[SPMySQLGeometryData alloc] initWithBytes:geoData length:length] autorelease]; +} + +/** + * copyWithZone + */ +- (id)copyWithZone:(NSZone *)zone +{ + return [self retain]; +} + +/** + * Return the hex representation of the WKB buffer (only for convenience) + */ +- (NSString*)description +{ + return [[NSData dataWithBytes:geoBuffer length:bufferLength] description]; +} + +/** + * Return the length of the WKB buffer + */ +- (NSUInteger)length +{ + return bufferLength; +} + +/** + * Return NSData pointer of the WKB buffer + */ +- (NSData *)data +{ + return [NSData dataWithBytes:geoBuffer length:bufferLength]; +} + +/** + * Return a human readable WKT string of the internal format (imitating the SQL function AsText()). + */ +- (NSString *)wktString +{ + char byteOrder; + uint32_t geoType, numberOfItems, numberOfSubItems, numberOfSubSubItems, numberOfCollectionItems; + int32_t srid; + st_point_2d aPoint; + + uint32_t i, j, k, n; // Loop counter for numberOf...Items + uint32_t ptr = BUFFER_START; // pointer to geoBuffer while parsing + + NSMutableString *wkt = [NSMutableString string]; + + if (bufferLength < WKB_HEADER_SIZE) + return @""; + + memcpy(&srid, &geoBuffer[0], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32; + + byteOrder = (char)geoBuffer[ptr]; + + if (byteOrder != 0x1) + return @"Byte order not yet supported"; + + ptr++; + geoType = geoBuffer[ptr]; + ptr += SIZEOF_STORED_UINT32; + + switch (geoType) { + + case wkb_point: + memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE); + return [NSString stringWithFormat:@"POINT(%.16g %.16g)%@", aPoint.x, aPoint.y, (srid) ? [NSString stringWithFormat:@",%d",srid]: @""]; + break; + + case wkb_linestring: + [wkt setString:@"LINESTRING("]; + memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32; + for (i=0; i < numberOfItems; i++) { + memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE); + [wkt appendFormat:@"%.16g %.16g%@", aPoint.x, aPoint.y, (i < numberOfItems-1) ? @"," : @""]; + ptr += POINT_DATA_SIZE; + } + [wkt appendFormat:@")%@", (srid) ? [NSString stringWithFormat:@",%d",srid]: @""]; + return wkt; + break; + + case wkb_polygon: + [wkt setString:@"POLYGON("]; + memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32; + for (i=0; i < numberOfItems; i++) { + memcpy(&numberOfSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32; + [wkt appendString:@"("]; + for (j=0; j < numberOfSubItems; j++) { + memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE); + [wkt appendFormat:@"%.16g %.16g%@", aPoint.x, aPoint.y, (j < numberOfSubItems-1) ? @"," : @""]; + ptr += POINT_DATA_SIZE; + } + [wkt appendFormat:@")%@", (i < numberOfItems-1) ? @"," : @""]; + } + [wkt appendFormat:@")%@", (srid) ? [NSString stringWithFormat:@",%d",srid]: @""]; + return wkt; + break; + + case wkb_multipoint: + [wkt setString:@"MULTIPOINT("]; + memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32+WKB_HEADER_SIZE; + for (i=0; i < numberOfItems; i++) { + memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE); + [wkt appendFormat:@"%.16g %.16g%@", aPoint.x, aPoint.y, (i < numberOfItems-1) ? @"," : @""]; + ptr += POINT_DATA_SIZE+WKB_HEADER_SIZE; + } + [wkt appendFormat:@")%@", (srid) ? [NSString stringWithFormat:@",%d",srid]: @""]; + return wkt; + break; + + case wkb_multilinestring: + [wkt setString:@"MULTILINESTRING("]; + memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32+WKB_HEADER_SIZE; + for (i=0; i < numberOfItems; i++) { + memcpy(&numberOfSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32; + [wkt appendString:@"("]; + for (j=0; j < numberOfSubItems; j++) { + memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE); + [wkt appendFormat:@"%.16g %.16g%@", aPoint.x, aPoint.y, (j < numberOfSubItems-1) ? @"," : @""]; + ptr += POINT_DATA_SIZE; + } + ptr += WKB_HEADER_SIZE; + [wkt appendFormat:@")%@", (i < numberOfItems-1) ? @"," : @""]; + } + [wkt appendFormat:@")%@", (srid) ? [NSString stringWithFormat:@",%d",srid]: @""]; + return wkt; + break; + + case wkb_multipolygon: + [wkt setString:@"MULTIPOLYGON("]; + memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32+WKB_HEADER_SIZE; + for (i=0; i < numberOfItems; i++) { + memcpy(&numberOfSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32; + [wkt appendString:@"("]; + for (j=0; j < numberOfSubItems; j++) { + memcpy(&numberOfSubSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32; + [wkt appendString:@"("]; + for (k=0; k < numberOfSubSubItems; k++) { + memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE); + [wkt appendFormat:@"%.16g %.16g%@", aPoint.x, aPoint.y, (k < numberOfSubSubItems-1) ? @"," : @""]; + ptr += POINT_DATA_SIZE; + } + [wkt appendFormat:@")%@", (j < numberOfSubItems-1) ? @"," : @""]; + } + ptr += WKB_HEADER_SIZE; + [wkt appendFormat:@")%@", (i < numberOfItems-1) ? @"," : @""]; + } + [wkt appendFormat:@")%@", (srid) ? [NSString stringWithFormat:@",%d",srid]: @""]; + return wkt; + break; + + case wkb_geometrycollection: + [wkt setString:@"GEOMETRYCOLLECTION("]; + numberOfCollectionItems = geoBuffer[ptr]; + ptr += SIZEOF_STORED_UINT32; + + for (n=0; n < numberOfCollectionItems; n++) { + + byteOrder = (char)geoBuffer[ptr]; + + if(byteOrder != 0x1) + return @"Byte order not yet supported"; + + ptr++; + geoType = geoBuffer[ptr]; + ptr += SIZEOF_STORED_UINT32; + + switch(geoType) { + + case wkb_point: + memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE); + [wkt appendFormat:@"POINT(%.16g %.16g)", aPoint.x, aPoint.y]; + ptr += POINT_DATA_SIZE; + break; + + case wkb_linestring: + [wkt appendString:@"LINESTRING("]; + memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32; + for (i=0; i < numberOfItems; i++) { + memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE); + [wkt appendFormat:@"%.16g %.16g%@", aPoint.x, aPoint.y, (i < numberOfItems-1) ? @"," : @""]; + ptr += POINT_DATA_SIZE; + } + [wkt appendString:@")"]; + break; + + case wkb_polygon: + [wkt appendString:@"POLYGON("]; + memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32; + for (i=0; i < numberOfItems; i++) { + memcpy(&numberOfSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32; + [wkt appendString:@"("]; + for (j=0; j < numberOfSubItems; j++) { + memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE); + [wkt appendFormat:@"%.16g %.16g%@", aPoint.x, aPoint.y, (j < numberOfSubItems-1) ? @"," : @""]; + ptr += POINT_DATA_SIZE; + } + [wkt appendFormat:@")%@", (i < numberOfItems-1) ? @"," : @""]; + } + [wkt appendString:@")"]; + break; + + case wkb_multipoint: + [wkt appendString:@"MULTIPOINT("]; + memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32+WKB_HEADER_SIZE; + for (i=0; i < numberOfItems; i++) { + memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE); + [wkt appendFormat:@"%.16g %.16g%@", aPoint.x, aPoint.y, (i < numberOfItems-1) ? @"," : @""]; + ptr += POINT_DATA_SIZE+WKB_HEADER_SIZE; + } + ptr -= WKB_HEADER_SIZE; + [wkt appendString:@")"]; + break; + + case wkb_multilinestring: + [wkt appendString:@"MULTILINESTRING("]; + memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32+WKB_HEADER_SIZE; + for (i=0; i < numberOfItems; i++) { + memcpy(&numberOfSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32; + [wkt appendString:@"("]; + for (j=0; j < numberOfSubItems; j++) { + memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE); + [wkt appendFormat:@"%.16g %.16g%@", aPoint.x, aPoint.y, (j < numberOfSubItems-1) ? @"," : @""]; + ptr += POINT_DATA_SIZE; + } + ptr += WKB_HEADER_SIZE; + [wkt appendFormat:@")%@", (i < numberOfItems-1) ? @"," : @""]; + } + ptr -= WKB_HEADER_SIZE; + [wkt appendString:@")"]; + break; + + case wkb_multipolygon: + [wkt appendString:@"MULTIPOLYGON("]; + memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32+WKB_HEADER_SIZE; + for (i=0; i < numberOfItems; i++) { + memcpy(&numberOfSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32; + [wkt appendString:@"("]; + for (j=0; j < numberOfSubItems; j++) { + memcpy(&numberOfSubSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32; + [wkt appendString:@"("]; + for (k=0; k < numberOfSubSubItems; k++) { + memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE); + [wkt appendFormat:@"%.16g %.16g%@", aPoint.x, aPoint.y, (k < numberOfSubSubItems-1) ? @"," : @""]; + ptr += POINT_DATA_SIZE; + } + [wkt appendFormat:@")%@", (j < numberOfSubItems-1) ? @"," : @""]; + } + ptr += WKB_HEADER_SIZE; + [wkt appendFormat:@")%@", (i < numberOfItems-1) ? @"," : @""]; + } + ptr -= WKB_HEADER_SIZE; + [wkt appendString:@")"]; + break; + + default: + return @"Error geometrycollection type parsing"; + } + [wkt appendString:(n < numberOfCollectionItems-1) ? @"," : @""]; + } + [wkt appendFormat:@")%@", (srid) ? [NSString stringWithFormat:@",%d",srid]: @""]; + return wkt; + break; + + default: + return @"Error geometry type parsing"; + } + + return @"Error while parsing"; +} + +/** + * Return a dictionary of coordinates, bbox, etc. to be able to draw the given geometry. + * + * @return A dictionary having the following keys: "bbox" as NSArray of NSNumbers of x_min x_max y_min y_max, "coordinates" as NSArray containing the + * the to be drawn points as NSPoint strings, "type" as NSString + */ +- (NSDictionary *)coordinates +{ + char byteOrder; + uint32_t geoType, numberOfItems, numberOfSubItems, numberOfSubSubItems, numberOfCollectionItems; + int32_t srid; + st_point_2d aPoint; + + uint32_t i, j, k, n; // Loop counter for numberOf...Items + uint32_t ptr = BUFFER_START; // pointer to geoBuffer while parsing + + double x_min = DBL_MAX; + double x_max = -DBL_MAX; + double y_min = DBL_MAX; + double y_max = -DBL_MAX; + + NSMutableArray *coordinates = [NSMutableArray array]; + NSMutableArray *subcoordinates = [NSMutableArray array]; + NSMutableArray *pointcoordinates = [NSMutableArray array]; + NSMutableArray *linecoordinates = [NSMutableArray array]; + NSMutableArray *linesubcoordinates = [NSMutableArray array]; + NSMutableArray *polygoncoordinates = [NSMutableArray array]; + NSMutableArray *polygonsubcoordinates = [NSMutableArray array]; + + if (bufferLength < WKB_HEADER_SIZE) + return nil; + + memcpy(&srid, &geoBuffer[0], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32; + + byteOrder = (char)geoBuffer[ptr]; + + if (byteOrder != 0x1) + return nil; + + ptr++; + geoType = geoBuffer[ptr]; + ptr += SIZEOF_STORED_UINT32; + + switch(geoType) { + + case wkb_point: + memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE); + x_min = aPoint.x; + x_max = aPoint.x; + y_min = aPoint.y; + y_max = aPoint.y; + [coordinates addObject:NSStringFromPoint(NSMakePoint((CGFloat)aPoint.x, (CGFloat)aPoint.y))]; + return [NSDictionary dictionaryWithObjectsAndKeys: + [NSArray arrayWithObjects: + [NSNumber numberWithDouble:x_min], + [NSNumber numberWithDouble:x_max], + [NSNumber numberWithDouble:y_min], + [NSNumber numberWithDouble:y_max], + nil], @"bbox", + coordinates, @"coordinates", + [NSNumber numberWithInt:srid], @"srid", + @"POINT", @"type", + nil]; + break; + + case wkb_linestring: + memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32; + for (i=0; i < numberOfItems; i++) { + memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE); + x_min = (aPoint.x < x_min) ? aPoint.x : x_min; + x_max = (aPoint.x > x_max) ? aPoint.x : x_max; + y_min = (aPoint.y < y_min) ? aPoint.y : y_min; + y_max = (aPoint.y > y_max) ? aPoint.y : y_max; + [coordinates addObject:NSStringFromPoint(NSMakePoint((CGFloat)aPoint.x, (CGFloat)aPoint.y))]; + ptr += POINT_DATA_SIZE; + } + return [NSDictionary dictionaryWithObjectsAndKeys: + [NSArray arrayWithObjects: + [NSNumber numberWithDouble:x_min], + [NSNumber numberWithDouble:x_max], + [NSNumber numberWithDouble:y_min], + [NSNumber numberWithDouble:y_max], + nil], @"bbox", + [NSArray arrayWithObjects:coordinates,nil], @"coordinates", + @"LINESTRING", @"type", + nil]; + break; + + case wkb_polygon: + memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32; + for (i=0; i < numberOfItems; i++) { + memcpy(&numberOfSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32; + for (j=0; j < numberOfSubItems; j++) { + memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE); + x_min = (aPoint.x < x_min) ? aPoint.x : x_min; + x_max = (aPoint.x > x_max) ? aPoint.x : x_max; + y_min = (aPoint.y < y_min) ? aPoint.y : y_min; + y_max = (aPoint.y > y_max) ? aPoint.y : y_max; + [subcoordinates addObject:NSStringFromPoint(NSMakePoint((CGFloat)aPoint.x, (CGFloat)aPoint.y))]; + ptr += POINT_DATA_SIZE; + } + [coordinates addObject:[[subcoordinates copy] autorelease]]; + [subcoordinates removeAllObjects]; + } + return [NSDictionary dictionaryWithObjectsAndKeys: + [NSArray arrayWithObjects: + [NSNumber numberWithDouble:x_min], + [NSNumber numberWithDouble:x_max], + [NSNumber numberWithDouble:y_min], + [NSNumber numberWithDouble:y_max], + nil], @"bbox", + coordinates, @"coordinates", + [NSNumber numberWithInt:srid], @"srid", + @"POLYGON", @"type", + nil]; + break; + + case wkb_multipoint: + memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32+WKB_HEADER_SIZE; + for (i=0; i < numberOfItems; i++) { + memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE); + x_min = (aPoint.x < x_min) ? aPoint.x : x_min; + x_max = (aPoint.x > x_max) ? aPoint.x : x_max; + y_min = (aPoint.y < y_min) ? aPoint.y : y_min; + y_max = (aPoint.y > y_max) ? aPoint.y : y_max; + [coordinates addObject:NSStringFromPoint(NSMakePoint((CGFloat)aPoint.x, (CGFloat)aPoint.y))]; + ptr += POINT_DATA_SIZE+WKB_HEADER_SIZE; + } + return [NSDictionary dictionaryWithObjectsAndKeys: + [NSArray arrayWithObjects: + [NSNumber numberWithDouble:x_min], + [NSNumber numberWithDouble:x_max], + [NSNumber numberWithDouble:y_min], + [NSNumber numberWithDouble:y_max], + nil], @"bbox", + coordinates, @"coordinates", + [NSNumber numberWithInt:srid], @"srid", + @"MULTIPOINT", @"type", + nil]; + break; + + case wkb_multilinestring: + memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32+WKB_HEADER_SIZE; + for (i=0; i < numberOfItems; i++) { + memcpy(&numberOfSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32; + for (j=0; j < numberOfSubItems; j++) { + memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE); + x_min = (aPoint.x < x_min) ? aPoint.x : x_min; + x_max = (aPoint.x > x_max) ? aPoint.x : x_max; + y_min = (aPoint.y < y_min) ? aPoint.y : y_min; + y_max = (aPoint.y > y_max) ? aPoint.y : y_max; + [subcoordinates addObject:NSStringFromPoint(NSMakePoint((CGFloat)aPoint.x, (CGFloat)aPoint.y))]; + ptr += POINT_DATA_SIZE; + } + ptr += WKB_HEADER_SIZE; + [coordinates addObject:[[subcoordinates copy] autorelease]]; + [subcoordinates removeAllObjects]; + } + return [NSDictionary dictionaryWithObjectsAndKeys: + [NSArray arrayWithObjects: + [NSNumber numberWithDouble:x_min], + [NSNumber numberWithDouble:x_max], + [NSNumber numberWithDouble:y_min], + [NSNumber numberWithDouble:y_max], + nil], @"bbox", + coordinates, @"coordinates", + [NSNumber numberWithInt:srid], @"srid", + @"MULTILINESTRING", @"type", + nil]; + break; + + case wkb_multipolygon: + memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32+WKB_HEADER_SIZE; + for (i=0; i < numberOfItems; i++) { + memcpy(&numberOfSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32; + for (j=0; j < numberOfSubItems; j++) { + memcpy(&numberOfSubSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32; + for (k=0; k < numberOfSubSubItems; k++) { + memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE); + x_min = (aPoint.x < x_min) ? aPoint.x : x_min; + x_max = (aPoint.x > x_max) ? aPoint.x : x_max; + y_min = (aPoint.y < y_min) ? aPoint.y : y_min; + y_max = (aPoint.y > y_max) ? aPoint.y : y_max; + [subcoordinates addObject:NSStringFromPoint(NSMakePoint((CGFloat)aPoint.x, (CGFloat)aPoint.y))]; + ptr += POINT_DATA_SIZE; + } + [coordinates addObject:[[subcoordinates copy] autorelease]]; + [subcoordinates removeAllObjects]; + } + ptr += WKB_HEADER_SIZE; + } + return [NSDictionary dictionaryWithObjectsAndKeys: + [NSArray arrayWithObjects: + [NSNumber numberWithDouble:x_min], + [NSNumber numberWithDouble:x_max], + [NSNumber numberWithDouble:y_min], + [NSNumber numberWithDouble:y_max], + nil], @"bbox", + coordinates, @"coordinates", + [NSNumber numberWithInt:srid], @"srid", + @"MULTIPOLYGON", @"type", + nil]; + break; + + case wkb_geometrycollection: + numberOfCollectionItems = geoBuffer[ptr]; + ptr += SIZEOF_STORED_UINT32; + + for (n=0; n < numberOfCollectionItems; n++) { + + byteOrder = (char)geoBuffer[ptr]; + + if (byteOrder != 0x1) + return nil; + + ptr++; + geoType = geoBuffer[ptr]; + ptr += SIZEOF_STORED_UINT32; + + switch(geoType) { + + case wkb_point: + memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE); + x_min = (aPoint.x < x_min) ? aPoint.x : x_min; + x_max = (aPoint.x > x_max) ? aPoint.x : x_max; + y_min = (aPoint.y < y_min) ? aPoint.y : y_min; + y_max = (aPoint.y > y_max) ? aPoint.y : y_max; + [pointcoordinates addObject:NSStringFromPoint(NSMakePoint((CGFloat)aPoint.x, (CGFloat)aPoint.y))]; + ptr += POINT_DATA_SIZE; + break; + + case wkb_linestring: + memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32; + for (i=0; i < numberOfItems; i++) { + memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE); + x_min = (aPoint.x < x_min) ? aPoint.x : x_min; + x_max = (aPoint.x > x_max) ? aPoint.x : x_max; + y_min = (aPoint.y < y_min) ? aPoint.y : y_min; + y_max = (aPoint.y > y_max) ? aPoint.y : y_max; + [linesubcoordinates addObject:NSStringFromPoint(NSMakePoint((CGFloat)aPoint.x, (CGFloat)aPoint.y))]; + ptr += POINT_DATA_SIZE; + } + [linecoordinates addObject:[[linesubcoordinates copy] autorelease]]; + [linesubcoordinates removeAllObjects]; + break; + + case wkb_polygon: + memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32; + for (i=0; i < numberOfItems; i++) { + memcpy(&numberOfSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32; + for (j=0; j < numberOfSubItems; j++) { + memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE); + x_min = (aPoint.x < x_min) ? aPoint.x : x_min; + x_max = (aPoint.x > x_max) ? aPoint.x : x_max; + y_min = (aPoint.y < y_min) ? aPoint.y : y_min; + y_max = (aPoint.y > y_max) ? aPoint.y : y_max; + [polygonsubcoordinates addObject:NSStringFromPoint(NSMakePoint((CGFloat)aPoint.x, (CGFloat)aPoint.y))]; + ptr += POINT_DATA_SIZE; + } + [polygoncoordinates addObject:[[polygonsubcoordinates copy] autorelease]]; + [polygonsubcoordinates removeAllObjects]; + } + break; + + case wkb_multipoint: + memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32+WKB_HEADER_SIZE; + for (i=0; i < numberOfItems; i++) { + memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE); + x_min = (aPoint.x < x_min) ? aPoint.x : x_min; + x_max = (aPoint.x > x_max) ? aPoint.x : x_max; + y_min = (aPoint.y < y_min) ? aPoint.y : y_min; + y_max = (aPoint.y > y_max) ? aPoint.y : y_max; + [pointcoordinates addObject:NSStringFromPoint(NSMakePoint((CGFloat)aPoint.x, (CGFloat)aPoint.y))]; + ptr += POINT_DATA_SIZE+WKB_HEADER_SIZE; + } + ptr -= WKB_HEADER_SIZE; + break; + + case wkb_multilinestring: + memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32+WKB_HEADER_SIZE; + for (i=0; i < numberOfItems; i++) { + memcpy(&numberOfSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32; + for (j=0; j < numberOfSubItems; j++) { + memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE); + x_min = (aPoint.x < x_min) ? aPoint.x : x_min; + x_max = (aPoint.x > x_max) ? aPoint.x : x_max; + y_min = (aPoint.y < y_min) ? aPoint.y : y_min; + y_max = (aPoint.y > y_max) ? aPoint.y : y_max; + [linesubcoordinates addObject:NSStringFromPoint(NSMakePoint((CGFloat)aPoint.x, (CGFloat)aPoint.y))]; + ptr += POINT_DATA_SIZE; + } + [linecoordinates addObject:[[linesubcoordinates copy] autorelease]]; + [linesubcoordinates removeAllObjects]; + ptr += WKB_HEADER_SIZE; + } + ptr -= WKB_HEADER_SIZE; + break; + + case wkb_multipolygon: + memcpy(&numberOfItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32+WKB_HEADER_SIZE; + for (i=0; i < numberOfItems; i++) { + memcpy(&numberOfSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32; + for (j=0; j < numberOfSubItems; j++) { + memcpy(&numberOfSubSubItems, &geoBuffer[ptr], SIZEOF_STORED_UINT32); + ptr += SIZEOF_STORED_UINT32; + for (k=0; k < numberOfSubSubItems; k++) { + memcpy(&aPoint, &geoBuffer[ptr], POINT_DATA_SIZE); + x_min = (aPoint.x < x_min) ? aPoint.x : x_min; + x_max = (aPoint.x > x_max) ? aPoint.x : x_max; + y_min = (aPoint.y < y_min) ? aPoint.y : y_min; + y_max = (aPoint.y > y_max) ? aPoint.y : y_max; + [polygonsubcoordinates addObject:NSStringFromPoint(NSMakePoint((CGFloat)aPoint.x, (CGFloat)aPoint.y))]; + ptr += POINT_DATA_SIZE; + } + [polygoncoordinates addObject:[[polygonsubcoordinates copy] autorelease]]; + [polygonsubcoordinates removeAllObjects]; + } + ptr += WKB_HEADER_SIZE; + } + ptr -= WKB_HEADER_SIZE; + break; + + default: + return nil; + } + } + return [NSDictionary dictionaryWithObjectsAndKeys: + [NSArray arrayWithObjects: + [NSNumber numberWithDouble:x_min], + [NSNumber numberWithDouble:x_max], + [NSNumber numberWithDouble:y_min], + [NSNumber numberWithDouble:y_max], + nil], @"bbox", + [NSArray arrayWithObjects:pointcoordinates, linecoordinates, polygoncoordinates, nil], @"coordinates", + @"GEOMETRYCOLLECTION", @"type", + nil]; + break; + + default: + return nil; + } + + return nil; +} + +/** + * Return the WKB type of the geoBuffer ie if buffer represents a POINT, LINESTRING, etc. + * according to stored wkbType in header file. It returns -1 if an error occurred. + */ +- (NSInteger)wkbType +{ + char byteOrder; + SInt32 geoType; + + NSUInteger ptr = BUFFER_START; // pointer to geoBuffer while parsing + + if (bufferLength < WKB_HEADER_SIZE) + return -1; + + byteOrder = (char)geoBuffer[ptr]; + + if (byteOrder != 0x1) + return -1; + + ptr++; + geoType = geoBuffer[ptr]; + + if (geoType > 0 && geoType < 8) + return geoType; + else + return -1; + +} + +/** + * Return the WKT type of the geoBuffer ie if buffer represents a POINT, LINESTRING, etc. + * according to stored wkbType in header file. It returns nil if an error occurred. + */ +- (NSString *)wktType +{ + switch ([self wkbType]) + { + case wkb_point: + return @"POINT"; + case wkb_linestring: + return @"LINESTRING"; + case wkb_polygon: + return @"POLYGON"; + case wkb_multipoint: + return @"MULTIPOINT"; + case wkb_multilinestring: + return @"MULTILINESTRING"; + case wkb_multipolygon: + return @"MULTIPOLYGON"; + case wkb_geometrycollection: + return @"GEOMETRYCOLLECTION"; + default: + return nil; + } + return nil; +} + +/** + * dealloc + */ +- (void)dealloc +{ + if (geoBuffer && bufferLength) free(geoBuffer); + [super dealloc]; +} + +@end diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Field Definitions.h b/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Field Definitions.h new file mode 100644 index 00000000..20e1ddc9 --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Field Definitions.h @@ -0,0 +1,38 @@ +// +// $Id$ +// +// Field Definitions.h +// SPMySQLFramework +// +// Created by Rowan Beentje (rowan.beent.je) on February 2, 2012 +// Copyright (c) 2012 Rowan Beentje. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at <http://code.google.com/p/sequel-pro/> + + +@interface SPMySQLResult (Field_Definitions) + +- (NSArray *)fieldDefinitions; + +@end diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Field Definitions.m b/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Field Definitions.m new file mode 100644 index 00000000..59e75d2f --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Field Definitions.m @@ -0,0 +1,557 @@ +// +// $Id$ +// +// Field Definitions.m +// SPMySQLFramework +// +// Created by Rowan Beentje (rowan.beent.je) on February 2, 2012 +// Copyright (c) 2012 Rowan Beentje. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "Field Definitions.h" + +@interface SPMySQLResult (Field_Definitions_Private_API) + +- (NSUInteger)_findCharsetMaxByteLengthPerCharForMySQLNumber:(NSUInteger)charsetnr; +- (NSString *)_charsetNameForMySQLNumber:(NSUInteger)charsetnr; +- (NSString *)_charsetCollationForMySQLNumber:(NSUInteger)charsetnr; +- (NSString *)_mysqlTypeToStringForType:(NSUInteger)type withCharsetNr:(NSUInteger)charsetnr withFlags:(NSUInteger)flags withLength:(unsigned long long)length; +- (NSString *)_mysqlTypeToGroupForType:(NSUInteger)type withCharsetNr:(NSUInteger)charsetnr withFlags:(NSUInteger)flags; + +@end + +// Import a private declaration from the SPMySQLResult file for use +@interface SPMySQLResult (Private_API) + +- (NSString *)_stringWithBytes:(const void *)bytes length:(NSUInteger)length; + +@end + +#define MAGIC_BINARY_CHARSET_NR 63 + +const SPMySQLResultCharset SPMySQLCharsetMap[] = +{ + {1, "big5","big5_chinese_ci", 1, 2}, + {3, "dec8", "dec8_swedisch_ci", 1, 1}, + {4, "cp850", "cp850_general_ci", 1, 1}, + {6, "hp8", "hp8_english_ci", 1, 1}, + {7, "koi8r", "koi8r_general_ci", 1, 1}, + {8, "latin1", "latin1_swedish_ci", 1, 1}, + {9, "latin2", "latin2_general_ci", 1, 1}, + {10, "swe7", "swe7_swedish_ci", 1, 1}, + {11, "ascii", "ascii_general_ci", 1, 1}, + {12, "ujis", "ujis_japanese_ci", 1, 3}, + {13, "sjis", "sjis_japanese_ci", 1, 2}, + {16, "hebrew", "hebrew_general_ci", 1, 1}, + {18, "tis620", "tis620_thai_ci", 1, 1}, + {19, "euckr", "euckr_korean_ci", 1, 2}, + {22, "koi8u", "koi8u_general_ci", 1, 1}, + {24, "gb2312", "gb2312_chinese_ci", 1, 2}, + {25, "greek", "greek_general_ci", 1, 1}, + {26, "cp1250", "cp1250_general_ci", 1, 1}, + {28, "gbk", "gbk_chinese_ci", 1, 2}, + {30, "latin5", "latin5_turkish_ci", 1, 1}, + {32, "armscii8", "armscii8_general_ci", 1, 1}, + {33, "utf8", "utf8_general_ci", 1, 3}, + {35, "ucs2", "ucs2_general_ci", 2, 2}, + {36, "cp866", "cp866_general_ci", 1, 1}, + {37, "keybcs2", "keybcs2_general_ci", 1, 1}, + {38, "macce", "macce_general_ci", 1, 1}, + {39, "macroman", "macroman_general_ci", 1, 1}, + {40, "cp852", "cp852_general_ci", 1, 1}, + {41, "latin7", "latin7_general_ci", 1, 1}, + {51, "cp1251", "cp1251_general_ci", 1, 1}, + {57, "cp1256", "cp1256_general_ci", 1, 1}, + {59, "cp1257", "cp1257_general_ci", 1, 1}, + {63, "binary", "binary", 1, 1}, + {92, "geostd8", "geostd8_general_ci", 1, 1}, + {95, "cp932", "cp932_japanese_ci", 1, 2}, + {97, "eucjpms", "eucjpms_japanese_ci", 1, 3}, + {2, "latin2", "latin2_czech_cs", 1, 1}, + {5, "latin1", "latin1_german_ci", 1, 1}, + {14, "cp1251", "cp1251_bulgarian_ci", 1, 1}, + {15, "latin1", "latin1_danish_ci", 1, 1}, + {17, "filename", "filename", 1, 5}, + {20, "latin7", "latin7_estonian_cs", 1, 1}, + {21, "latin2", "latin2_hungarian_ci", 1, 1}, + {23, "cp1251", "cp1251_ukrainian_ci", 1, 1}, + {27, "latin2", "latin2_croatian_ci", 1, 1}, + {29, "cp1257", "cp1257_lithunian_ci", 1, 1}, + {31, "latin1", "latin1_german2_ci", 1, 1}, + {34, "cp1250", "cp1250_czech_cs", 1, 1}, + {42, "latin7", "latin7_general_cs", 1, 1}, + {43, "macce", "macce_bin", 1, 1}, + {44, "cp1250", "cp1250_croatian_ci", 1, 1}, + {45, "utf8", "utf8_general_ci", 1, 1}, + {46, "utf8", "utf8_bin", 1, 1}, + {47, "latin1", "latin1_bin", 1, 1}, + {48, "latin1", "latin1_general_ci", 1, 1}, + {49, "latin1", "latin1_general_cs", 1, 1}, + {50, "cp1251", "cp1251_bin", 1, 1}, + {52, "cp1251", "cp1251_general_cs", 1, 1}, + {53, "macroman", "macroman_bin", 1, 1}, + {58, "cp1257", "cp1257_bin", 1, 1}, + {60, "armascii8", "armascii8_bin", 1, 1}, + {65, "ascii", "ascii_bin", 1, 1}, + {66, "cp1250", "cp1250_bin", 1, 1}, + {67, "cp1256", "cp1256_bin", 1, 1}, + {68, "cp866", "cp866_bin", 1, 1}, + {69, "dec8", "dec8_bin", 1, 1}, + {70, "greek", "greek_bin", 1, 1}, + {71, "hebew", "hebrew_bin", 1, 1}, + {72, "hp8", "hp8_bin", 1, 1}, + {73, "keybcs2", "keybcs2_bin", 1, 1}, + {74, "koi8r", "koi8r_bin", 1, 1}, + {75, "koi8u", "koi8u_bin", 1, 1}, + {77, "latin2", "latin2_bin", 1, 1}, + {78, "latin5", "latin5_bin", 1, 1}, + {79, "latin7", "latin7_bin", 1, 1}, + {80, "cp850", "cp850_bin", 1, 1}, + {81, "cp852", "cp852_bin", 1, 1}, + {82, "swe7", "swe7_bin", 1, 1}, + {93, "geostd8", "geostd8_bin", 1, 1}, + {83, "utf8", "utf8_bin", 1, 3}, + {84, "big5", "big5_bin", 1, 2}, + {85, "euckr", "euckr_bin", 1, 2}, + {86, "gb2312", "gb2312_bin", 1, 2}, + {87, "gbk", "gbk_bin", 1, 2}, + {88, "sjis", "sjis_bin", 1, 2}, + {89, "tis620", "tis620_bin", 1, 1}, + {90, "ucs2", "ucs2_bin", 2, 2}, + {91, "ujis", "ujis_bin", 1, 3}, + {94, "latin1", "latin1_spanish_ci", 1, 1}, + {96, "cp932", "cp932_bin", 1, 2}, + {99, "cp1250", "cp1250_polish_ci", 1, 1}, + {98, "eucjpms", "eucjpms_bin", 1, 3}, + {128, "ucs2", "ucs2_unicode_ci", 2, 2}, + {129, "ucs2", "ucs2_icelandic_ci", 2, 2}, + {130, "ucs2", "ucs2_latvian_ci", 2, 2}, + {131, "ucs2", "ucs2_romanian_ci", 2, 2}, + {132, "ucs2", "ucs2_slovenian_ci", 2, 2}, + {133, "ucs2", "ucs2_polish_ci", 2, 2}, + {134, "ucs2", "ucs2_estonian_ci", 2, 2}, + {135, "ucs2", "ucs2_spanish_ci", 2, 2}, + {136, "ucs2", "ucs2_swedish_ci", 2, 2}, + {137, "ucs2", "ucs2_turkish_ci", 2, 2}, + {138, "ucs2", "ucs2_czech_ci", 2, 2}, + {139, "ucs2", "ucs2_danish_ci", 2, 2}, + {140, "ucs2", "ucs2_lithunian_ci", 2, 2}, + {141, "ucs2", "ucs2_slovak_ci", 2, 2}, + {142, "ucs2", "ucs2_spanish2_ci", 2, 2}, + {143, "ucs2", "ucs2_roman_ci", 2, 2}, + {144, "ucs2", "ucs2_persian_ci", 2, 2}, + {145, "ucs2", "ucs2_esperanto_ci", 2, 2}, + {146, "ucs2", "ucs2_hungarian_ci", 2, 2}, + {147, "ucs2", "ucs2_sinhala_ci", 2, 2}, + {192, "utf8mb3", "utf8mb3_general_ci", 1, 3}, + {193, "utf8mb3", "utf8mb3_icelandic_ci", 1, 3}, + {194, "utf8mb3", "utf8mb3_latvian_ci", 1, 3}, + {195, "utf8mb3", "utf8mb3_romanian_ci", 1, 3}, + {196, "utf8mb3", "utf8mb3_slovenian_ci", 1, 3}, + {197, "utf8mb3", "utf8mb3_polish_ci", 1, 3}, + {198, "utf8mb3", "utf8mb3_estonian_ci", 1, 3}, + {119, "utf8mb3", "utf8mb3_spanish_ci", 1, 3}, + {200, "utf8mb3", "utf8mb3_swedish_ci", 1, 3}, + {201, "utf8mb3", "utf8mb3_turkish_ci", 1, 3}, + {202, "utf8mb3", "utf8mb3_czech_ci", 1, 3}, + {203, "utf8mb3", "utf8mb3_danish_ci", 1, 3}, + {204, "utf8mb3", "utf8mb3_lithunian_ci", 1, 3}, + {205, "utf8mb3", "utf8mb3_slovak_ci", 1, 3}, + {206, "utf8mb3", "utf8mb3_spanish2_ci", 1, 3}, + {207, "utf8mb3", "utf8mb3_roman_ci", 1, 3}, + {208, "utf8mb3", "utf8mb3_persian_ci", 1, 3}, + {209, "utf8mb3", "utf8mb3_esperanto_ci", 1, 3}, + {210, "utf8mb3", "utf8mb3_hungarian_ci", 1, 3}, + {211, "utf8mb3", "utf8mb3_sinhala_ci", 1, 3}, + {224, "utf8", "utf8_unicode_ci", 1, 3}, + {225, "utf8", "utf8_icelandic_ci", 1, 3}, + {226, "utf8", "utf8_latvian_ci", 1, 3}, + {227, "utf8", "utf8_romanian_ci", 1, 3}, + {228, "utf8", "utf8_slovenian_ci", 1, 3}, + {229, "utf8", "utf8_polish_ci", 1, 3}, + {230, "utf8", "utf8_estonian_ci", 1, 3}, + {231, "utf8", "utf8_spanish_ci", 1, 3}, + {232, "utf8", "utf8_swedish_ci", 1, 3}, + {233, "utf8", "utf8_turkish_ci", 1, 3}, + {234, "utf8", "utf8_czech_ci", 1, 3}, + {235, "utf8", "utf8_danish_ci", 1, 3}, + {236, "utf8", "utf8_lithuanian_ci", 1, 3}, + {237, "utf8", "utf8_slovak_ci", 1, 3}, + {238, "utf8", "utf8_spanish2_ci", 1, 3}, + {239, "utf8", "utf8_roman_ci", 1, 3}, + {240, "utf8", "utf8_persian_ci", 1, 3}, + {241, "utf8", "utf8_esperanto_ci", 1, 3}, + {242, "utf8", "utf8_hungarian_ci", 1, 3}, + {243, "utf8", "utf8_sinhala_ci", 1, 3}, + {254, "utf8mb3", "utf8mb3_general_cs", 1, 3}, + {0, NULL, NULL, 0, 0} +}; + +#pragma mark - + +@implementation SPMySQLResult (Field_Definitions) + +/** + * Return an array of NSDictionaries, each containing information about one of + * the columns in the result set. + * MySQL returns non-valid details as empty strings - these are converted to + * unset entries in the dictionary. + */ +- (NSArray *)fieldDefinitions +{ + NSUInteger i; + NSMutableArray *theFieldDefinitions = [NSMutableArray array]; + NSMutableDictionary *eachField; + MYSQL_FIELD mysqlField; + + for (i = 0; i < numberOfFields; i++) { + eachField = [NSMutableDictionary dictionary]; + mysqlField = fieldDefinitions[i]; + + // Record the original column position within the result set + [eachField setObject:[NSString stringWithFormat:@"%llu", (unsigned long long)i] forKey:@"datacolumnindex"]; + + // Record the column name, or alias if one is being used + [eachField setObject:[self _stringWithBytes:mysqlField.name length:mysqlField.name_length] forKey:@"name"]; + + // Record the original column name if using an alias + [eachField setObject:[self _stringWithBytes:mysqlField.org_name length:mysqlField.org_name_length] forKey:@"org_name"]; + + // If the column had an underlying table, record the table name, respecting aliases + if (mysqlField.table_length) { + [eachField setObject:[self _stringWithBytes:mysqlField.table length:mysqlField.table_length] forKey:@"table"]; + } + + // If the column had an underlying table, record the original table name, ignoring aliases + if (mysqlField.org_table_length) { + [eachField setObject:[self _stringWithBytes:mysqlField.org_table length:mysqlField.org_table_length] forKey:@"org_table"]; + } + + // If the column had an underlying database, record the database name + if (mysqlField.db_length) { + [eachField setObject:[self _stringWithBytes:mysqlField.db length:mysqlField.db_length] forKey:@"db"]; + } + + // Width of column (minimum real length in bytes) + [eachField setObject:[NSNumber numberWithUnsignedLongLong:mysqlField.length] forKey:@"byte_length"]; + + // Width of column (as in create) + // TODO: Discuss the logic of this with Hans-Jörg Bibiko; is this related to max_byte_length? + [eachField setObject:[NSNumber numberWithUnsignedLongLong:(mysqlField.length/[self _findCharsetMaxByteLengthPerCharForMySQLNumber:mysqlField.charsetnr])] forKey:@"char_length"]; + + // Max width (bytes) for selected set. Note that this will be 0 for streaming results. + [eachField setObject:[NSNumber numberWithUnsignedLongLong:mysqlField.max_length] forKey:@"max_byte_length"]; + + // Bit-flags that describe the field, in entirety and split out + [eachField setObject:[NSNumber numberWithUnsignedInt:mysqlField.flags] forKey:@"flags"]; + [eachField setObject:[NSNumber numberWithBool:(mysqlField.flags & NOT_NULL_FLAG) ? YES : NO] forKey:@"null"]; + [eachField setObject:[NSNumber numberWithBool:(mysqlField.flags & PRI_KEY_FLAG) ? YES : NO] forKey:@"PRI_KEY_FLAG"]; + [eachField setObject:[NSNumber numberWithBool:(mysqlField.flags & UNIQUE_KEY_FLAG) ? YES : NO] forKey:@"UNIQUE_KEY_FLAG"]; + [eachField setObject:[NSNumber numberWithBool:(mysqlField.flags & MULTIPLE_KEY_FLAG) ? YES : NO] forKey:@"MULTIPLE_KEY_FLAG"]; + [eachField setObject:[NSNumber numberWithBool:(mysqlField.flags & BLOB_FLAG) ? YES : NO] forKey:@"BLOB_FLAG"]; + [eachField setObject:[NSNumber numberWithBool:(mysqlField.flags & UNSIGNED_FLAG) ? YES : NO] forKey:@"UNSIGNED_FLAG"]; + [eachField setObject:[NSNumber numberWithBool:(mysqlField.flags & ZEROFILL_FLAG) ? YES : NO] forKey:@"ZEROFILL_FLAG"]; + [eachField setObject:[NSNumber numberWithBool:(mysqlField.flags & BINARY_FLAG) ? YES : NO] forKey:@"BINARY_FLAG"]; + [eachField setObject:[NSNumber numberWithBool:(mysqlField.flags & ENUM_FLAG) ? YES : NO] forKey:@"ENUM_FLAG"]; + [eachField setObject:[NSNumber numberWithBool:(mysqlField.flags & AUTO_INCREMENT_FLAG) ? YES : NO] forKey:@"AUTO_INCREMENT_FLAG"]; + [eachField setObject:[NSNumber numberWithBool:(mysqlField.flags & SET_FLAG) ? YES : NO] forKey:@"SET_FLAG"]; + [eachField setObject:[NSNumber numberWithBool:(mysqlField.flags & NUM_FLAG) ? YES : NO] forKey:@"NUM_FLAG"]; + [eachField setObject:[NSNumber numberWithBool:(mysqlField.flags & PART_KEY_FLAG) ? YES : NO] forKey:@"PART_KEY_FLAG"]; + + // For numeric fields, record the number of decimals + [eachField setObject:[NSNumber numberWithUnsignedInteger:mysqlField.decimals] forKey:@"decimals"]; + + // Character set details + [eachField setObject:[NSNumber numberWithUnsignedInteger:mysqlField.charsetnr] forKey:@"charsetnr"]; + [eachField setObject:[self _charsetNameForMySQLNumber:mysqlField.charsetnr] forKey:@"charset_name"]; + [eachField setObject:[self _charsetCollationForMySQLNumber:mysqlField.charsetnr] forKey:@"charset_collation"]; + + /* Table type */ + [eachField setObject:[self _mysqlTypeToStringForType:mysqlField.type + withCharsetNr:mysqlField.charsetnr + withFlags:mysqlField.flags + withLength:mysqlField.length + ] forKey:@"type"]; + + /* Table type group*/ + [eachField setObject:[self _mysqlTypeToGroupForType:mysqlField.type + withCharsetNr:mysqlField.charsetnr + withFlags:mysqlField.flags + ] forKey:@"typegrouping"]; + + [theFieldDefinitions addObject:eachField]; + } + + return theFieldDefinitions; +} + +@end + +#pragma mark - +#pragma mark Field defintion internals + +@implementation SPMySQLResult (Field_Definitions_Private_API) + +/** + * Return the maximum byte length to store a char by using + * a specific mysql_charsetnr + */ +- (NSUInteger)_findCharsetMaxByteLengthPerCharForMySQLNumber:(NSUInteger)charsetnr +{ + const SPMySQLResultCharset *c = SPMySQLCharsetMap; + + do { + if (c->nr == charsetnr) + return c->char_maxlen; + ++c; + } while (c[0].nr != 0); + + return 1; +} + +/** + * Convert a mysql_charsetnr into a charset name as string + */ +- (NSString *)_charsetNameForMySQLNumber:(NSUInteger)charsetnr +{ + const SPMySQLResultCharset *c = SPMySQLCharsetMap; + + do { + if (c->nr == charsetnr) + return [NSString stringWithCString:c->name encoding:stringEncoding]; + ++c; + } while (c[0].nr != 0); + + return @"UNKNOWN"; +} + +/** + * Convert a mysql_charsetnr into a collation name as string + */ +- (NSString *)_charsetCollationForMySQLNumber:(NSUInteger)charsetnr +{ + const SPMySQLResultCharset *c = SPMySQLCharsetMap; + + do { + if (c->nr == charsetnr) + return [NSString stringWithCString:c->collation encoding:stringEncoding]; + ++c; + } while (c[0].nr != 0); + + return @"UNKNOWN"; +} + +/** + * Convert a mysql_type to a string + */ +- (NSString *)_mysqlTypeToStringForType:(NSUInteger)type withCharsetNr:(NSUInteger)charsetnr withFlags:(NSUInteger)flags withLength:(unsigned long long)length +{ + + switch (type) { + + case FIELD_TYPE_BIT: + return @"BIT"; + + case MYSQL_TYPE_DECIMAL: + case MYSQL_TYPE_NEWDECIMAL: + return @"DECIMAL"; + + case MYSQL_TYPE_TINY: + return @"TINYINT"; + + case MYSQL_TYPE_SHORT: + return @"SMALLINT"; + + case MYSQL_TYPE_LONG: + return @"INT"; + + case MYSQL_TYPE_FLOAT: + return @"FLOAT"; + + case MYSQL_TYPE_DOUBLE: + return @"DOUBLE"; + + case MYSQL_TYPE_NULL: + return @"NULL"; + + case MYSQL_TYPE_TIMESTAMP: + return @"TIMESTAMP"; + + case MYSQL_TYPE_LONGLONG: + return @"BIGINT"; + + case MYSQL_TYPE_INT24: + return @"MEDIUMINT"; + + case MYSQL_TYPE_DATE: + return @"DATE"; + + case MYSQL_TYPE_TIME: + return @"TIME"; + + case MYSQL_TYPE_DATETIME: + return @"DATETIME"; + + case MYSQL_TYPE_TINY_BLOB:// should no appear over the wire + case MYSQL_TYPE_MEDIUM_BLOB:// should no appear over the wire + case MYSQL_TYPE_LONG_BLOB:// should no appear over the wire + case MYSQL_TYPE_BLOB: + { + BOOL isBlob = (charsetnr == MAGIC_BINARY_CHARSET_NR); + switch (length/[self _findCharsetMaxByteLengthPerCharForMySQLNumber:charsetnr]) { + case 255: return isBlob? @"TINYBLOB":@"TINYTEXT"; + case 65535: return isBlob? @"BLOB":@"TEXT"; + case 16777215: return isBlob? @"MEDIUMBLOB":@"MEDIUMTEXT"; + case 4294967295: return isBlob? @"LONGBLOB":@"LONGTEXT"; + default: + switch (length) { + case 255: return isBlob? @"TINYBLOB":@"TINYTEXT"; + case 65535: return isBlob? @"BLOB":@"TEXT"; + case 16777215: return isBlob? @"MEDIUMBLOB":@"MEDIUMTEXT"; + case 4294967295: return isBlob? @"LONGBLOB":@"LONGTEXT"; + default: + return @"UNKNOWN"; + } + } + } + + case MYSQL_TYPE_VAR_STRING: + if (flags & ENUM_FLAG) { + return @"ENUM"; + } + if (flags & SET_FLAG) { + return @"SET"; + } + if (charsetnr == MAGIC_BINARY_CHARSET_NR) { + return @"VARBINARY"; + } + return @"VARCHAR"; + + case MYSQL_TYPE_STRING: + if (flags & ENUM_FLAG) { + return @"ENUM"; + } + if (flags & SET_FLAG) { + return @"SET"; + } + if ((flags & BINARY_FLAG) && charsetnr == MAGIC_BINARY_CHARSET_NR) { + return @"BINARY"; + } + return @"CHAR"; + + case MYSQL_TYPE_ENUM: + /* This should never happen */ + return @"ENUM"; + + case MYSQL_TYPE_YEAR: + return @"YEAR"; + + case MYSQL_TYPE_SET: + /* This should never happen */ + return @"SET"; + + case MYSQL_TYPE_GEOMETRY: + return @"GEOMETRY"; + + default: + return @"UNKNOWN"; + } +} + +/** + * Merge mysql_types into type groups + */ +- (NSString *)_mysqlTypeToGroupForType:(NSUInteger)type withCharsetNr:(NSUInteger)charsetnr withFlags:(NSUInteger)flags +{ + switch(type){ + + case FIELD_TYPE_BIT: + return @"bit"; + + case MYSQL_TYPE_TINY: + case MYSQL_TYPE_SHORT: + case MYSQL_TYPE_LONG: + case MYSQL_TYPE_LONGLONG: + case MYSQL_TYPE_INT24: + return @"integer"; + + case MYSQL_TYPE_FLOAT: + case MYSQL_TYPE_DOUBLE: + case MYSQL_TYPE_DECIMAL: + case MYSQL_TYPE_NEWDECIMAL: + return @"float"; + + case MYSQL_TYPE_YEAR: + case MYSQL_TYPE_DATETIME: + case MYSQL_TYPE_TIME: + case MYSQL_TYPE_DATE: + case MYSQL_TYPE_TIMESTAMP: + return @"date"; + + case MYSQL_TYPE_VAR_STRING: + if (flags & ENUM_FLAG) { + return @"enum"; + } + if (flags & SET_FLAG) { + return @"enum"; + } + if (charsetnr == MAGIC_BINARY_CHARSET_NR) { + return @"binary"; + } + return @"string"; + + case MYSQL_TYPE_STRING: + if (flags & ENUM_FLAG) { + return @"enum"; + } + if (flags & SET_FLAG) { + return @"enum"; + } + if ((flags & BINARY_FLAG) && charsetnr == MAGIC_BINARY_CHARSET_NR) { + return @"binary"; + } + return @"string"; + + case MYSQL_TYPE_TINY_BLOB: // should no appear over the wire + case MYSQL_TYPE_MEDIUM_BLOB: // should no appear over the wire + case MYSQL_TYPE_LONG_BLOB: // should no appear over the wire + case MYSQL_TYPE_BLOB: + { + if (charsetnr == MAGIC_BINARY_CHARSET_NR) { + return @"blobdata"; + } else { + return @"textdata"; + } + } + + case MYSQL_TYPE_GEOMETRY: + return @"geometry"; + + default: + return @"blobdata"; + } +} + +@end
\ No newline at end of file diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLResult.h b/Frameworks/SPMySQLFramework/Source/SPMySQLResult.h new file mode 100644 index 00000000..baddf8aa --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLResult.h @@ -0,0 +1,120 @@ +// +// $Id$ +// +// SPMySQLResult.h +// SPMySQLFramework +// +// Created by Rowan Beentje (rowan.beent.je) on January 26, 2012 +// Copyright (c) 2012 Rowan Beentje. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at <http://code.google.com/p/sequel-pro/> + + +typedef enum { + SPMySQLResultFieldAsUnhandled = 0, + SPMySQLResultFieldAsString = 1, + SPMySQLResultFieldAsStringOrBlob = 2, + SPMySQLResultFieldAsBlob = 3, + SPMySQLResultFieldAsBit = 4, + SPMySQLResultFieldAsGeometry = 5, + SPMySQLResultFieldAsNull = 6 +} SPMySQLResultFieldProcessor; + +@interface SPMySQLResult : NSObject <NSFastEnumeration> { + + // Wrapped MySQL result set and its encoding + struct st_mysql_res *resultSet; + NSStringEncoding stringEncoding; + + // Number of fields in the result set, and the field names and information + NSUInteger numberOfFields; + struct st_mysql_field *fieldDefinitions; + unsigned int *fieldTypes; + NSString **fieldNames; + + // Number of rows in the result set and an internal data position counter + unsigned long long numberOfRows; + unsigned long long currentRowIndex; + + // How long it took to execute the query that produced this result + double queryExecutionTime; + + // The target result set type for fast enumeration and unspecified row retrieval + SPMySQLResultRowType defaultRowReturnType; + + // Whether all data should be returned as strings - useful for working with some older server types + BOOL returnDataAsStrings; +} + +// Master init method +- (id)initWithMySQLResult:(void *)theResult stringEncoding:(NSStringEncoding)theStringEncoding; + +// Result set information +- (NSUInteger)numberOfFields; +- (unsigned long long)numberOfRows; + +// Column information +- (NSArray *)fieldNames; + +// Data retrieval (note that fast enumeration is also supported, using instance-default format) +- (void)seekToRow:(unsigned long long)targetRow; +- (id)getRow; +- (NSArray *)getRowAsArray; +- (NSDictionary *)getRowAsDictionary; +- (id)getRowAsType:(SPMySQLResultRowType)theType; + +// Data conversion ++ (NSString *)bitStringWithBytes:(const char *)bytes length:(NSUInteger)length padToLength:(NSUInteger)padLength; + +#pragma mark - +#pragma mark Synthesized properties + +/** + * Set whether the result should return data types as strings. This may be useful + * for queries where the result may be returned in either string or data form, but + * will be converted to string for display and use anyway. + * Note that certain MySQL versions also return data types for strings - eg SHOW + * commands like SHOW CREATE TABLE or SHOW VARIABLES, and this conversion can be + * necessary there. + */ +@property (readwrite, assign) BOOL returnDataAsStrings; + +@property (readwrite, assign) SPMySQLResultRowType defaultRowReturnType; + +@end + +/** + * Set up a static function to allow fast calling with cached selectors + */ +static inline id SPMySQLResultGetRow(SPMySQLResult* self, SPMySQLResultRowType rowType) +{ + typedef id (*SPMySQLResultGetRowMethodPtr)(SPMySQLResult*, SEL, SPMySQLResultRowType); + static SPMySQLResultGetRowMethodPtr cachedMethodPointer; + static SEL cachedSelector; + + if (!cachedSelector) cachedSelector = @selector(getRowAsType:); + if (!cachedMethodPointer) cachedMethodPointer = (SPMySQLResultGetRowMethodPtr)[self methodForSelector:cachedSelector]; + + return cachedMethodPointer(self, cachedSelector, rowType); +}
\ No newline at end of file diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLResult.m b/Frameworks/SPMySQLFramework/Source/SPMySQLResult.m new file mode 100644 index 00000000..b110958d --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLResult.m @@ -0,0 +1,459 @@ +// +// $Id$ +// +// SPMySQLResult.m +// SPMySQLFramework +// +// Created by Rowan Beentje (rowan.beent.je) on January 26, 2012 +// Copyright (c) 2012 Rowan Beentje. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPMySQLResult.h" +#import "SPMySQL Private APIs.h" + +static SPMySQLResultFieldProcessor fieldProcessingMap[256]; + +@implementation SPMySQLResult + +#pragma mark - +#pragma mark Synthesized properties + +@synthesize returnDataAsStrings; +@synthesize defaultRowReturnType; + +#pragma mark - +#pragma mark Setup and teardown + +/** + * In the one-off class initialisation, set up the result processing map + */ ++ (void)initialize +{ + + // Go through the list of enum_field_types in mysql_com.h, mapping each to the method for + // processing that result set. + fieldProcessingMap[MYSQL_TYPE_DECIMAL] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_TINY] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_SHORT] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_LONG] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_FLOAT] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_DOUBLE] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_NULL] = SPMySQLResultFieldAsNull; + fieldProcessingMap[MYSQL_TYPE_TIMESTAMP] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_LONGLONG] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_INT24] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_DATE] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_TIME] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_DATETIME] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_YEAR] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_NEWDATE] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_VARCHAR] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_BIT] = SPMySQLResultFieldAsBit; + fieldProcessingMap[MYSQL_TYPE_NEWDECIMAL] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_ENUM] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_SET] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_TINY_BLOB] = SPMySQLResultFieldAsBlob; + fieldProcessingMap[MYSQL_TYPE_MEDIUM_BLOB] = SPMySQLResultFieldAsBlob; + fieldProcessingMap[MYSQL_TYPE_LONG_BLOB] = SPMySQLResultFieldAsBlob; + fieldProcessingMap[MYSQL_TYPE_BLOB] = SPMySQLResultFieldAsBlob; + fieldProcessingMap[MYSQL_TYPE_VAR_STRING] = SPMySQLResultFieldAsStringOrBlob; + fieldProcessingMap[MYSQL_TYPE_STRING] = SPMySQLResultFieldAsStringOrBlob; + fieldProcessingMap[MYSQL_TYPE_GEOMETRY] = SPMySQLResultFieldAsGeometry; + fieldProcessingMap[MYSQL_TYPE_DECIMAL] = SPMySQLResultFieldAsString; +} + +/** + * Prevent SPMySQLResults from being init'd normally. + */ +- (id)init +{ + [NSException raise:NSInternalInconsistencyException format:@"SPMySQLResults should not be init'd directly; use initWithMySQLResult:stringEncoding: instead."]; + return nil; +} + +/** + * Standard init method, constructing the SPMySQLResult around a MySQL + * result pointer and the encoding to use when working with the data. + */ +- (id)initWithMySQLResult:(void *)theResult stringEncoding:(NSStringEncoding)theStringEncoding +{ + + // If no result set was passed in, return nil. + if (!theResult) return nil; + + if ((self = [super init])) { + stringEncoding = theStringEncoding; + queryExecutionTime = -1; + + // Get the result set and cache the number of fields and number of rows + resultSet = theResult; + numberOfFields = mysql_num_fields(resultSet); + numberOfRows = mysql_num_rows(resultSet); + currentRowIndex = 0; + + // Cache the field definitions and build up an array of cached field names and types + fieldDefinitions = mysql_fetch_fields(resultSet); + fieldNames = malloc(sizeof(NSString *) * numberOfFields); + fieldTypes = malloc(sizeof(unsigned int) * numberOfFields); + for (NSUInteger i = 0; i < numberOfFields; i++) { + MYSQL_FIELD aField = fieldDefinitions[i]; + fieldNames[i] = [[self _stringWithBytes:aField.name length:aField.name_length] retain]; + fieldTypes[i] = aField.type; + } + + defaultRowReturnType = SPMySQLResultRowAsDictionary; + } + + return self; +} + +- (void)dealloc +{ + mysql_free_result(resultSet); + + for (NSUInteger i = 0; i < numberOfFields; i++) { + [fieldNames[i] release]; + } + free(fieldNames); + free(fieldTypes); + + [super dealloc]; +} + +#pragma mark - +#pragma mark Result set information + +/** + * Return the number of fields in the result set. + */ +- (NSUInteger)numberOfFields +{ + return numberOfFields; +} + +/** + * Return the number of data rows in the result set. + */ +- (unsigned long long)numberOfRows +{ + return numberOfRows; +} + +/** + * Return how long the original query took to execute - including connection lag! + */ +- (double)queryExecutionTime +{ + return queryExecutionTime; +} + +#pragma mark - +#pragma mark Column information + +/** + * Retrieve the field names for the result set, as an NSArray of NSStrings. + */ +- (NSArray *)fieldNames +{ + return [NSArray arrayWithObjects:fieldNames count:numberOfFields]; +} + +/** + * For field definitions, see Result Categories/Field Definitions.h/m + */ + +#pragma mark - +#pragma mark Data retrieval + +/** + * Jump to a specified row in the result set; when the result set is initialised, + * the internal pointer automatically starts at 0. + */ +- (void)seekToRow:(unsigned long long)targetRow +{ + if (targetRow == currentRowIndex) return; + + if (targetRow >= numberOfRows) { + targetRow = numberOfRows - 1; + } + + mysql_data_seek(resultSet, targetRow); + currentRowIndex = targetRow; +} + +/** + * Retrieve the next row in the result set, using the internal pointer, in the + * instance-specified setDefaultRowReturnType: row format (defaulting to NSDictionary). + * If there are no rows remaining, returns nil. + */ +- (id)getRow +{ + return SPMySQLResultGetRow(self, SPMySQLResultRowAsDefault); +} + +/** + * Retrieve the next row in the result set, using the internal pointer, in the + * instance-specified setDefaultRowReturnType: row format (defaulting to NSDictionary). + * If there are no rows remaining, returns nil. + */ +- (NSArray *)getRowAsArray +{ + return SPMySQLResultGetRow(self, SPMySQLResultRowAsArray); +} + +/** + * Retrieve the next row in the result set, using the internal pointer, in the + * instance-specified setDefaultRowReturnType: row format (defaulting to NSDictionary). + * If there are no rows remaining, returns nil. + */ +- (NSDictionary *)getRowAsDictionary +{ + return SPMySQLResultGetRow(self, SPMySQLResultRowAsDictionary); +} + +/** + * Retrieve the next row in the result set, using the internal pointer, in the specified + * return format. + * If there are no rows remaining in the current iteration, returns nil. + */ +- (id)getRowAsType:(SPMySQLResultRowType)theType +{ + MYSQL_ROW theRow; + unsigned long *theRowDataLengths; + id theReturnData; + + // Retrieve the row in MySQL format, and the length of the data within the row + theRow = mysql_fetch_row(resultSet); + theRowDataLengths = mysql_fetch_lengths(resultSet); + + // If no row was returned, likely at the end of the result set - return nil + if (!theRow) return nil; + + // If the target type was unspecified, use the instance default + if (theType == SPMySQLResultRowAsDefault) theType = defaultRowReturnType; + + // Set up the return data as appropriate + if (theType == SPMySQLResultRowAsArray) { + theReturnData = [NSMutableArray arrayWithCapacity:numberOfFields]; + } else { + theReturnData = [NSMutableDictionary dictionaryWithCapacity:numberOfFields]; + } + + // Convert each of the cells in the row in turn + for (NSUInteger i = 0; i < numberOfFields; i++) { + id cellData = SPMySQLResultGetObject(self, theRow[i], theRowDataLengths[i], fieldTypes[i], i); + + // If object creation failed, display a null + if (!cellData) cellData = [NSNull null]; + + // Add to the result array/dictionary + if (theType == SPMySQLResultRowAsArray) { + [(NSMutableArray *)theReturnData addObject:cellData]; + } else { + [(NSMutableDictionary *)theReturnData setObject:cellData forKey:fieldNames[i]]; + } + } + + // Increment the row pointer index and set to NSNotFound if the end of the result set has + // been reached + currentRowIndex++; + if (currentRowIndex > numberOfRows) currentRowIndex = NSNotFound; + + return theReturnData; +} + +#pragma mark - +#pragma mark Data retrieval for fast enumeration + +/** + * Implement the fast enumeration endpoint. Rows for fast enumeration are retrieved in + * the instance default, as specified in setDefaultRowReturnType: or defaulting to + * NSDictionary. + */ +- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len +{ + + // If the start index is out of bounds, return 0 to indicate end of results + if (state->state >= numberOfRows) return 0; + + // Sync up the MySQL pointer position with the requested state if necessary + if (state->state != currentRowIndex) [self seekToRow:state->state]; + + // Determine how many objects to return - 128, len, or all items remaining + NSUInteger itemsToReturn = 128; + if (len < 128) itemsToReturn = len; + if (numberOfRows - state->state < itemsToReturn) { + itemsToReturn = (unsigned long)(numberOfRows - state->state); + } + + // Loop through the rows and add them to the result stack + NSUInteger i; + for (i = 0; i < itemsToReturn; i++) { + stackbuf[i] = SPMySQLResultGetRow(self, SPMySQLResultRowAsDefault); + } + + state->state += itemsToReturn; + state->itemsPtr = stackbuf; + state->mutationsPtr = (unsigned long *)self; + + return itemsToReturn; +} + +#pragma mark - +#pragma mark Data conversion + +/** + * Provides a binary representation of the supplied bytes as a returned NSString. + * The resulting binary representation will be zero-padded according to the supplied + * field length. + */ ++ (NSString *)bitStringWithBytes:(const char *)bytes length:(NSUInteger)length padToLength:(NSUInteger)padLength +{ + if (bytes == NULL) return nil; + + NSUInteger i = 0; + length--; + padLength--; + + // Generate a C string representation of the binary data + char *cStringBuffer = malloc(length + 1); + while (i <= padLength) { + cStringBuffer[padLength - i++] = ( (bytes[length - (i >> 3)] >> (i & 0x7)) & 1 ) ? '1' : '0'; + } + cStringBuffer[padLength+1] = '\0'; + + // Convert to a string + NSString *returnString = [NSString stringWithUTF8String:cStringBuffer]; + + // Free up memory and return + free(cStringBuffer); + return returnString; +} + +@end + +#pragma mark - +#pragma mark Result set internals + +@implementation SPMySQLResult (Private_API) + +/** + * Support internal string conversions which take a supplied byte sequence and length + * and convert them to an NSString using the instance encoding. Will preserve nul + * characters within the string. + */ +- (id)_stringWithBytes:(const void *)bytes length:(NSUInteger)length +{ + return [[[NSString alloc] initWithBytes:bytes length:length encoding:stringEncoding] autorelease]; +} + +/** + * Allow setting the execution time for the original query (including connection lag) + * so it can be requested later without relying on connection state. + */ +- (void)_setQueryExecutionTime:(double)theExecutionTime +{ + queryExecutionTime = theExecutionTime; +} + +/** + * Core data conversion function, taking C data provided by MySQL and converting + * to an appropriate return type. + * Note that the data passed in currently is *not* nul-terminated for fast + * streaming results, which is safe for the current implementation but should be + * kept in mind for future changes. + */ +- (id)_getObjectFromBytes:(char *)bytes ofLength:(NSUInteger)length fieldType:(unsigned int)fieldType fieldDefinitionIndex:(NSUInteger)fieldIndex +{ + + // A NULL pointer for the data indicates a null value; return a NSNull object. + if (bytes == NULL) return [NSNull null]; + + // Determine the field processor to use + SPMySQLResultFieldProcessor dataProcessor = fieldProcessingMap[fieldType]; + + // Switch the method to process the cell data based on the field type mapping. + // Do this in two passes: the first as logic may cause a change in processor required. + switch (dataProcessor) { + + // STRING and VAR_STRING types may be strings or binary types; check the binary flag + case SPMySQLResultFieldAsStringOrBlob: + if (fieldDefinitions[fieldIndex].flags & BINARY_FLAG) { + dataProcessor = SPMySQLResultFieldAsBlob; + } + break; + + // Blob types may be automatically be converted to strings, or may be non-binary + case SPMySQLResultFieldAsBlob: + if (!(fieldDefinitions[fieldIndex].flags & BINARY_FLAG)) { + dataProcessor = SPMySQLResultFieldAsString; + } + break; + + // In most cases, use the original data processor. + default: + break; + } + + // If this instance is set to convert all data as strings, alter the processor. + if (returnDataAsStrings && dataProcessor == SPMySQLResultFieldAsBlob) { + dataProcessor = SPMySQLResultFieldAsString; + } + + // Now switch the processing method again to actually process the data. + switch (dataProcessor) { + + // Convert string types using a method that will preserve any nul characters + // within the string + case SPMySQLResultFieldAsString: + case SPMySQLResultFieldAsStringOrBlob: + return [[[NSString alloc] initWithBytes:bytes length:length encoding:stringEncoding] autorelease]; + + // Convert BLOB types to NSData + case SPMySQLResultFieldAsBlob: + return [NSData dataWithBytes:bytes length:length]; + + // For Geometry types, use a special Geometry object to handle their complexity + case SPMySQLResultFieldAsGeometry: + return [SPMySQLGeometryData dataWithBytes:bytes length:length]; + + // For bit fields, get a zero-padded representation of the data + case SPMySQLResultFieldAsBit: + return [SPMySQLResult bitStringWithBytes:bytes length:length padToLength:fieldDefinitions[fieldIndex].length]; + + // Convert null types to NSNulls + case SPMySQLResultFieldAsNull: + return [NSNull null]; + + case SPMySQLResultFieldAsUnhandled: + NSLog(@"SPMySQLResult processing encountered an unknown field type (%d), falling back to NSData handling", fieldType); + return [NSData dataWithBytes:bytes length:length]; + } + + [NSException raise:NSInternalInconsistencyException format:@"Unhandled field type when processing SPMySQLResults"]; + return nil; +} + +@end diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResult.h b/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResult.h new file mode 100644 index 00000000..5abb85db --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResult.h @@ -0,0 +1,54 @@ +// +// $Id$ +// +// SPMySQLStreamingResult.h +// SPMySQLFramework +// +// Created by Rowan Beentje (rowan.beent.je) on February 18, 2012 +// Copyright (c) 2012 Rowan Beentje. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at <http://code.google.com/p/sequel-pro/> + +@interface SPMySQLStreamingResult : SPMySQLResult { + + // Keep a link to the parent connection for locking purposes + SPMySQLConnection *parentConnection; + + // Streaming result information and tracking + BOOL connectionUnlocked; + BOOL dataDownloaded; + + // Counts and memory length tracking + NSUInteger downloadedRowCount; + + IMP isConnectedPtr; + SEL isConnectedSelector; +} + +- (id)initWithMySQLResult:(void *)theResult stringEncoding:(NSStringEncoding)theStringEncoding connection:(SPMySQLConnection *)theConnection; + +// Allow result fetching to be cancelled +- (void)cancelResultLoad; + +@end diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResult.m b/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResult.m new file mode 100644 index 00000000..b19e5356 --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResult.m @@ -0,0 +1,246 @@ +// +// $Id$ +// +// SPMySQLStreamingResult.m +// SPMySQLFramework +// +// Created by Rowan Beentje (rowan.beent.je) on February 18, 2012 +// Copyright (c) 2012 Rowan Beentje. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPMySQLStreamingResult.h" +#import "SPMySQL Private APIs.h" + + +/** + * This type of streaming result allows each row to be accessed on-demand; this can + * be dangerous as it means a SELECT will tie up the server for longer, as for MyISAM + * tables updates (and subsequent reads) must block while a SELECT is still running. + * However this can be useful for certain processes such as working with very large + * tables to keep memory usage low. + */ + +@implementation SPMySQLStreamingResult + +#pragma mark - + +/** + * Prevent SPMySQLStreamingResults from being init'd as SPMySQLResults. + */ +- (id)initWithMySQLResult:(void *)theResult stringEncoding:(NSStringEncoding)theStringEncoding +{ + [NSException raise:NSInternalInconsistencyException format:@"SPMySQLFullStreamingResults should not be init'd as SPMySQLResults; use initWithMySQLResult:stringEncoding:connection:withFullStreaming: instead."]; + return nil; +} + +/** + * Standard init method, constructing the SPMySQLStreamingResult around a MySQL + * result pointer and the encoding to use when working with the data. + * As opposed to SPMySQLResult, defaults to returning rows as arrays, as the result + * sets are likely to be larger and processed in loops. + */ +- (id)initWithMySQLResult:(void *)theResult stringEncoding:(NSStringEncoding)theStringEncoding connection:(SPMySQLConnection *)theConnection +{ + + // If no result set was passed in, return nil. + if (!theResult) return nil; + + if ((self = [super initWithMySQLResult:theResult stringEncoding:theStringEncoding])) { + parentConnection = theConnection; + numberOfRows = NSNotFound; + + // Start with no rows downloaded + downloadedRowCount = 0; + dataDownloaded = NO; + connectionUnlocked = NO; + + // Cache the isConnected selector and pointer for fast connection checks + isConnectedSelector = @selector(isConnected); + isConnectedPtr = [parentConnection methodForSelector:isConnectedSelector]; + + // Default to returning rows as arrays + defaultRowReturnType = SPMySQLResultRowAsArray; + } + + return self; +} + +/** + * Deallocate the result and ensure the parent connection is unlocked for further use. + */ +- (void)dealloc +{ + + // Ensure all data is processed and the parent connection is unlocked + [self cancelResultLoad]; + + // Throw an exception if in invalid state + if (!connectionUnlocked) { + [parentConnection _unlockConnection]; + [NSException raise:NSInternalInconsistencyException format:@"Parent connection remains locked after SPMySQLStreamingResult use"]; + } + + [super dealloc]; +} + +#pragma mark - +#pragma mark Result set information + +/** + * Override the return of the number of rows in the data set. If this is used before the + * data is fully downloaded, the number of results is still unknown (the server may still + * be seeking/matching), so return NSNotFound; otherwise the number of rows is returned. + */ +- (unsigned long long)numberOfRows +{ + if (!dataDownloaded) return NSNotFound; + + return downloadedRowCount; +} + +#pragma mark - +#pragma mark Data retrieval + +/** + * Override seeking behaviour: seeking cannot be used in streaming result sets. + */ +- (void)seekToRow:(unsigned long long)targetRow +{ + [NSException raise:NSInternalInconsistencyException format:@"Seeking is not supported in streaming SPMySQL result sets."]; +} + +/** + * Override the convenience selectors so that forwarding works correctly. + */ +- (id)getRow +{ + return SPMySQLResultGetRow(self, SPMySQLResultRowAsDefault); +} +- (NSArray *)getRowAsArray +{ + return SPMySQLResultGetRow(self, SPMySQLResultRowAsArray); +} +- (NSDictionary *)getRowAsDictionary +{ + return SPMySQLResultGetRow(self, SPMySQLResultRowAsDictionary); +} + +/** + * Retrieve the next row in the result set, using the internal pointer, in the specified + * return format. + * If there are no rows remaining in the current iteration, returns nil. + */ +- (id)getRowAsType:(SPMySQLResultRowType)theType +{ + id theRow = nil; + + // Ensure that the connection is still up before performing a row fetch + if ((*isConnectedPtr)(parentConnection, isConnectedSelector)) { + + // The core of result fetching in streaming mode is still based around mysql_fetch_row, + // so use the super to perform normal processing. + theRow = [super getRowAsType:theType]; + } + + // If no row was returned, the end of the result set has been reached. Clear markers, + // unlock the parent connection, and return nil. + if (!theRow) { + dataDownloaded = YES; + [parentConnection _unlockConnection]; + connectionUnlocked = YES; + return nil; + } + + // Otherwise increment the data downloaded counter and return the row + downloadedRowCount++; + + return theRow; +} + +/* + * Ensure the result set is fully processed and freed without any processing + * This method ensures that the connection is unlocked. + */ +- (void)cancelResultLoad +{ + + // If data has already been downloaded successfully, no further action is required + if (dataDownloaded) return; + + MYSQL_ROW theRow; + + // Loop through all the rows and ensure the rows are fetched. + while (1) { + theRow = mysql_fetch_row(resultSet); + + // If no data was returned, we're at the end of the result set - return. + if (theRow == NULL) { + dataDownloaded = YES; + if (!connectionUnlocked) { + [parentConnection _unlockConnection]; + connectionUnlocked = YES; + } + return; + } + + downloadedRowCount++; + } +} + +#pragma mark - +#pragma mark Data retrieval for fast enumeration + +/** + * Implement the fast enumeration endpoint. Rows for fast enumeration are retrieved in + * the instance default, as specified in setDefaultRowReturnType: or defaulting to + * NSDictionary. Full streaming mode - return one row at a time. + */ +- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len +{ + + // If all rows have been retrieved, return 0 to stop iteration. + if (dataDownloaded) return 0; + + // If the MySQL row pointer does not match the requested state, throw an exception + if (state->state != currentRowIndex) { + [NSException raise:NSRangeException format:@"SPMySQLStreamingResult results can only be accessed linearly"]; + } + + // In full streaming mode return one row at a time. Retrieve the row. + id theRow = SPMySQLResultGetRow(self, SPMySQLResultRowAsDefault); + + // If nil was returned the end of the result resource has been reached + if (!theRow) return 0; + + // Add the row to the result stack and update state + stackbuf[0] = theRow; + state->state += 1; + state->itemsPtr = stackbuf; + state->mutationsPtr = (unsigned long *)self; + + return 1; +} + +@end diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLStringAdditions.h b/Frameworks/SPMySQLFramework/Source/SPMySQLStringAdditions.h new file mode 100644 index 00000000..b1f9ccd0 --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLStringAdditions.h @@ -0,0 +1,39 @@ +// +// $Id$ +// +// SPMySQLStringAdditions.h +// SPMySQLFramework +// +// Created by Rowan Beentje (rowan.beent.je) on February 8, 2012 +// Copyright (c) 2012 Rowan Beentje. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at <http://code.google.com/p/sequel-pro/> + + +@interface NSString (SPMySQLStringAdditions) + +- (NSString *)mySQLBacktickQuotedString; +- (NSString *)mySQLTickQuotedString; + +@end diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLStringAdditions.m b/Frameworks/SPMySQLFramework/Source/SPMySQLStringAdditions.m new file mode 100644 index 00000000..1ea966de --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLStringAdditions.m @@ -0,0 +1,57 @@ +// +// $Id$ +// +// SPMySQLStringAdditions.h +// SPMySQLFramework +// +// Created by Rowan Beentje (rowan.beent.je) on February 8, 2012 +// Copyright (c) 2012 Rowan Beentje. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPMySQLStringAdditions.h" + +@implementation NSString (SPMySQLStringAdditions) + +/** + * Returns the string quoted with backticks as required for MySQL identifiers + * eg.: tablename => `tablename` + * my`table => `my``table` + */ +- (NSString *)mySQLBacktickQuotedString +{ + return [NSString stringWithFormat: @"`%@`", [self stringByReplacingOccurrencesOfString:@"`" withString:@"``"]]; +} + +/** + * Returns the string quoted with ticks as required for MySQL identifiers + * eg.: tablename => 'tablename' + * my'table => 'my''table' + */ +- (NSString *)mySQLTickQuotedString +{ + return [NSString stringWithFormat: @"'%@'", [self stringByReplacingOccurrencesOfString:@"'" withString:@"''"]]; +} + +@end diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLUtilities.h b/Frameworks/SPMySQLFramework/Source/SPMySQLUtilities.h new file mode 100644 index 00000000..0a8c19b0 --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLUtilities.h @@ -0,0 +1,45 @@ +// +// $Id$ +// +// Locking.h +// SPMySQLFramework +// +// Created by Rowan Beentje (rowan.beent.je) on February 6, 2012 +// Copyright (c) 2012 Rowan Beentje. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at <http://code.google.com/p/sequel-pro/> + +#include <mach/mach_time.h> + +/** + * Define a project function to make it easier to use mach_absolute_time() + * to track monotonically increasing time. + */ +static double _elapsedSecondsSinceAbsoluteTime(uint64_t comparisonTime) +{ + uint64_t elapsedTime_t = mach_absolute_time() - comparisonTime; + Nanoseconds elapsedTime = AbsoluteToNanoseconds(*(AbsoluteTime *)&(elapsedTime_t)); + + return (((double)UnsignedWideToUInt64(elapsedTime)) * 1e-9); +}
\ No newline at end of file |