diff options
Diffstat (limited to 'Frameworks')
19 files changed, 1572 insertions, 175 deletions
diff --git a/Frameworks/SPMySQLFramework/SPMySQLEmptyResult.m b/Frameworks/SPMySQLFramework/SPMySQLEmptyResult.m index d325a3f4..b8172c04 100644 --- a/Frameworks/SPMySQLFramework/SPMySQLEmptyResult.m +++ b/Frameworks/SPMySQLFramework/SPMySQLEmptyResult.m @@ -111,7 +111,7 @@ return nil; } -- (id)_getObjectFromBytes:(char *)bytes ofLength:(NSUInteger)length fieldType:(unsigned int)fieldType fieldDefinitionIndex:(NSUInteger)fieldIndex +- (id)_getObjectFromBytes:(char *)bytes ofLength:(NSUInteger)length fieldDefinitionIndex:(NSUInteger)fieldIndex previewLength:(NSUInteger)previewLength { return nil; } diff --git a/Frameworks/SPMySQLFramework/SPMySQLFramework.xcodeproj/project.pbxproj b/Frameworks/SPMySQLFramework/SPMySQLFramework.xcodeproj/project.pbxproj index ad34eef8..6189fd48 100644 --- a/Frameworks/SPMySQLFramework/SPMySQLFramework.xcodeproj/project.pbxproj +++ b/Frameworks/SPMySQLFramework/SPMySQLFramework.xcodeproj/project.pbxproj @@ -9,6 +9,9 @@ /* Begin PBXBuildFile section */ 580A331E14D75CF7000D6933 /* SPMySQLGeometryData.h in Headers */ = {isa = PBXBuildFile; fileRef = 580A331C14D75CF7000D6933 /* SPMySQLGeometryData.h */; settings = {ATTRIBUTES = (Public, ); }; }; 580A331F14D75CF7000D6933 /* SPMySQLGeometryData.m in Sources */ = {isa = PBXBuildFile; fileRef = 580A331D14D75CF7000D6933 /* SPMySQLGeometryData.m */; }; + 583C734A17A489CC0056B284 /* SPMySQLStreamingResultStoreDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 583C734917A489CC0056B284 /* SPMySQLStreamingResultStoreDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 583C734D17B0778A0056B284 /* Data Conversion.h in Headers */ = {isa = PBXBuildFile; fileRef = 583C734B17B0778A0056B284 /* Data Conversion.h */; }; + 583C734E17B0778A0056B284 /* Data Conversion.m in Sources */ = {isa = PBXBuildFile; fileRef = 583C734C17B0778A0056B284 /* Data Conversion.m */; }; 58428E0014BA5FAE000F8438 /* SPMySQLConnection.h in Headers */ = {isa = PBXBuildFile; fileRef = 58428DFE14BA5FAE000F8438 /* SPMySQLConnection.h */; settings = {ATTRIBUTES = (Public, ); }; }; 58428E0114BA5FAE000F8438 /* SPMySQLConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 58428DFF14BA5FAE000F8438 /* SPMySQLConnection.m */; }; 5842929F14C34B36000F8438 /* my_alloc.h in Headers */ = {isa = PBXBuildFile; fileRef = 5842929414C34B36000F8438 /* my_alloc.h */; settings = {ATTRIBUTES = (); }; }; @@ -31,6 +34,8 @@ 584D812F15057ECD00F24774 /* SPMySQLKeepAliveTimer.m in Sources */ = {isa = PBXBuildFile; fileRef = 584D812D15057ECD00F24774 /* SPMySQLKeepAliveTimer.m */; }; 584D82551509775000F24774 /* Copying.h in Headers */ = {isa = PBXBuildFile; fileRef = 584D82531509775000F24774 /* Copying.h */; }; 584D82561509775000F24774 /* Copying.m in Sources */ = {isa = PBXBuildFile; fileRef = 584D82541509775000F24774 /* Copying.m */; }; + 584F16A81752911200D150A6 /* SPMySQLStreamingResultStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 584F16A61752911100D150A6 /* SPMySQLStreamingResultStore.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 584F16A91752911200D150A6 /* SPMySQLStreamingResultStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 584F16A71752911100D150A6 /* SPMySQLStreamingResultStore.m */; }; 586A99FB14F02E21007F82BF /* SPMySQLStreamingResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 586A99F914F02E21007F82BF /* SPMySQLStreamingResult.h */; settings = {ATTRIBUTES = (Public, ); }; }; 586A99FC14F02E21007F82BF /* SPMySQLStreamingResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 586A99FA14F02E21007F82BF /* SPMySQLStreamingResult.m */; }; 586AA16714F30C5F007F82BF /* Convenience Methods.h in Headers */ = {isa = PBXBuildFile; fileRef = 586AA16514F30C5F007F82BF /* Convenience Methods.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -76,6 +81,9 @@ 32DBCF5E0370ADEE00C91783 /* SPMySQLFramework_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SPMySQLFramework_Prefix.pch; path = Source/SPMySQLFramework_Prefix.pch; sourceTree = "<group>"; }; 580A331C14D75CF7000D6933 /* SPMySQLGeometryData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SPMySQLGeometryData.h; path = Source/SPMySQLGeometryData.h; sourceTree = "<group>"; }; 580A331D14D75CF7000D6933 /* SPMySQLGeometryData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SPMySQLGeometryData.m; path = Source/SPMySQLGeometryData.m; sourceTree = "<group>"; }; + 583C734917A489CC0056B284 /* SPMySQLStreamingResultStoreDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SPMySQLStreamingResultStoreDelegate.h; path = Source/SPMySQLStreamingResultStoreDelegate.h; sourceTree = "<group>"; }; + 583C734B17B0778A0056B284 /* Data Conversion.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "Data Conversion.h"; path = "Source/SPMySQLResult Categories/Data Conversion.h"; sourceTree = "<group>"; }; + 583C734C17B0778A0056B284 /* Data Conversion.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "Data Conversion.m"; path = "Source/SPMySQLResult Categories/Data Conversion.m"; sourceTree = "<group>"; }; 58428DF614BA5A13000F8438 /* build-mysql-client.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = "build-mysql-client.sh"; sourceTree = "<group>"; }; 58428DFE14BA5FAE000F8438 /* SPMySQLConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SPMySQLConnection.h; path = Source/SPMySQLConnection.h; sourceTree = "<group>"; }; 58428DFF14BA5FAE000F8438 /* SPMySQLConnection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SPMySQLConnection.m; path = Source/SPMySQLConnection.m; sourceTree = "<group>"; }; @@ -100,6 +108,8 @@ 584D812D15057ECD00F24774 /* SPMySQLKeepAliveTimer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SPMySQLKeepAliveTimer.m; path = Source/SPMySQLKeepAliveTimer.m; sourceTree = "<group>"; }; 584D82531509775000F24774 /* Copying.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Copying.h; path = "Source/SPMySQLConnection Categories/Copying.h"; sourceTree = "<group>"; }; 584D82541509775000F24774 /* Copying.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Copying.m; path = "Source/SPMySQLConnection Categories/Copying.m"; sourceTree = "<group>"; }; + 584F16A61752911100D150A6 /* SPMySQLStreamingResultStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SPMySQLStreamingResultStore.h; path = Source/SPMySQLStreamingResultStore.h; sourceTree = "<group>"; }; + 584F16A71752911100D150A6 /* SPMySQLStreamingResultStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SPMySQLStreamingResultStore.m; path = Source/SPMySQLStreamingResultStore.m; sourceTree = "<group>"; }; 586A99F914F02E21007F82BF /* SPMySQLStreamingResult.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SPMySQLStreamingResult.h; path = Source/SPMySQLStreamingResult.h; sourceTree = "<group>"; }; 586A99FA14F02E21007F82BF /* SPMySQLStreamingResult.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SPMySQLStreamingResult.m; path = Source/SPMySQLStreamingResult.m; sourceTree = "<group>"; }; 586AA0E714F1CEC8007F82BF /* Readme.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Readme.txt; sourceTree = "<group>"; }; @@ -212,6 +222,8 @@ 586A99FA14F02E21007F82BF /* SPMySQLStreamingResult.m */, 58C7C1E214DB6E4C00436315 /* SPMySQLFastStreamingResult.h */, 58C7C1E314DB6E4C00436315 /* SPMySQLFastStreamingResult.m */, + 584F16A61752911100D150A6 /* SPMySQLStreamingResultStore.h */, + 584F16A71752911100D150A6 /* SPMySQLStreamingResultStore.m */, 58C7C1E114DB6E3000436315 /* Result Categories */, 580A331B14D75CCF000D6933 /* Result types */, 584D812C15057ECD00F24774 /* SPMySQLKeepAliveTimer.h */, @@ -337,6 +349,7 @@ isa = PBXGroup; children = ( 588414BC14CE3B110078027F /* SPMySQLConnectionDelegate.h */, + 583C734917A489CC0056B284 /* SPMySQLStreamingResultStoreDelegate.h */, 58C008CC14E2AC7D00AC489A /* SPMySQLConnectionProxy.h */, ); name = Protocols; @@ -355,6 +368,8 @@ 58C7C1E114DB6E3000436315 /* Result Categories */ = { isa = PBXGroup; children = ( + 583C734B17B0778A0056B284 /* Data Conversion.h */, + 583C734C17B0778A0056B284 /* Data Conversion.m */, 58C7C1E614DB6E8600436315 /* Field Definitions.h */, 58C7C1E714DB6E8600436315 /* Field Definitions.m */, 586AA16514F30C5F007F82BF /* Convenience Methods.h */, @@ -375,11 +390,14 @@ 584294F614CB8002000F8438 /* Querying & Preparation.h in Headers */, 584294F014CB8002000F8438 /* Ping & KeepAlive.h in Headers */, 584294FA14CB8002000F8438 /* Encoding.h in Headers */, + 58C7C1E414DB6E4C00436315 /* SPMySQLFastStreamingResult.h in Headers */, 584294FE14CB8002000F8438 /* Server Info.h in Headers */, 5842929F14C34B36000F8438 /* my_alloc.h in Headers */, 584292A014C34B36000F8438 /* my_list.h in Headers */, + 583C734A17A489CC0056B284 /* SPMySQLStreamingResultStoreDelegate.h in Headers */, 584292A114C34B36000F8438 /* mysql.h in Headers */, 584292A214C34B36000F8438 /* mysql_com.h in Headers */, + 584F16A81752911200D150A6 /* SPMySQLStreamingResultStore.h in Headers */, 584292A414C34B36000F8438 /* mysql_time.h in Headers */, 584292A514C34B36000F8438 /* mysql_version.h in Headers */, 584292A614C34B36000F8438 /* typelib.h in Headers */, @@ -389,7 +407,6 @@ 588414BD14CE3B110078027F /* SPMySQLConnectionDelegate.h in Headers */, 5884165514D2306A0078027F /* SPMySQLResult.h in Headers */, 580A331E14D75CF7000D6933 /* SPMySQLGeometryData.h in Headers */, - 58C7C1E414DB6E4C00436315 /* SPMySQLFastStreamingResult.h in Headers */, 58C7C1E814DB6E8600436315 /* Field Definitions.h in Headers */, 58C006C814E0B18A00AC489A /* SPMySQLUtilities.h in Headers */, 58C008CD14E2AC7D00AC489A /* SPMySQLConnectionProxy.h in Headers */, @@ -404,6 +421,7 @@ 584D812E15057ECD00F24774 /* SPMySQLKeepAliveTimer.h in Headers */, 584D82551509775000F24774 /* Copying.h in Headers */, 58D2A4D116EDF1C6002EB401 /* SPMySQLEmptyResult.h in Headers */, + 583C734D17B0778A0056B284 /* Data Conversion.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -493,6 +511,8 @@ 584D812F15057ECD00F24774 /* SPMySQLKeepAliveTimer.m in Sources */, 584D82561509775000F24774 /* Copying.m in Sources */, 58D2A4D216EDF1C6002EB401 /* SPMySQLEmptyResult.m in Sources */, + 584F16A91752911200D150A6 /* SPMySQLStreamingResultStore.m in Sources */, + 583C734E17B0778A0056B284 /* Data Conversion.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQL Private APIs.h b/Frameworks/SPMySQLFramework/Source/SPMySQL Private APIs.h index 50d40eac..37409b36 100644 --- a/Frameworks/SPMySQLFramework/Source/SPMySQL Private APIs.h +++ b/Frameworks/SPMySQLFramework/Source/SPMySQL Private APIs.h @@ -95,21 +95,28 @@ - (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 + +// SPMySQLResult Data Conversion Private API +@interface SPMySQLResult (Data_Conversion_Private_API) + ++ (void)_initializeDataConversion; +- (id)_getObjectFromBytes:(char *)bytes ofLength:(NSUInteger)length fieldDefinitionIndex:(NSUInteger)fieldIndex previewLength:(NSUInteger)previewLength; @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) +static inline id SPMySQLResultGetObject(SPMySQLResult* self, char* bytes, NSUInteger length, NSUInteger fieldIndex, NSUInteger previewLength) { - typedef id (*SPMySQLResultGetObjectMethodPtr)(SPMySQLResult*, SEL, char*, NSUInteger, unsigned int, NSUInteger); + typedef id (*SPMySQLResultGetObjectMethodPtr)(SPMySQLResult*, SEL, char*, NSUInteger, NSUInteger, NSUInteger); static SPMySQLResultGetObjectMethodPtr cachedMethodPointer; static SEL cachedSelector; - if (!cachedSelector) cachedSelector = @selector(_getObjectFromBytes:ofLength:fieldType:fieldDefinitionIndex:); + if (!cachedSelector) cachedSelector = @selector(_getObjectFromBytes:ofLength:fieldDefinitionIndex:previewLength:); if (!cachedMethodPointer) cachedMethodPointer = (SPMySQLResultGetObjectMethodPtr)[self methodForSelector:cachedSelector]; - return cachedMethodPointer(self, cachedSelector, bytes, length, fieldType, fieldIndex); + return cachedMethodPointer(self, cachedSelector, bytes, length, fieldIndex, previewLength); } diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQL.h b/Frameworks/SPMySQLFramework/Source/SPMySQL.h index 904f390c..1e618a18 100644 --- a/Frameworks/SPMySQLFramework/Source/SPMySQL.h +++ b/Frameworks/SPMySQLFramework/Source/SPMySQL.h @@ -30,7 +30,7 @@ // // More info at <http://code.google.com/p/sequel-pro/> -@class SPMySQLConnection, SPMySQLResult, SPMySQLStreamingResult, SPMySQLFastStreamingResult; +@class SPMySQLConnection, SPMySQLResult, SPMySQLStreamingResult, SPMySQLFastStreamingResult, SPMySQLStreamingResultStore; // Global include file for the framework. // Constants @@ -61,8 +61,12 @@ #import "SPMySQLEmptyResult.h" #import "SPMySQLStreamingResult.h" #import "SPMySQLFastStreamingResult.h" +#import "SPMySQLStreamingResultStore.h" #import "Field Definitions.h" #import "Convenience Methods.h" +// MySQL result store delegate protocol +#import "SPMySQLStreamingResultStoreDelegate.h" + // Result data objects #import "SPMySQLGeometryData.h" diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Encoding.m b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Encoding.m index 38eb104f..ca052d48 100644 --- a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Encoding.m +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Encoding.m @@ -269,7 +269,7 @@ } else if (!strcmp(mysqlCharset, "cp1251")) { return NSWindowsCP1251StringEncoding; } else if (!strcmp(mysqlCharset, "utf16")) { - return NSUnicodeStringEncoding; + return NSUTF16BigEndianStringEncoding; } else if (!strcmp(mysqlCharset, "utf16le")) { return NSUTF16LittleEndianStringEncoding; } else if (!strcmp(mysqlCharset, "cp1256")) { diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.h b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.h index 0f086a89..e80a197f 100644 --- a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.h +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.h @@ -43,6 +43,7 @@ - (SPMySQLResult *)queryString:(NSString *)theQueryString; - (SPMySQLFastStreamingResult *)streamingQueryString:(NSString *)theQueryString; - (id)streamingQueryString:(NSString *)theQueryString useLowMemoryBlockingStreaming:(BOOL)fullStreaming; +- (SPMySQLStreamingResultStore *)resultStoreFromQueryString:(NSString *)theQueryString; - (id)queryString:(NSString *)theQueryString usingEncoding:(NSStringEncoding)theEncoding withResultType:(SPMySQLResultType)theReturnType; // Query convenience functions diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.m b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.m index f437ca87..7ccd0175 100644 --- a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.m +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.m @@ -191,7 +191,17 @@ { 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 result set which also handles data storage. Note + * that the donwloading of results will not occur until -[resultSet startDownload] is called. + */ +- (SPMySQLStreamingResultStore *)resultStoreFromQueryString:(NSString *)theQueryString +{ + return SPMySQLConnectionQueryString(self, theQueryString, stringEncoding, SPMySQLResultAsStreamingResultStore); +} + /** * 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 @@ -346,6 +356,12 @@ mysqlResult = mysql_use_result(mySQLConnection); theResult = [[SPMySQLFastStreamingResult alloc] initWithMySQLResult:mysqlResult stringEncoding:theEncoding connection:self]; break; + + // Also set up the result for streaming result data stores, but note the data download does not start yet + case SPMySQLResultAsStreamingResultStore: + mysqlResult = mysql_use_result(mySQLConnection); + theResult = [[SPMySQLStreamingResultStore alloc] initWithMySQLResult:mysqlResult stringEncoding:theEncoding connection:self]; + break; } // Update the error message, if appropriate, to reflect result store errors or overall success diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConstants.h b/Frameworks/SPMySQLFramework/Source/SPMySQLConstants.h index 7144d968..f7a41b5a 100644 --- a/Frameworks/SPMySQLFramework/Source/SPMySQLConstants.h +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConstants.h @@ -73,5 +73,6 @@ typedef struct { typedef enum { SPMySQLResultAsResult = 0, SPMySQLResultAsFastStreamingResult = 1, - SPMySQLResultAsLowMemStreamingResult = 2 + SPMySQLResultAsLowMemStreamingResult = 2, + SPMySQLResultAsStreamingResultStore = 3 } SPMySQLResultType; diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLFastStreamingResult.m b/Frameworks/SPMySQLFramework/Source/SPMySQLFastStreamingResult.m index 064494f3..d06caea2 100644 --- a/Frameworks/SPMySQLFramework/Source/SPMySQLFastStreamingResult.m +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLFastStreamingResult.m @@ -204,7 +204,7 @@ typedef struct st_spmysqlstreamingrowdata { copiedDataLength += fieldLength; // Convert to the correct object type - cellData = SPMySQLResultGetObject(self, rawCellData, fieldLength, fieldTypes[i], i); + cellData = SPMySQLResultGetObject(self, rawCellData, fieldLength, i, NSNotFound); } // If object creation failed, display a null diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Convenience Methods.m b/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Convenience Methods.m index 4d18db18..dbf3f66a 100644 --- a/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Convenience Methods.m +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Convenience Methods.m @@ -63,7 +63,10 @@ if (previousSeekPosition) [self seekToRow:previousSeekPosition]; // Instead of empty arrays, return nil if there are no rows. - if (![rowsToReturn count]) return nil; + if (![rowsToReturn count]) { + [rowsToReturn release]; + return nil; + } return [rowsToReturn autorelease]; } diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Data Conversion.h b/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Data Conversion.h new file mode 100644 index 00000000..a2b347e3 --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Data Conversion.h @@ -0,0 +1,42 @@ +// +// $Id$ +// +// Data Conversion.h +// SPMySQLFramework +// +// Created by Rowan Beentje (rowan.beent.je) on May 26, 2013 +// Copyright (c) 2013 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 (Data_Conversion_Private_API) + +- (id)_getObjectFromBytes:(char *)bytes ofLength:(NSUInteger)length fieldDefinitionIndex:(NSUInteger)fieldIndex previewLength:(NSUInteger)previewLength; + +static inline SPMySQLResultFieldProcessor _processorForField(MYSQL_FIELD aField); + +static inline NSString * _stringWithBytes(const void *dataBytes, NSUInteger dataLength, NSStringEncoding aStringEncoding, NSUInteger previewLength); +static inline NSString * _bitStringWithBytes(const char *bytes, NSUInteger length, NSUInteger padLength); + +@end diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Data Conversion.m b/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Data Conversion.m new file mode 100644 index 00000000..80b198d5 --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Data Conversion.m @@ -0,0 +1,413 @@ +// +// $Id$ +// +// Data Conversion.m +// SPMySQLFramework +// +// Created by Rowan Beentje (rowan.beent.je) on May 26, 2013 +// Copyright (c) 2013 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 "Data Conversion.h" + +static SPMySQLResultFieldProcessor fieldProcessingMap[256]; +static id NSNullPointer; +static NSStringEncoding NSFromCFStringEncodingBig5; +static NSStringEncoding NSFromCFStringEncodingDOSJapanese; +static NSStringEncoding NSFromCFStringEncodingEUC_KR; +static NSStringEncoding NSFromCFStringEncodingGB_2312_80; +static NSStringEncoding NSFromCFStringEncodingGBK_95; + +@implementation SPMySQLResult (Data_Conversion_Private_API) + +/** + * In the one-off class initialisation, set up the result processing map + */ ++ (void)_initializeDataConversion +{ + // Cached NSNull singleton reference + if (!NSNullPointer) NSNullPointer = [NSNull null]; + + // Go through the list of enum_field_types in mysql_com.h, mapping each to the method for + // processing that result set. + fieldProcessingMap[MYSQL_TYPE_DECIMAL] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_TINY] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_SHORT] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_LONG] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_FLOAT] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_DOUBLE] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_NULL] = SPMySQLResultFieldAsNull; + fieldProcessingMap[MYSQL_TYPE_TIMESTAMP] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_LONGLONG] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_INT24] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_DATE] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_TIME] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_DATETIME] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_YEAR] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_NEWDATE] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_VARCHAR] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_BIT] = SPMySQLResultFieldAsBit; + fieldProcessingMap[MYSQL_TYPE_NEWDECIMAL] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_ENUM] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_SET] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_TINY_BLOB] = SPMySQLResultFieldAsBlob; + fieldProcessingMap[MYSQL_TYPE_MEDIUM_BLOB] = SPMySQLResultFieldAsBlob; + fieldProcessingMap[MYSQL_TYPE_LONG_BLOB] = SPMySQLResultFieldAsBlob; + fieldProcessingMap[MYSQL_TYPE_BLOB] = SPMySQLResultFieldAsBlob; + fieldProcessingMap[MYSQL_TYPE_VAR_STRING] = SPMySQLResultFieldAsStringOrBlob; + fieldProcessingMap[MYSQL_TYPE_STRING] = SPMySQLResultFieldAsStringOrBlob; + fieldProcessingMap[MYSQL_TYPE_GEOMETRY] = SPMySQLResultFieldAsGeometry; + fieldProcessingMap[MYSQL_TYPE_DECIMAL] = SPMySQLResultFieldAsString; + + // Set up string encodings use in if/else checks + NSFromCFStringEncodingBig5 = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingBig5); + NSFromCFStringEncodingDOSJapanese = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingDOSJapanese); + NSFromCFStringEncodingEUC_KR = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingEUC_KR); + NSFromCFStringEncodingGB_2312_80 = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingGB_2312_80); + NSFromCFStringEncodingGBK_95 = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingGBK_95); +} + +/** + * 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. + * If a preview length is supplied, the returned data will be shortened to + * approximately that length, allowing optimisation of data conversion - although + * note only text and data typess will be shortened, and if shortened, will have + * an ellipsis added to indicate truncation. Supply NSNotFound as the length + * to retrieve the entire cell value. + */ +- (id)_getObjectFromBytes:(char *)bytes ofLength:(NSUInteger)length fieldDefinitionIndex:(NSUInteger)fieldIndex previewLength:(NSUInteger)previewLength +{ + MYSQL_FIELD theField = fieldDefinitions[fieldIndex]; + + // A NULL pointer for the data indicates a null value; return a NSNull object. + if (bytes == NULL) { + return NSNullPointer; + } + + // Determine the field processor to use + SPMySQLResultFieldProcessor dataProcessor = _processorForField(theField); + + // If this instance is set to convert all data as strings, override blob processors. + 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 _convertStringData(bytes, length, stringEncoding, previewLength); + + // Convert BLOB types to NSData. + // Use the preview length as supplied. + case SPMySQLResultFieldAsBlob: + if (previewLength != NSNotFound && previewLength < length) { + NSMutableData *theData = [NSMutableData dataWithBytes:bytes length:previewLength]; + if (previewLength > 5) { + [theData replaceBytesInRange:NSMakeRange(previewLength - 3, 3) withBytes:"..."]; + } else { + [theData appendBytes:"..." length:3]; + } + return theData; + } + 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 _bitStringWithBytes(bytes, length, fieldDefinitions[fieldIndex].length); + + // Convert null types to NSNulls + case SPMySQLResultFieldAsNull: + return NSNullPointer; + + case SPMySQLResultFieldAsUnhandled: + NSLog(@"SPMySQLResult processing encountered an unknown field type (%d), falling back to NSData handling", fieldDefinitions[fieldIndex].type); + return [NSData dataWithBytes:bytes length:length]; + } + + [NSException raise:NSInternalInconsistencyException format:@"Unhandled field type when processing SPMySQLResults"]; + return nil; +} + +/** + * Returns the field processor to use for a specified field. + */ +static inline SPMySQLResultFieldProcessor _processorForField(MYSQL_FIELD aField) +{ + // Determine the default field processor to use + SPMySQLResultFieldProcessor dataProcessor = fieldProcessingMap[aField.type]; + + // Switch the method to process the cell data based on the field type mapping. + switch (dataProcessor) { + + // STRING and VAR_STRING types may be strings or binary types; check the binary flag + case SPMySQLResultFieldAsStringOrBlob: + if (aField.flags & BINARY_FLAG) { + dataProcessor = SPMySQLResultFieldAsBlob; + } + break; + + // Blob types may be automatically be converted to strings, or may be non-binary + case SPMySQLResultFieldAsBlob: + if (!(aField.flags & BINARY_FLAG)) { + dataProcessor = SPMySQLResultFieldAsString; + } + break; + + // In most cases, use the original data processor. + default: + break; + } + + return dataProcessor; +} + +/** + * 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. + * MySQL stores bit data as string data stored in an 8-bit wide character set. + */ +static inline NSString * _bitStringWithBytes(const char *bytes, NSUInteger length, NSUInteger padLength) +{ + NSUInteger i = 0; + NSUInteger bitLength = length << 3; + + if (bytes == NULL) { + return nil; + } + + // Ensure padLength is never lower than the length + if (padLength < bitLength) { + padLength = bitLength; + } + + // Generate a nul-terminated C string representation of the binary data + char *cStringBuffer = malloc(padLength + 1); + cStringBuffer[padLength] = '\0'; + while (i < bitLength) { + cStringBuffer[padLength - ++i] = ( (bytes[length - 1 - (i >> 3)] >> (i & 0x7)) & 1 ) ? '1' : '0'; + } + while (i++ < padLength) { + cStringBuffer[padLength - i] = '0'; + } + + // Convert to a string + NSString *returnString = [NSString stringWithUTF8String:cStringBuffer]; + + // Free up memory and return + free(cStringBuffer); + return returnString; +} + +/** + * Converts stored string data - which may contain nul bytes - to a native + * Objective-C string, using the current class encoding. + */ +static inline NSString * _convertStringData(const void *dataBytes, NSUInteger dataLength, NSStringEncoding aStringEncoding, NSUInteger previewLength) +{ + + // Fast case - if not using a preview length, or if the data length is shorter, return the requested data. + if (previewLength == NSNotFound || dataLength <= previewLength) { + return [[[NSString alloc] initWithBytes:dataBytes length:dataLength encoding:aStringEncoding] autorelease]; + } + + NSUInteger i = 0, characterLength = 0, byteLength = previewLength; + uint16_t continuationStart, continuationEnd; + + // Handle various special encodings: + + // Variable-length UTF16, in either endianness. Code points U+D800 to U+DFFF are used to + // indicate continuation characters, so can be used to identify if each character is two + // or four bytes long. + if (aStringEncoding == NSUTF16LittleEndianStringEncoding || aStringEncoding == NSUTF16BigEndianStringEncoding) + { + if (aStringEncoding == NSUTF16LittleEndianStringEncoding) { + continuationStart = 0x00D8; + continuationEnd = 0xFFDF; + } else { + continuationStart = 0xD800; + continuationEnd = 0xDFFF; + } + + while (i < dataLength && characterLength < previewLength) { + uint16_t charStart = ((uint16_t *)dataBytes)[i/2]; + if (charStart >= continuationStart && charStart <= continuationEnd) { + i += 4; + } else { + i += 2; + } + characterLength++; + } + byteLength = i; + } + + // Variable-length UTF-8 string encoding. The first bits can be inspected to determine + // character length; one-byte characters start with a zero, two-byte characters with + // 110..., three-byte characters with 1110..., and four-byte with 11110... + else if (aStringEncoding == NSUTF8StringEncoding) + { + while (i < dataLength && characterLength < previewLength) { + uint8_t charStart = ((uint8_t *)dataBytes)[i]; + if ((charStart & 0xf0) == 0xf0) { + i += 4; + } else if ((charStart & 0xe0) == 0xe0) { + i += 3; + } else if ((charStart & 0xc0) == 0xc0) { + i += 2; + } else { + i++; + } + characterLength++; + } + byteLength = i; + } + + // Variable-length CP932 encoding; if the first byte is between 0x81-0x9F, + // or between 0xE0-0xFC, the character takes two bytes. + else if (aStringEncoding == NSFromCFStringEncodingDOSJapanese) { + while (i < dataLength && characterLength < previewLength) { + uint8_t charStart = ((uint8_t *)dataBytes)[i]; + if ((charStart >= 0x81 && charStart <= 0x9f) || (charStart >= 0xE0 && charStart <= 0xFC)) { + i += 2; + } else { + i++; + } + characterLength++; + } + byteLength = i; + } + + // Variable-length EUCJPMS encoding, which can be one to three bytes. If a character + // begins with 0x8F, it's three bytes long; if it begins with 0x8E or 0xA1-0xFE, it's + // two bytes long, otherwise only one. + else if (aStringEncoding == NSJapaneseEUCStringEncoding) { + while (i < dataLength && characterLength < previewLength) { + uint8_t charStart = ((uint8_t *)dataBytes)[i]; + if (charStart == 0x8F) { + i += 3; + } else if (charStart == 0x8E || (charStart >= 0xA1 && charStart <= 0xFE)) { + i += 2; + } else { + i++; + } + characterLength++; + } + byteLength = i; + } + + // Variable-length EUC-KR, which can be one or two bytes. If a character begins with + // 0xA1-0xFE, it's two bytes long, otherwise just one byte long. The checks below have + // been modified to look for 0x81-0xFE for two byte logic, for additional compatibility + // with CP949. + // Similarly, variable-length GBK, which can be one or two bytes; a character beginning + // with 0x81-0xFE is two bytes long, otherwise one byte. + else if (aStringEncoding == NSFromCFStringEncodingEUC_KR || aStringEncoding == NSFromCFStringEncodingGBK_95) { + while (i < dataLength && characterLength < previewLength) { + uint8_t charStart = ((uint8_t *)dataBytes)[i]; + if (charStart >= 0x81 && charStart <= 0xFE) { + i += 2; + } else { + i++; + } + characterLength++; + } + byteLength = i; + } + + // Shift JIS, which can be one or two bytes. A character starting in the ranges + // 0x80-0xA0 or 0xE0-0xFF is two bytes, otherwise one. + else if (aStringEncoding == NSShiftJISStringEncoding) { + while (i < dataLength && characterLength < previewLength) { + uint8_t charStart = ((uint8_t *)dataBytes)[i]; + if ((charStart >= 0x80 && charStart <= 0xA0) || (charStart >= 0xE0 && charStart <= 0xFF)) { + i += 2; + } else { + i++; + } + characterLength++; + } + byteLength = i; + } + + // Encodings where characters are always 4 bytes + else if (aStringEncoding == NSUTF32StringEncoding) + { + characterLength = MIN(previewLength, floor(dataLength / 4)); + byteLength = characterLength * 4; + } + + // Encodings where characters are always 2 bytes + else if ( + aStringEncoding == NSFromCFStringEncodingBig5 || + aStringEncoding == NSFromCFStringEncodingGB_2312_80 || + aStringEncoding == NSUnicodeStringEncoding /* UCS-2 */ + ) { + characterLength = MIN(previewLength, floor(dataLength / 2)); + byteLength = characterLength * 2; + } + + // Default to a single byte per character + else { + characterLength = previewLength; + byteLength = previewLength; + } + + // If returning the full string, use a fast path + if (byteLength >= dataLength) { + return [[[NSString alloc] initWithBytes:dataBytes length:dataLength encoding:aStringEncoding] autorelease]; + } + + // Get a string using the calculated details + NSMutableString *previewString = [[[NSMutableString alloc] initWithBytes:dataBytes length:byteLength encoding:aStringEncoding] autorelease]; + + // If that failed, fall back to using NSString methods to produce a preview + if (!previewString) { + previewString = [[[NSMutableString alloc] initWithBytes:dataBytes length:dataLength encoding:aStringEncoding] autorelease]; + if ([previewString length] > previewLength) { + [previewString deleteCharactersInRange:NSMakeRange(previewLength, [previewString length] - previewLength)]; + } + } + + // Add an indication the string is a preview + [previewString appendString:@"..."]; + + return previewString; +} + + +@end diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLResult.h b/Frameworks/SPMySQLFramework/Source/SPMySQLResult.h index 29518b5d..df9b7698 100644 --- a/Frameworks/SPMySQLFramework/Source/SPMySQLResult.h +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLResult.h @@ -50,7 +50,6 @@ typedef enum { // 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 @@ -85,9 +84,6 @@ typedef enum { - (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 diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLResult.m b/Frameworks/SPMySQLFramework/Source/SPMySQLResult.m index fdc83332..8f51118b 100644 --- a/Frameworks/SPMySQLFramework/Source/SPMySQLResult.m +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLResult.m @@ -34,7 +34,6 @@ #import "SPMySQL Private APIs.h" #import "SPMySQLArrayAdditions.h" -static SPMySQLResultFieldProcessor fieldProcessingMap[256]; static id NSNullPointer; @implementation SPMySQLResult @@ -48,45 +47,14 @@ static id NSNullPointer; #pragma mark - #pragma mark Setup and teardown -/** - * In the one-off class initialisation, set up the result processing map - */ + (void)initialize { // Cached NSNull singleton reference if (!NSNullPointer) NSNullPointer = [NSNull null]; - // Go through the list of enum_field_types in mysql_com.h, mapping each to the method for - // processing that result set. - fieldProcessingMap[MYSQL_TYPE_DECIMAL] = SPMySQLResultFieldAsString; - fieldProcessingMap[MYSQL_TYPE_TINY] = SPMySQLResultFieldAsString; - fieldProcessingMap[MYSQL_TYPE_SHORT] = SPMySQLResultFieldAsString; - fieldProcessingMap[MYSQL_TYPE_LONG] = SPMySQLResultFieldAsString; - fieldProcessingMap[MYSQL_TYPE_FLOAT] = SPMySQLResultFieldAsString; - fieldProcessingMap[MYSQL_TYPE_DOUBLE] = SPMySQLResultFieldAsString; - fieldProcessingMap[MYSQL_TYPE_NULL] = SPMySQLResultFieldAsNull; - fieldProcessingMap[MYSQL_TYPE_TIMESTAMP] = SPMySQLResultFieldAsString; - fieldProcessingMap[MYSQL_TYPE_LONGLONG] = SPMySQLResultFieldAsString; - fieldProcessingMap[MYSQL_TYPE_INT24] = SPMySQLResultFieldAsString; - fieldProcessingMap[MYSQL_TYPE_DATE] = SPMySQLResultFieldAsString; - fieldProcessingMap[MYSQL_TYPE_TIME] = SPMySQLResultFieldAsString; - fieldProcessingMap[MYSQL_TYPE_DATETIME] = SPMySQLResultFieldAsString; - fieldProcessingMap[MYSQL_TYPE_YEAR] = SPMySQLResultFieldAsString; - fieldProcessingMap[MYSQL_TYPE_NEWDATE] = SPMySQLResultFieldAsString; - fieldProcessingMap[MYSQL_TYPE_VARCHAR] = SPMySQLResultFieldAsString; - fieldProcessingMap[MYSQL_TYPE_BIT] = SPMySQLResultFieldAsBit; - fieldProcessingMap[MYSQL_TYPE_NEWDECIMAL] = SPMySQLResultFieldAsString; - fieldProcessingMap[MYSQL_TYPE_ENUM] = SPMySQLResultFieldAsString; - fieldProcessingMap[MYSQL_TYPE_SET] = SPMySQLResultFieldAsString; - fieldProcessingMap[MYSQL_TYPE_TINY_BLOB] = SPMySQLResultFieldAsBlob; - fieldProcessingMap[MYSQL_TYPE_MEDIUM_BLOB] = SPMySQLResultFieldAsBlob; - fieldProcessingMap[MYSQL_TYPE_LONG_BLOB] = SPMySQLResultFieldAsBlob; - fieldProcessingMap[MYSQL_TYPE_BLOB] = SPMySQLResultFieldAsBlob; - fieldProcessingMap[MYSQL_TYPE_VAR_STRING] = SPMySQLResultFieldAsStringOrBlob; - fieldProcessingMap[MYSQL_TYPE_STRING] = SPMySQLResultFieldAsStringOrBlob; - fieldProcessingMap[MYSQL_TYPE_GEOMETRY] = SPMySQLResultFieldAsGeometry; - fieldProcessingMap[MYSQL_TYPE_DECIMAL] = SPMySQLResultFieldAsString; + // Set up data conversion details + [self _initializeDataConversion]; } /** @@ -105,7 +73,6 @@ static id NSNullPointer; fieldDefinitions = NULL; fieldNames = NULL; - fieldTypes = NULL; defaultRowReturnType = SPMySQLResultRowAsDictionary; } @@ -134,11 +101,9 @@ static id NSNullPointer; // 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; } } @@ -154,7 +119,6 @@ static id NSNullPointer; [fieldNames[i] release]; } free(fieldNames); - free(fieldTypes); } [super dealloc]; @@ -281,7 +245,7 @@ static id NSNullPointer; // 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); + id cellData = SPMySQLResultGetObject(self, theRow[i], theRowDataLengths[i], i, NSNotFound); // If object creation failed, display a null if (!cellData) cellData = NSNullPointer; @@ -339,47 +303,6 @@ static id NSNullPointer; 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. - * MySQL stores bit data as string data stored in an 8-bit wide character set. - */ -+ (NSString *)bitStringWithBytes:(const char *)bytes length:(NSUInteger)length padToLength:(NSUInteger)padLength -{ - NSUInteger i = 0; - NSUInteger bitLength = length << 3; - - if (bytes == NULL) { - return nil; - } - - // Ensure padLength is never lower than the length - if (padLength < bitLength) { - padLength = bitLength; - } - - // Generate a nul-terminated C string representation of the binary data - char *cStringBuffer = malloc(padLength + 1); - cStringBuffer[padLength] = '\0'; - while (i < bitLength) { - cStringBuffer[padLength - ++i] = ( (bytes[length - 1 - (i >> 3)] >> (i & 0x7)) & 1 ) ? '1' : '0'; - } - while (i++ < padLength) { - cStringBuffer[padLength - i] = '0'; - } - - // Convert to a string - NSString *returnString = [NSString stringWithUTF8String:cStringBuffer]; - - // Free up memory and return - free(cStringBuffer); - return returnString; -} - @end #pragma mark - @@ -406,82 +329,4 @@ static id NSNullPointer; queryExecutionTime = theExecutionTime; } -/** - * Core data conversion function, taking C data provided by MySQL and converting - * to an appropriate return type. - * Note that the data passed in currently is *not* nul-terminated for fast - * streaming results, which is safe for the current implementation but should be - * kept in mind for future changes. - */ -- (id)_getObjectFromBytes:(char *)bytes ofLength:(NSUInteger)length fieldType:(unsigned int)fieldType fieldDefinitionIndex:(NSUInteger)fieldIndex -{ - - // A NULL pointer for the data indicates a null value; return a NSNull object. - if (bytes == NULL) return NSNullPointer; - - // Determine the field processor to use - SPMySQLResultFieldProcessor dataProcessor = fieldProcessingMap[fieldType]; - - // Switch the method to process the cell data based on the field type mapping. - // Do this in two passes: the first as logic may cause a change in processor required. - switch (dataProcessor) { - - // STRING and VAR_STRING types may be strings or binary types; check the binary flag - case SPMySQLResultFieldAsStringOrBlob: - if (fieldDefinitions[fieldIndex].flags & BINARY_FLAG) { - dataProcessor = SPMySQLResultFieldAsBlob; - } - break; - - // Blob types may be automatically be converted to strings, or may be non-binary - case SPMySQLResultFieldAsBlob: - if (!(fieldDefinitions[fieldIndex].flags & BINARY_FLAG)) { - dataProcessor = SPMySQLResultFieldAsString; - } - break; - - // In most cases, use the original data processor. - default: - break; - } - - // If this instance is set to convert all data as strings, alter the processor. - if (returnDataAsStrings && dataProcessor == SPMySQLResultFieldAsBlob) { - dataProcessor = SPMySQLResultFieldAsString; - } - - // Now switch the processing method again to actually process the data. - switch (dataProcessor) { - - // Convert string types using a method that will preserve any nul characters - // within the string - case SPMySQLResultFieldAsString: - case SPMySQLResultFieldAsStringOrBlob: - return [[[NSString alloc] initWithBytes:bytes length:length encoding:stringEncoding] autorelease]; - - // Convert BLOB types to NSData - case SPMySQLResultFieldAsBlob: - return [NSData dataWithBytes:bytes length:length]; - - // For Geometry types, use a special Geometry object to handle their complexity - case SPMySQLResultFieldAsGeometry: - return [SPMySQLGeometryData dataWithBytes:bytes length:length]; - - // For bit fields, get a zero-padded representation of the data - case SPMySQLResultFieldAsBit: - return [SPMySQLResult bitStringWithBytes:bytes length:length padToLength:fieldDefinitions[fieldIndex].length]; - - // Convert null types to NSNulls - case SPMySQLResultFieldAsNull: - return NSNullPointer; - - case SPMySQLResultFieldAsUnhandled: - NSLog(@"SPMySQLResult processing encountered an unknown field type (%d), falling back to NSData handling", fieldType); - return [NSData dataWithBytes:bytes length:length]; - } - - [NSException raise:NSInternalInconsistencyException format:@"Unhandled field type when processing SPMySQLResults"]; - return nil; -} - @end diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResult.h b/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResult.h index 5abb85db..de8ca747 100644 --- a/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResult.h +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResult.h @@ -46,6 +46,8 @@ SEL isConnectedSelector; } +@property (readonly, assign) BOOL dataDownloaded; + - (id)initWithMySQLResult:(void *)theResult stringEncoding:(NSStringEncoding)theStringEncoding connection:(SPMySQLConnection *)theConnection; // Allow result fetching to be cancelled diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResult.m b/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResult.m index 51a17611..040dbbfb 100644 --- a/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResult.m +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResult.m @@ -44,6 +44,10 @@ @implementation SPMySQLStreamingResult +#pragma mark - Synthesized properties + +@synthesize dataDownloaded; + #pragma mark - /** diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResultStore.h b/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResultStore.h new file mode 100644 index 00000000..93efd6d9 --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResultStore.h @@ -0,0 +1,110 @@ +// +// $Id$ +// +// SPMySQLStreamingResultStore.h +// SPMySQLFramework +// +// Created by Rowan Beentje (rowan.beent.je) on May 26, 2013 +// Copyright (c) 2013 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/SPMySQL.h> +#import "SPMySQLStreamingResultStoreDelegate.h" +#include <malloc/malloc.h> + +typedef char SPMySQLStreamingResultStoreRowData; + +@interface SPMySQLStreamingResultStore : SPMySQLStreamingResult { + BOOL loadStarted; + BOOL loadCancelled; + id <SPMySQLStreamingResultStoreDelegate> delegate; + + // Data storage and allocation + NSUInteger rowCapacity; + NSUInteger rowDownloadIterator; + malloc_zone_t *storageMallocZone; + SPMySQLStreamingResultStoreRowData **dataStorage; + + // Thread safety + pthread_mutex_t dataLock; + +} + +@property (readwrite, assign) id <SPMySQLStreamingResultStoreDelegate> delegate; + +/* Setup and teardown */ +- (void)replaceExistingResultStore:(SPMySQLStreamingResultStore *)previousResultStore; +- (void)startDownload; + +/* Data retrieval */ +- (NSMutableArray *)rowContentsAtIndex:(NSUInteger)rowIndex; +- (id)cellDataAtRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex; +- (id)cellPreviewAtRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex previewLength:(NSUInteger)previewLength; +- (BOOL)cellIsNullAtRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex; + +/* Deleting rows and addition of placeholder rows */ +- (void) addDummyRow; +- (void) insertDummyRowAtIndex:(NSUInteger)anIndex; +- (void) removeRowAtIndex:(NSUInteger)anIndex; +- (void) removeRowsInRange:(NSRange)rangeToRemove; +- (void) removeAllRows; + +@end + +#pragma mark - +#pragma mark Cached method calls to remove obj-c messaging overhead in tight loops + +static inline unsigned long long SPMySQLResultStoreGetRowCount(SPMySQLStreamingResultStore* self) +{ + typedef unsigned long long (*SPMSRSRowCountMethodPtr)(SPMySQLStreamingResultStore*, SEL); + static SPMSRSRowCountMethodPtr SPMSRSRowCount; + if (!SPMSRSRowCount) SPMSRSRowCount = (SPMSRSRowCountMethodPtr)[self methodForSelector:@selector(numberOfRows)]; + return SPMSRSRowCount(self, @selector(numberOfRows)); +} + +static inline id SPMySQLResultStoreGetRow(SPMySQLStreamingResultStore* self, NSUInteger rowIndex) +{ + typedef id (*SPMSRSRowFetchMethodPtr)(SPMySQLStreamingResultStore*, SEL, NSUInteger); + static SPMSRSRowFetchMethodPtr SPMSRSRowFetch; + if (!SPMSRSRowFetch) SPMSRSRowFetch = (SPMSRSRowFetchMethodPtr)[self methodForSelector:@selector(rowContentsAtIndex:)]; + return SPMSRSRowFetch(self, @selector(rowContentsAtIndex:), rowIndex); +} + +static inline id SPMySQLResultStoreObjectAtRowAndColumn(SPMySQLStreamingResultStore* self, NSUInteger rowIndex, NSUInteger colIndex) +{ + typedef id (*SPMSRSObjectFetchMethodPtr)(SPMySQLStreamingResultStore*, SEL, NSUInteger, NSUInteger); + static SPMSRSObjectFetchMethodPtr SPMSRSObjectFetch; + if (!SPMSRSObjectFetch) SPMSRSObjectFetch = (SPMSRSObjectFetchMethodPtr)[self methodForSelector:@selector(cellDataAtRow:column:)]; + return SPMSRSObjectFetch(self, @selector(cellDataAtRow:column:), rowIndex, colIndex); +} + +static inline id SPMySQLResultStorePreviewAtRowAndColumn(SPMySQLStreamingResultStore* self, NSUInteger rowIndex, NSUInteger colIndex, NSUInteger previewLength) +{ + typedef id (*SPMSRSObjectPreviewMethodPtr)(SPMySQLStreamingResultStore*, SEL, NSUInteger, NSUInteger, NSUInteger); + static SPMSRSObjectPreviewMethodPtr SPMSRSObjectPreview; + if (!SPMSRSObjectPreview) SPMSRSObjectPreview = (SPMSRSObjectPreviewMethodPtr)[self methodForSelector:@selector(cellPreviewAtRow:column:previewLength:)]; + return SPMSRSObjectPreview(self, @selector(cellPreviewAtRow:column:previewLength:), rowIndex, colIndex, previewLength); +} diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResultStore.m b/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResultStore.m new file mode 100644 index 00000000..47a3a615 --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResultStore.m @@ -0,0 +1,888 @@ +// +// $Id$ +// +// SPMySQLStreamingResultStore.m +// SPMySQLFramework +// +// Created by Rowan Beentje (rowan.beent.je) on May 26, 2013 +// Copyright (c) 2013 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 "SPMySQLStreamingResultStore.h" +#import "SPMySQL Private APIs.h" +#import "SPMySQLArrayAdditions.h" +#include <pthread.h> + +static id NSNullPointer; + +typedef enum { + SPMySQLStoreMetadataAsChar = sizeof(unsigned char), + SPMySQLStoreMetadataAsShort = sizeof(unsigned short), + SPMySQLStoreMetadataAsLong = sizeof(unsigned long) +} SPMySQLResultStoreRowMetadataType; + +/** + * This type of result provides its own storage for the MySQL result set, converting + * rows or cells on-demand to Objective-C types as they are requested. The results + * are fetched in streaming fashion after the result store object is returned, with + * a background thread set up to download the results as fast as possible. Delegate + * methods can be used to display a progress bar during downloads as rows are retrieved. + */ + +@interface SPMySQLStreamingResultStore (PrivateAPI) + +- (void) _downloadAllData; +- (void) _ensureCapacityForAdditionalRowCount:(NSUInteger)numExtraRows; +- (void) _increaseCapacity; +- (NSUInteger) _rowCapacity; +- (SPMySQLStreamingResultStoreRowData **) _transferResultStoreData; + +@end + +#pragma mark - + +@implementation SPMySQLStreamingResultStore + +@synthesize delegate; + +static inline void SPMySQLStreamingResultStoreEnsureCapacityForAdditionalRowCount(SPMySQLStreamingResultStore* self, NSUInteger numExtraRows) +{ + typedef void (*SPMSRSEnsureCapacityMethodPtr)(SPMySQLStreamingResultStore*, SEL, NSUInteger); + static SPMSRSEnsureCapacityMethodPtr SPMSRSEnsureCapacity; + if (!SPMSRSEnsureCapacity) { + SPMSRSEnsureCapacity = (SPMSRSEnsureCapacityMethodPtr)[self methodForSelector:@selector(_ensureCapacityForAdditionalRowCount:)]; + } + SPMSRSEnsureCapacity(self, @selector(_ensureCapacityForAdditionalRowCount:), numExtraRows); +} + +static inline void SPMySQLStreamingResultStoreFreeRowData(SPMySQLStreamingResultStoreRowData* aRow) +{ + if (aRow == NULL) { + return; + } + + free(aRow); +} + + +#pragma mark - Setup and teardown + +/** + * In the one-off class initialisation, cache static variables + */ ++ (void)initialize +{ + + // Cached NSNull singleton reference + if (!NSNullPointer) NSNullPointer = [NSNull null]; +} + +/** + * Standard init method, constructing the SPMySQLStreamingResult around a MySQL + * result pointer and the encoding to use when working with the data. + * As opposed to SPMySQLResult, defaults to returning rows as arrays, as the result + * sets are likely to be larger and processed in loops. + * The download of results is not started at once - instead, it must be triggered manually + * via -startDownload, which allows assignment of a result set to replace before use. + */ +- (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 streaming result counts and tracking + numberOfRows = 0; + rowDownloadIterator = 0; + loadStarted = NO; + loadCancelled = NO; + rowCapacity = 0; + dataStorage = NULL; + storageMallocZone = NULL; + delegate = nil; + + // Set up the storage lock + pthread_mutex_init(&dataLock, NULL); + } + + return self; +} + +/** + * Prime the result set with an existing result store. This is typically used when reloading a + * result set; re-using the existing data store allows the data to be updated without blanking + * the visual display first, providing a more consistent experience. + */ +- (void)replaceExistingResultStore:(SPMySQLStreamingResultStore *)previousResultStore +{ + if (dataStorage != NULL) { + [NSException raise:NSInternalInconsistencyException format:@"Data storage has already been assigned or created"]; + } + + pthread_mutex_lock(&dataLock); + + // Talk to the previous result store, claiming its malloc zone and data + numberOfRows = [previousResultStore numberOfRows]; + rowCapacity = [previousResultStore _rowCapacity]; + dataStorage = [previousResultStore _transferResultStoreData]; + storageMallocZone = malloc_zone_from_ptr(dataStorage); + + // If the new column count is higher than the old column count, the old data needs + // to have null data added to the end of it to prevent problems while loading. + NSUInteger previousNumberOfFields = [previousResultStore numberOfFields]; + if (numberOfFields > previousNumberOfFields) { + unsigned long long i; + NSUInteger j; + SPMySQLStreamingResultStoreRowData *oldRow, *newRow; + + size_t sizeOfMetadata, newMetadataLength, newDataOffset, oldMetadataLength, oldDataOffset; + unsigned long dataLength; + + for (i = 0; i < numberOfRows; i++) { + oldRow = dataStorage[i]; + if (oldRow != NULL) { + + // Get the metadata size for this row + sizeOfMetadata = oldRow[0]; + + // Derive some base sizes + newMetadataLength = (size_t)(sizeOfMetadata * numberOfFields); + newDataOffset = (size_t)(1 + (sizeOfMetadata + sizeof(BOOL)) * numberOfFields); + oldMetadataLength = (size_t)(sizeOfMetadata * previousNumberOfFields); + oldDataOffset = (size_t)(1 + (sizeOfMetadata + sizeof(BOOL)) * previousNumberOfFields); + + // Manually unroll the logic for the different cases. This is messy, but + // the large memory savings for small rows make this extra work worth it. + switch (sizeOfMetadata) { + case SPMySQLStoreMetadataAsChar: + + // The length of the data is stored in the last end-position slot + dataLength = ((unsigned char *)(oldRow + 1))[previousNumberOfFields - 1]; + break; + + case SPMySQLStoreMetadataAsShort: + dataLength = ((unsigned short *)(oldRow + 1))[previousNumberOfFields - 1]; + break; + case SPMySQLStoreMetadataAsLong: + default: + dataLength = ((unsigned long *)(oldRow + 1))[previousNumberOfFields - 1]; + break; + } + + // The overall new size for the row is the new size of the metadata + // (positions and null indicators), plus the old size of the data. + dataStorage[i] = malloc_zone_malloc(storageMallocZone, newDataOffset + dataLength); + newRow = dataStorage[i]; + + // Copy the old row's metadata + memcpy(newRow, oldRow, 1 + oldMetadataLength); + + // Copy the null status data + memcpy(newRow + 1 + newMetadataLength, oldRow + 1 + oldMetadataLength, (size_t)(sizeof(BOOL) * previousNumberOfFields)); + + // Copy the cell data to the new end of the memory area + memcpy(newRow + newDataOffset, oldRow + oldDataOffset, dataLength); + + // Change the row pointers to point to the start of the metadata + oldRow = oldRow + 1; + newRow = newRow + 1; + + switch (sizeOfMetadata) { + case SPMySQLStoreMetadataAsLong: + + // Add the new metadata and null statuses + for (j = previousNumberOfFields; j < numberOfFields; j++) { + ((unsigned long *)newRow)[j] = ((unsigned long *)oldRow)[j - 1]; + ((BOOL *)(newRow + newMetadataLength))[j] = YES; + } + break; + case SPMySQLStoreMetadataAsShort:; + for (j = previousNumberOfFields; j < numberOfFields; j++) { + ((unsigned short *)newRow)[j] = ((unsigned short *)oldRow)[j - 1]; + ((BOOL *)(newRow + newMetadataLength))[j] = YES; + } + break; + case SPMySQLStoreMetadataAsChar:; + for (j = previousNumberOfFields; j < numberOfFields; j++) { + ((unsigned char *)newRow)[j] = ((unsigned char *)oldRow)[j - 1]; + ((BOOL *)(newRow + newMetadataLength))[j] = YES; + } + break; + } + + // Free the entire old row, correcting the row pointer tweak + free(oldRow - 1); + } + } + } + + pthread_mutex_unlock(&dataLock); +} + +/** + * Start downloading the result data. + */ +- (void)startDownload +{ + if (loadStarted) { + [NSException raise:NSInternalInconsistencyException format:@"Data download has already been started"]; + } + + // If not already assigned, initialise the data storage, initially with space for 100 rows + if (dataStorage == NULL) { + + // Set up the malloc zone + storageMallocZone = malloc_create_zone(64 * 1024, 0); + malloc_set_zone_name(storageMallocZone, "SPMySQLStreamingResultStore_Heap"); + + rowCapacity = 100; + dataStorage = malloc_zone_malloc(storageMallocZone, rowCapacity * sizeof(SPMySQLStreamingResultStoreRowData *)); + } + + loadStarted = YES; + [NSThread detachNewThreadSelector:@selector(_downloadAllData) toTarget:self withObject:nil]; +} + +/** + * 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]; + + // Free all the data, by destroying the parent zone + if (storageMallocZone) { + malloc_destroy_zone(storageMallocZone); + } + + // 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 - 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), but the rows downloaded to date is returned; otherwise the number + * of rows is returned. + */ +- (unsigned long long)numberOfRows +{ + if (!dataDownloaded) { + return rowDownloadIterator; + } + + return numberOfRows; +} + +#pragma mark - Data retrieval + +/** + * Return a mutable array containing the data for a specified row. + */ +- (NSMutableArray *)rowContentsAtIndex:(NSUInteger)rowIndex +{ + + // Throw an exception if the index is out of bounds + if (rowIndex >= numberOfRows) { + [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", (unsigned long long)rowIndex, (unsigned long long)numberOfRows]; + } + + // If the row store is a null pointer, the row is a dummy row. + if (dataStorage[rowIndex] == NULL) { + return nil; + } + + // Construct a mutable array and add all the cells in the row + NSMutableArray *rowArray = [NSMutableArray arrayWithCapacity:numberOfFields]; + for (NSUInteger columnIndex = 0; columnIndex < numberOfFields; columnIndex++) { + CFArrayAppendValue((CFMutableArrayRef)rowArray, SPMySQLResultStoreObjectAtRowAndColumn(self, rowIndex, columnIndex)); + } + + return rowArray; +} + +/** + * Return the data at a specified row and column index. + */ +- (id)cellDataAtRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex +{ + + // Wrap the preview method, passing in a length limit of NSNotFound + return SPMySQLResultStorePreviewAtRowAndColumn(self, rowIndex, columnIndex, NSNotFound); +} + +/** + * Return the data at a specified row and column index. If a preview length is supplied, + * the cell data will be checked, and if longer, will be shortened to around that length, + * although multibyte encodings will show some variation. + */ +- (id)cellPreviewAtRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex previewLength:(NSUInteger)previewLength +{ + // Throw an exception if the row or column index is out of bounds + if (rowIndex >= numberOfRows || columnIndex >= numberOfFields) { + [NSException raise:NSRangeException format:@"Requested storage index (row %llu, col %llu) beyond bounds (%llu, %llu)", (unsigned long long)rowIndex, (unsigned long long)columnIndex, (unsigned long long)numberOfRows, (unsigned long long)numberOfFields]; + } + + id cellData = nil; + char *rawCellDataStart; + SPMySQLStreamingResultStoreRowData *rowData = dataStorage[rowIndex]; + + // A null pointer for the row indicates a dummy entry + if (rowData == NULL) { + return nil; + } + + unsigned long dataStart, dataLength; + size_t sizeOfMetadata; + + // Get the metadata size for this row and adjust the data pointer past the indicator + sizeOfMetadata = rowData[0]; + rowData = rowData + 1; + + static size_t sizeOfNullRecord = sizeof(BOOL); + + // Retrieve the data positions within the stored data. Manually unroll the logic for + // the different data size cases; again, this is messy, but the large memory savings for + // small rows make this extra work worth it. + if (columnIndex == 0) { + dataStart = 0; + switch (sizeOfMetadata) { + case SPMySQLStoreMetadataAsChar: + dataLength = ((unsigned char *)rowData)[columnIndex]; + break; + case SPMySQLStoreMetadataAsShort: + dataLength = ((unsigned short *)rowData)[columnIndex]; + break; + case SPMySQLStoreMetadataAsLong: + default: + dataLength = ((unsigned long *)rowData)[columnIndex]; + break; + } + } else { + switch (sizeOfMetadata) { + case SPMySQLStoreMetadataAsChar: + dataStart = ((unsigned char *)rowData)[columnIndex - 1]; + dataLength = ((unsigned char *)rowData)[columnIndex] - dataStart; + break; + case SPMySQLStoreMetadataAsShort: + dataStart = ((unsigned short *)rowData)[columnIndex - 1]; + dataLength = ((unsigned short *)rowData)[columnIndex] - dataStart; + break; + case SPMySQLStoreMetadataAsLong: + default: + dataStart = ((unsigned long *)rowData)[columnIndex - 1]; + dataLength = ((unsigned long *)rowData)[columnIndex] - dataStart; + break; + } + + } + + // If the data length is empty, check whether the cell is null and return null if so + if (((BOOL *)(rowData + (sizeOfMetadata * numberOfFields)))[columnIndex]) { + return NSNullPointer; + } + + // Get a reference to the start of the cell data + rawCellDataStart = rowData + ((sizeOfMetadata + sizeOfNullRecord) * numberOfFields) + dataStart; + + // Attempt to convert to the correct native object type, which will result in nil on error/invalidity + cellData = SPMySQLResultGetObject(self, rawCellDataStart, dataLength, columnIndex, previewLength); + + // If object creation failed, use a null + if (!cellData) { + cellData = NSNullPointer; + } + + return cellData; +} + +/** + * Returns whether the data at a specified row and column index is NULL. + */ +- (BOOL)cellIsNullAtRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex +{ + // Throw an exception if the row or column index is out of bounds + if (rowIndex >= numberOfRows || columnIndex >= numberOfFields) { + [NSException raise:NSRangeException format:@"Requested storage index (row %llu, col %llu) beyond bounds (%llu, %llu)", (unsigned long long)rowIndex, (unsigned long long)columnIndex, (unsigned long long)numberOfRows, (unsigned long long)numberOfFields]; + } + + SPMySQLStreamingResultStoreRowData *rowData = dataStorage[rowIndex]; + + // A null pointer for the row indicates a dummy entry + if (rowData == NULL) { + return NO; + } + + size_t sizeOfMetadata; + + // Get the metadata size for this row and adjust the data pointer past the indicator + sizeOfMetadata = rowData[0]; + rowData = rowData + 1; + + // Check whether the cell is null + return (((BOOL *)(rowData + (sizeOfMetadata * numberOfFields)))[columnIndex]); + +} + +#pragma mark - Data retrieval overrides + +/** + * Override the standard fetch and convenience selectors to indicate the difference in use + */ +- (id)getRow +{ + return SPMySQLResultGetRow(self, SPMySQLResultRowAsDefault); +} +- (NSArray *)getRowAsArray +{ + return SPMySQLResultGetRow(self, SPMySQLResultRowAsArray); +} +- (NSDictionary *)getRowAsDictionary +{ + return SPMySQLResultGetRow(self, SPMySQLResultRowAsDictionary); +} +- (id)getRowAsType:(SPMySQLResultRowType)theType +{ + [NSException raise:NSInternalInconsistencyException format:@"Streaming SPMySQL result store sets should be used directly as result stores."]; + return nil; +} + +/* + * Ensure the result set is fully processed and freed without any processing + * This method ensures that the connection is unlocked. + */ +- (void)cancelResultLoad +{ + + // Track that loading has been cancelled, allowing faster result download without processing + loadCancelled = YES; + + if (!loadStarted) { + [self startDownload]; + } + + // Loop until all data is processed, using a usleep (migrate to pthread condition variable?). + // This waits on the data download thread (see _downloadAllData) to fetch all rows from the + // server result set to avoid MySQL issues. + while (!dataDownloaded) { + usleep(1000); + } +} + +#pragma mark - Data retrieval for fast enumeration + +/** + * Implement the fast enumeration endpoint. Rows for fast enumeration are retrieved in + * as NSArrays. + * Note that rows are currently retrieved individually to avoid mutation and locking issues, + * although this could be improved on. + */ +- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len +{ + NSMutableArray *theRow = SPMySQLResultStoreGetRow(self, state->state); + + // If no row was available, return 0 to stop iteration. + if (!theRow) return 0; + + // Otherwise, add the item to the buffer and return the appropriate state. + stackbuf[0] = theRow; + + state->state += 1; + state->itemsPtr = stackbuf; + state->mutationsPtr = (unsigned long *)self; + + return 1; +} + +#pragma mark - Addition of placeholder rows and deletion of rows + +/** + * Add a placeholder row to the end of the result set, comprising of a pointer + * to NULL. This is to allow classes wrapping the result store to provide + * editing capabilities before saving rows directly back to MySQL. + */ +- (void) addDummyRow +{ + + // Currently only support editing after loading is finished; thi could be addressed by checking rowDownloadIterator vs numberOfRows etc + if (!dataDownloaded) { + [NSException raise:NSInternalInconsistencyException format:@"Streaming SPMySQL result editing is currently only supported once loading is complete."]; + } + + // Lock the data mutex + pthread_mutex_lock(&dataLock); + + // Ensure that sufficient capacity is available + SPMySQLStreamingResultStoreEnsureCapacityForAdditionalRowCount(self, 1); + + // Add a dummy entry to the data store + dataStorage[numberOfRows] = NULL; + numberOfRows++; + + // Unlock the mutex + pthread_mutex_unlock(&dataLock); +} + +/** + * Insert a placeholder row into the result set at the specified index, comprising + * of a pointer to NULL. This is to allow classes wrapping the result store to + * provide editing capabilities before saving rows directly back to MySQL. + */ +- (void) insertDummyRowAtIndex:(NSUInteger)anIndex +{ + // Throw an exception if the index is out of bounds + if (anIndex > numberOfRows) { + [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", (unsigned long long)anIndex, (unsigned long long)numberOfRows]; + } + + // Currently only support editing after loading is finished; this could be addressed by checking rowDownloadIterator vs numberOfRows etc + if (!dataDownloaded) { + [NSException raise:NSInternalInconsistencyException format:@"Streaming SPMySQL result editing is currently only supported once loading is complete."]; + } + + // If "inserting" at the end of the array just add a row + if (anIndex == numberOfRows) { + return [self addDummyRow]; + } + + // Lock the data mutex + pthread_mutex_lock(&dataLock); + + // Ensure that sufficient capacity is available to hold all the rows + SPMySQLStreamingResultStoreEnsureCapacityForAdditionalRowCount(self, 1); + + // Reindex the specified index, and all subsequent indices, to create a gap + size_t pointerSize = sizeof(SPMySQLStreamingResultStoreRowData *); + memmove(dataStorage + anIndex + 1, dataStorage + anIndex, (numberOfRows - anIndex) * pointerSize); + + // Add a null pointer at the specified location + dataStorage[anIndex] = NULL; + numberOfRows++; + + // Unlock the mutex + pthread_mutex_unlock(&dataLock); +} + +/** + * Delete a row at the specified index from the result set. This allows the program + * to remove or reorder rows without having to reload the entire result set from the + * server. + */ +- (void) removeRowAtIndex:(NSUInteger)anIndex +{ + + // Throw an exception if the index is out of bounds + if (anIndex > numberOfRows) { + [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", (unsigned long long)anIndex, (unsigned long long)numberOfRows]; + } + + // Lock the data mutex + pthread_mutex_lock(&dataLock); + + // Free the row data + SPMySQLStreamingResultStoreFreeRowData(dataStorage[anIndex]); + numberOfRows--; + + // Renumber all subsequent indices to fill the gap + size_t pointerSize = sizeof(SPMySQLStreamingResultStoreRowData *); + memmove(dataStorage + anIndex, dataStorage + anIndex + 1, (numberOfRows - anIndex) * pointerSize); + + // Unlock the mutex + pthread_mutex_unlock(&dataLock); +} + +/** + * Delete a set of rows at the specified result index range from the result set. This + * allows the program to remove or reorder rows without having to reload the entire result + * set from the server. + */ +- (void) removeRowsInRange:(NSRange)rangeToRemove +{ + + // Throw an exception if the range is out of bounds + if (rangeToRemove.location + rangeToRemove.length > numberOfRows) { + [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", (unsigned long long)(rangeToRemove.location + rangeToRemove.length), (unsigned long long)numberOfRows]; + } + + // Lock the data mutex + pthread_mutex_lock(&dataLock); + + // Free rows in the range + NSUInteger i; + for (i = rangeToRemove.location; i < rangeToRemove.location + rangeToRemove.length; i++) { + SPMySQLStreamingResultStoreFreeRowData(dataStorage[i]); + } + numberOfRows -= rangeToRemove.length; + + // Renumber all subsequent indices to fill the gap + size_t pointerSize = sizeof(SPMySQLStreamingResultStoreRowData *); + memmove(dataStorage + rangeToRemove.location, dataStorage + rangeToRemove.location + rangeToRemove.length, (numberOfRows - rangeToRemove.location) * pointerSize); + + // Unlock the mutex + pthread_mutex_unlock(&dataLock); +} + +/** + * Clear the result set, allowing truncation of the result set without needing an extra query + * to return an empty set from the server. + */ +- (void) removeAllRows +{ + + // Lock the data mutex + pthread_mutex_lock(&dataLock); + + // Free all the data + while (numberOfRows > 0) { + SPMySQLStreamingResultStoreFreeRowData(dataStorage[--numberOfRows]); + } + + // Unlock the mutex + pthread_mutex_unlock(&dataLock); +} + +@end + +#pragma mark - Result set internals + +@implementation SPMySQLStreamingResultStore (PrivateAPI) + +/** + * Used internally to download results in a background thread, downloading + * the entire result set as MySQL data (and data lengths) to the internal + * storage. + */ +- (void)_downloadAllData +{ + NSAutoreleasePool *downloadPool = [[NSAutoreleasePool alloc] init]; + MYSQL_ROW theRow; + unsigned long *fieldLengths; + NSUInteger i, dataCopiedLength, rowDataLength; + SPMySQLStreamingResultStoreRowData *newRowStore; + + [[NSThread currentThread] setName:@"SPMySQLStreamingResultStore data download thread"]; + + size_t sizeOfMetadata, lengthOfMetadata; + size_t lengthOfNullRecords = (size_t)(sizeof(BOOL) * 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)) + ) + { + + // If the load has been cancelled, skip any processing - we're only interested + // in ensuring that mysql_fetch_row is called for all rows. + if (loadCancelled) { + continue; + } + + // The row store is a single block of memory. It's made up of four blocks of data: + // Firstly, a single char containing the type of data used to store positions. + // Secondly, a series of those types recording the *end position* of each field + // Thirdly, a series of BOOLs recording whether the fields are NULLS - which can't just be from length + // Finally, a char sequence comprising the actual cell data, which can be looked up by position/length. + + // Retrieve the lengths of the returned data, and calculate the overall length of data + fieldLengths = mysql_fetch_lengths(resultSet); + rowDataLength = 0; + for (i = 0; i < numberOfFields; i++) { + rowDataLength += fieldLengths[i]; + } + + // Depending on the length of the row, vary the metadata size appropriately. This + // makes defining the data processing much lengthier, but is worth it to reduce the + // overhead for small rows. + if (rowDataLength <= UCHAR_MAX) { + sizeOfMetadata = SPMySQLStoreMetadataAsChar; + } else if (rowDataLength <= USHRT_MAX) { + sizeOfMetadata = SPMySQLStoreMetadataAsShort; + } else { + sizeOfMetadata = SPMySQLStoreMetadataAsLong; + } + lengthOfMetadata = sizeOfMetadata * numberOfFields; + + // Allocate the memory for the row and set the type marker + newRowStore = malloc_zone_malloc(storageMallocZone, 1 + lengthOfMetadata + lengthOfNullRecords + (rowDataLength * sizeOfChar)); + newRowStore[0] = sizeOfMetadata; + + // Set the data end positions. Manually unroll the logic for the different cases; messy + // but again worth the large memory savings for smaller rows + rowDataLength = 0; + switch (sizeOfMetadata) { + case SPMySQLStoreMetadataAsLong: + for (i = 0; i < numberOfFields; i++) { + rowDataLength += fieldLengths[i]; + ((unsigned long *)(newRowStore + 1))[i] = rowDataLength; + ((BOOL *)(newRowStore + 1 + lengthOfMetadata))[i] = (theRow[i] == NULL); + } + break; + case SPMySQLStoreMetadataAsShort: + for (i = 0; i < numberOfFields; i++) { + rowDataLength += fieldLengths[i]; + ((unsigned short *)(newRowStore + 1))[i] = rowDataLength; + ((BOOL *)(newRowStore + 1 + lengthOfMetadata))[i] = (theRow[i] == NULL); + } + break; + case SPMySQLStoreMetadataAsChar: + for (i = 0; i < numberOfFields; i++) { + rowDataLength += fieldLengths[i]; + ((unsigned char *)(newRowStore + 1))[i] = rowDataLength; + ((BOOL *)(newRowStore + 1 + lengthOfMetadata))[i] = (theRow[i] == NULL); + } + break; + } + + // If the row has content, copy it in + if (rowDataLength) { + dataCopiedLength = 1 + lengthOfMetadata + lengthOfNullRecords; + for (i = 0; i < numberOfFields; i++) { + if (theRow[i] != NULL) { + memcpy(newRowStore + dataCopiedLength, theRow[i], fieldLengths[i]); + dataCopiedLength += fieldLengths[i]; + } + } + } + + // Lock the data mutex + pthread_mutex_lock(&dataLock); + + // Ensure that sufficient capacity is available + SPMySQLStreamingResultStoreEnsureCapacityForAdditionalRowCount(self, 1); + + // Add the newly allocated row to the storage + if (rowDownloadIterator < numberOfRows) { + SPMySQLStreamingResultStoreFreeRowData(dataStorage[rowDownloadIterator]); + } + dataStorage[rowDownloadIterator] = newRowStore; + rowDownloadIterator++; + + // Update the total row count if exceeded + if (rowDownloadIterator > numberOfRows) { + numberOfRows++; + } + + // Unlock the mutex + pthread_mutex_unlock(&dataLock); + } + + // Update the total number of rows in the result set now download + // is complete, freeing extra rows from a previous result set + if (numberOfRows > rowDownloadIterator) { + pthread_mutex_lock(&dataLock); + while (numberOfRows > rowDownloadIterator) { + SPMySQLStreamingResultStoreFreeRowData(dataStorage[--numberOfRows]); + } + 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; + + // If the connection query may have been cancelled with a query kill, double-check connection + if ([parentConnection lastQueryWasCancelled] && [parentConnection serverMajorVersion] < 5) { + [parentConnection checkConnection]; + } + + dataDownloaded = YES; + + // Inform the delegate the download was completed + if ([delegate respondsToSelector:@selector(resultStoreDidFinishLoadingData:)]) { + [delegate resultStoreDidFinishLoadingData:self]; + } + + [downloadPool drain]; +} + +/** + * Private method to ensure the storage array always has sufficient capacity + * to store any additional rows required. + */ +- (void) _ensureCapacityForAdditionalRowCount:(NSUInteger)numExtraRows +{ + while (numberOfRows + numExtraRows > rowCapacity) { + [self _increaseCapacity]; + } +} + +/** + * Private method to increase the storage available for the array; + * currently doubles the capacity as boundaries are reached. + */ +- (void) _increaseCapacity +{ + rowCapacity *= 2; + dataStorage = malloc_zone_realloc(storageMallocZone, dataStorage, rowCapacity * sizeof(SPMySQLStreamingResultStoreRowData *)); +} + +/** + * Private method to return the internal result store capacity. + */ +- (NSUInteger) _rowCapacity +{ + return rowCapacity; +} + +/** + * Private method to return the internal result store, relinquishing + * ownership to allow transfer of data. Note that the returned result + * store will be allocated memory which will need freeing. + */ +- (SPMySQLStreamingResultStoreRowData **) _transferResultStoreData +{ + if (!dataDownloaded) { + [NSException raise:NSInternalInconsistencyException format:@"Attempted to transfer result store data before loading completed"]; + } + + SPMySQLStreamingResultStoreRowData **previousData = dataStorage; + + pthread_mutex_lock(&dataLock); + dataStorage = NULL; + storageMallocZone = NULL; + rowCapacity = 0; + numberOfRows = 0; + pthread_mutex_unlock(&dataLock); + + return previousData; +} + +@end diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResultStoreDelegate.h b/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResultStoreDelegate.h new file mode 100644 index 00000000..ef5a05e2 --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResultStoreDelegate.h @@ -0,0 +1,45 @@ +// +// $Id$ +// +// SPMySQLStreamingResultStoreDelegate.h +// SPMySQLFramework +// +// Created by Rowan Beentje (rowan.beent.je) on July 27, 2013 +// Copyright (c) 2013 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 SPMySQLStreamingResultStore; + +@protocol SPMySQLStreamingResultStoreDelegate <NSObject> +@optional + +/** + * Notifies the delegate that loading of data has completed. + * + * @param resultStore The result store that has finished loading data + */ +- (void)resultStoreDidFinishLoadingData:(SPMySQLStreamingResultStore *)resultStore; + +@end |