diff options
31 files changed, 2046 insertions, 670 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 diff --git a/Source/SPCopyTable.m b/Source/SPCopyTable.m index 212a545c..0fa58711 100644 --- a/Source/SPCopyTable.m +++ b/Source/SPCopyTable.m @@ -800,8 +800,8 @@ static const NSInteger kBlobAsImageFile = 4; maxCellWidth = 0; for (i = 0; i < rowsToCheck; i += rowStep) { - // Retrieve the cell's content - contentString = [tableStorage cellDataAtRow:i column:columnIndex]; + // Retrieve part of the cell's content to get widths, topping out at a maximum length + contentString = SPDataStoragePreviewAtRowAndColumn(tableStorage, i, columnIndex, 500); // If the cell hasn't loaded yet, skip processing if (!contentString) diff --git a/Source/SPCustomQuery.h b/Source/SPCustomQuery.h index c0e309af..528b8209 100644 --- a/Source/SPCustomQuery.h +++ b/Source/SPCustomQuery.h @@ -57,7 +57,7 @@ @class SPSplitView; @class SPFieldEditorController; @class SPMySQLConnection; -@class SPMySQLFastStreamingResult; +@class SPMySQLStreamingResultStore; @class SPTextView; #ifdef SP_CODA @@ -162,6 +162,7 @@ SPDataStorage *resultData; pthread_mutex_t resultDataLock; + NSCondition *resultLoadingCondition; NSInteger resultDataCount; NSArray *cqColumnDefinition; NSString *lastExecutedQuery; @@ -257,7 +258,7 @@ - (NSArray *)currentResult; - (NSArray *)currentDataResultWithNULLs:(BOOL)includeNULLs truncateDataFields:(BOOL)truncate; - (NSUInteger)currentResultRowCount; -- (void)processResultIntoDataStorage:(SPMySQLFastStreamingResult *)theResult; +- (void)updateResultStore:(SPMySQLStreamingResultStore *)theResultStore; // Retrieving and setting table state - (void)updateTableView; diff --git a/Source/SPCustomQuery.m b/Source/SPCustomQuery.m index 686e700b..27fc11c6 100644 --- a/Source/SPCustomQuery.m +++ b/Source/SPCustomQuery.m @@ -69,8 +69,7 @@ @interface SPCustomQuery (PrivateAPI) -- (id)_resultDataItemAtRow:(NSInteger)row columnIndex:(NSUInteger)column; -- (id)_convertResultDataValueToDisplayableRepresentation:(id)value whilePreservingNULLs:(BOOL)preserveNULLs truncateDataFields:(BOOL)truncate; +- (id)_resultDataItemAtRow:(NSInteger)row columnIndex:(NSUInteger)column preserveNULLs:(BOOL)preserveNULLs asPreview:(BOOL)asPreview; + (NSString *)linkToHelpTopic:(NSString *)aTopic; @end @@ -581,7 +580,7 @@ { NSAutoreleasePool *queryRunningPool = [[NSAutoreleasePool alloc] init]; NSArray *queries = [taskArguments objectForKey:@"queries"]; - SPMySQLFastStreamingResult *streamingResult = nil; + SPMySQLStreamingResultStore *resultStore = nil; NSMutableString *errors = [NSMutableString string]; SEL callbackMethod = NULL; NSString *taskButtonString; @@ -652,8 +651,8 @@ [tempQueries addObject:query]; // Run the query, timing execution (note this also includes network and overhead) - streamingResult = [[mySQLConnection streamingQueryString:query] retain]; - executionTime += [streamingResult queryExecutionTime]; + resultStore = [[mySQLConnection resultStoreFromQueryString:query] retain]; + executionTime += [resultStore queryExecutionTime]; totalQueriesRun++; // If this is the last query, retrieve and store the result; otherwise, @@ -662,7 +661,7 @@ // Retrieve and cache the column definitions for the result array if (cqColumnDefinition) [cqColumnDefinition release]; - cqColumnDefinition = [[streamingResult fieldDefinitions] retain]; + cqColumnDefinition = [[resultStore fieldDefinitions] retain]; if(!reloadingExistingResult) { [[self onMainThread] updateTableView]; @@ -683,18 +682,18 @@ // Init copyTable with necessary information for copying selected rows as SQL INSERT [customQueryView setTableInstance:self withTableData:resultData withColumns:cqColumnDefinition withTableName:resultTableName withConnection:mySQLConnection]; - [self processResultIntoDataStorage:streamingResult]; + [self updateResultStore:resultStore]; } else { - [streamingResult cancelResultLoad]; + [resultStore cancelResultLoad]; } // Record any affected rows if ( [mySQLConnection rowsAffectedByLastQuery] != (unsigned long long)~0 ) totalAffectedRows += (NSUInteger)[mySQLConnection rowsAffectedByLastQuery]; - else if ( [streamingResult numberOfRows] ) - totalAffectedRows += (NSUInteger)[streamingResult numberOfRows]; + else if ( [resultStore numberOfRows] ) + totalAffectedRows += (NSUInteger)[resultStore numberOfRows]; - [streamingResult release]; + [resultStore release]; // Store any error messages if ([mySQLConnection queryErrored] || [mySQLConnection lastQueryWasCancelled]) { @@ -804,8 +803,8 @@ // Perform empty query if no query is given if ( !queryCount ) { - streamingResult = [mySQLConnection streamingQueryString:@""]; - [streamingResult cancelResultLoad]; + resultStore = [mySQLConnection resultStoreFromQueryString:@""]; + [resultStore cancelResultLoad]; [errors setString:[mySQLConnection lastErrorMessage]]; } @@ -944,55 +943,38 @@ } /** - * Processes a supplied streaming result set, loading it into the data array. + * Processes a supplied streaming result store, monitoring the load and updating + * the data displayed during download. */ -- (void)processResultIntoDataStorage:(SPMySQLFastStreamingResult *)theResult +- (void)updateResultStore:(SPMySQLStreamingResultStore *)theResultStore { - NSAutoreleasePool *dataLoadingPool; // Remove all items from the table resultDataCount = 0; [customQueryView performSelectorOnMainThread:@selector(noteNumberOfRowsChanged) withObject:nil waitUntilDone:YES]; pthread_mutex_lock(&resultDataLock); [resultData removeAllRows]; + + // Add the new store + [resultData setDataStorage:theResultStore updatingExisting:NO]; pthread_mutex_unlock(&resultDataLock); - // Set the column count on the data store before setting up anything else - - // ensures that SPDataStorage is set up for timer-driven data loads - [resultData setColumnCount:[theResult numberOfFields]]; + // Start the data downloading + [theResultStore startDownload]; - // Set up the table updates timer + // Set up the table updates timer and wait for it to notify this thread about completion [[self onMainThread] initQueryLoadTimer]; - // Set up an autorelease pool for row processing - dataLoadingPool = [[NSAutoreleasePool alloc] init]; - - // Loop through the result rows as they become available - for (NSArray *eachRow in theResult) { - - pthread_mutex_lock(&resultDataLock); - SPDataStorageAddRow(resultData, eachRow); - resultDataCount++; - pthread_mutex_unlock(&resultDataLock); - - // Drain and reset the autorelease pool every ~1024 rows - if (!(resultDataCount % 1024)) { - [dataLoadingPool drain]; - dataLoadingPool = [[NSAutoreleasePool alloc] init]; - } + [resultLoadingCondition lock]; + while (![resultData dataDownloaded]) { + [resultLoadingCondition waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]]; } - - // Clean up the interface update timer - [[self onMainThread] clearQueryLoadTimer]; + [resultLoadingCondition unlock]; // If the final column autoresize wasn't performed, perform it if (queryLoadLastRowCount < 200) [[self onMainThread] autosizeColumns]; [customQueryView performSelectorOnMainThread:@selector(noteNumberOfRowsChanged) withObject:nil waitUntilDone:NO]; - [customQueryView setNeedsDisplay:YES]; - - // Clean up the autorelease pool - [dataLoadingPool drain]; } /** @@ -1483,11 +1465,20 @@ */ - (void) queryLoadUpdate:(NSTimer *)theTimer { + resultDataCount = [resultData count]; + if (queryLoadTimerTicksSinceLastUpdate < queryLoadInterfaceUpdateInterval) { queryLoadTimerTicksSinceLastUpdate++; return; } + if ([resultData dataDownloaded]) { + [resultLoadingCondition lock]; + [resultLoadingCondition signal]; + [self clearQueryLoadTimer]; + [resultLoadingCondition unlock]; + } + // Check whether a table update is required, based on whether new rows are // available to display. if (resultDataCount == (NSInteger)queryLoadLastRowCount) { @@ -1496,7 +1487,6 @@ // Update the table display [customQueryView noteNumberOfRowsChanged]; - if (!queryLoadLastRowCount) [customQueryView setNeedsDisplay:YES]; // Update column widths in two cases: on very first rows displayed, and once // more than 200 rows are present. @@ -1574,9 +1564,7 @@ while ((tableColumn = [enumerator nextObject])) { - id value = [self _resultDataItemAtRow:i columnIndex:[[tableColumn identifier] integerValue]]; - - [tempRow addObject:[self _convertResultDataValueToDisplayableRepresentation:value whilePreservingNULLs:includeNULLs truncateDataFields:truncate]]; + [tempRow addObject:[self _resultDataItemAtRow:i columnIndex:[[tableColumn identifier] integerValue] preserveNULLs:includeNULLs asPreview:truncate]]; } [currentResult addObject:[NSArray arrayWithArray:tempRow]]; @@ -1682,7 +1670,6 @@ [dataCell setLineBreakMode:NSLineBreakByTruncatingTail]; [dataCell setFormatter:[[SPDataCellFormatter new] autorelease]]; - [[dataCell formatter] setDisplayLimit:150]; // Set field length limit if field is a varchar to match varchar length if ([[columnDefinition objectForKey:@"typegrouping"] isEqualToString:@"string"] @@ -2045,7 +2032,7 @@ } else { #endif // otherwise, just update the data in the data storage - SPDataStorageReplaceObjectAtRowAndColumn(resultData, rowIndex, [[aTableColumn identifier] intValue], anObject); + [resultData replaceObjectInRow:rowIndex column:[[aTableColumn identifier] intValue] withObject:anObject]; #ifndef SP_CODA } #endif @@ -2075,35 +2062,34 @@ { if (aTableView == customQueryView) { + if (![cell respondsToSelector:@selector(setTextColor:)]) { + return; + } + // For NULL cell's display the user's NULL value placeholder in grey to easily distinguish it from other values - if ([cell respondsToSelector:@selector(setTextColor:)]) { - - id value = nil; - NSUInteger columnIndex = [[aTableColumn identifier] integerValue]; - - // While the table is being loaded, additional validation is required - data - // locks must be used to avoid crashes, and indexes higher than the available - // rows or columns may be requested. Use gray to show loading in these cases. - if (isWorking) { - pthread_mutex_lock(&resultDataLock); - - if (rowIndex < resultDataCount && columnIndex < [resultData columnCount]) { - value = SPDataStorageObjectAtRowAndColumn(resultData, rowIndex, columnIndex); - } - - pthread_mutex_unlock(&resultDataLock); + BOOL showCellAsGray = NO; - if (!value) { - [cell setTextColor:[NSColor lightGrayColor]]; - return; - } - } - else { - value = SPDataStorageObjectAtRowAndColumn(resultData, rowIndex, columnIndex); + NSUInteger columnIndex = [[aTableColumn identifier] integerValue]; + + // While the table is being loaded, additional validation is required - data + // locks must be used to avoid crashes, and indexes higher than the available + // rows or columns may be requested. Use gray to show loading in these cases. + if (isWorking) { + pthread_mutex_lock(&resultDataLock); + + if (rowIndex < resultDataCount && columnIndex < [resultData columnCount]) { + showCellAsGray = [resultData cellIsNullOrUnloadedAtRow:rowIndex column:columnIndex]; + } else { + showCellAsGray = YES; } - [cell setTextColor:[value isNSNull] ? [NSColor lightGrayColor] : [NSColor blackColor]]; + pthread_mutex_unlock(&resultDataLock); } + else { + showCellAsGray = [resultData cellIsNullOrUnloadedAtRow:rowIndex column:columnIndex]; + } + + [cell setTextColor:showCellAsGray ? [NSColor lightGrayColor] : [NSColor blackColor]]; } } @@ -2113,8 +2099,7 @@ - (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)rowIndex { if (aTableView == customQueryView) { - - return [self _convertResultDataValueToDisplayableRepresentation:[self _resultDataItemAtRow:rowIndex columnIndex:[[tableColumn identifier] integerValue]] whilePreservingNULLs:NO truncateDataFields:YES]; + return [self _resultDataItemAtRow:rowIndex columnIndex:[[tableColumn identifier] integerValue] preserveNULLs:NO asPreview:YES]; } return @""; @@ -3768,6 +3753,7 @@ runPrimaryActionButtonAsSelection = nil; queryLoadTimer = nil; + resultLoadingCondition = [NSCondition new]; prefs = [NSUserDefaults standardUserDefaults]; @@ -3988,12 +3974,13 @@ /** * Retrieves the value from the underlying data storage at the supplied row and column indices. * - * @param row The row index - * @param column The column index + * @param row The row index + * @param column The column index + * @param preserveNULLs Whether t * * @return The value from the data storage */ -- (id)_resultDataItemAtRow:(NSInteger)row columnIndex:(NSUInteger)column +- (id)_resultDataItemAtRow:(NSInteger)row columnIndex:(NSUInteger)column preserveNULLs:(BOOL)preserveNULLs asPreview:(BOOL)asPreview; { id value = nil; @@ -4005,7 +3992,11 @@ pthread_mutex_lock(&resultDataLock); if (row < resultDataCount && column < [resultData columnCount]) { - value = [[SPDataStorageObjectAtRowAndColumn(resultData, row, column) copy] autorelease]; + if (asPreview) { + value = SPDataStoragePreviewAtRowAndColumn(resultData, row, column, 150); + } else { + value = SPDataStorageObjectAtRowAndColumn(resultData, row, column); + } } pthread_mutex_unlock(&resultDataLock); @@ -4013,36 +4004,23 @@ if (!value) value = @"..."; } else { - value = SPDataStorageObjectAtRowAndColumn(resultData, row, column); + if (asPreview) { + value = SPDataStoragePreviewAtRowAndColumn(resultData, row, column, 150); + } else { + value = SPDataStorageObjectAtRowAndColumn(resultData, row, column); + } } - - return value; -} -/** - * Converts the supplied value into it's displayable representation. - * - * @param value The value to convert - * @param preserveNULLs Whether or not NULLs should be preserved or converted to the - * user's NULL placeholder preference. - * @param truncate Whether or not data fields should be truncates for display purposes. - * - * @return The converted value - */ -- (id)_convertResultDataValueToDisplayableRepresentation:(id)value whilePreservingNULLs:(BOOL)preserveNULLs truncateDataFields:(BOOL)truncate -{ + if ([value isKindOfClass:[SPMySQLGeometryData class]]) + return [value wktString]; + + if ([value isNSNull]) + return preserveNULLs ? value : [prefs objectForKey:SPNullValue]; + if ([value isKindOfClass:[NSData class]]) { - value = truncate ? [value shortStringRepresentationUsingEncoding:[mySQLConnection stringEncoding]] : [value stringRepresentationUsingEncoding:[mySQLConnection stringEncoding]]; + return [value stringRepresentationUsingEncoding:[mySQLConnection stringEncoding]]; } - - if ([value isNSNull] && !preserveNULLs) { - value = [prefs objectForKey:SPNullValue]; - } - - if ([value isKindOfClass:[SPMySQLGeometryData class]]) { - value = [value wktString]; - } - + return value; } @@ -4057,6 +4035,7 @@ [NSObject cancelPreviousPerformRequestsWithTarget:customQueryView]; [self clearQueryLoadTimer]; + [resultLoadingCondition release]; [usedQuery release]; [lastExecutedQuery release]; [resultData release]; diff --git a/Source/SPDataCellFormatter.h b/Source/SPDataCellFormatter.h index d1f9c7d4..00cc85bb 100644 --- a/Source/SPDataCellFormatter.h +++ b/Source/SPDataCellFormatter.h @@ -33,12 +33,10 @@ @interface SPDataCellFormatter : NSFormatter { NSInteger textLimit; - NSUInteger displayLimit; NSString *fieldType; } @property (readwrite, assign) NSInteger textLimit; -@property (readwrite, assign) NSUInteger displayLimit; @property (readwrite, retain) NSString* fieldType; @end diff --git a/Source/SPDataCellFormatter.m b/Source/SPDataCellFormatter.m index 951fafbc..616e59db 100644 --- a/Source/SPDataCellFormatter.m +++ b/Source/SPDataCellFormatter.m @@ -36,24 +36,10 @@ @implementation SPDataCellFormatter @synthesize textLimit; -@synthesize displayLimit; @synthesize fieldType; -- (id)init -{ - if ((self = [super init])) { - displayLimit = NSNotFound; - } - return self; -} - - (NSString *)stringForObjectValue:(id)anObject { - // Truncate the string for speed purposes if it's very long - improves table scrolling speed. - if (displayLimit != NSNotFound && [anObject isKindOfClass:[NSString class]] && [(NSString *)anObject length] > displayLimit) { - return ([NSString stringWithFormat:@"%@...", [anObject substringToIndex:displayLimit - 3]]); - } - if (![anObject isKindOfClass:[NSString class]]) { return [anObject description]; } diff --git a/Source/SPDataStorage.h b/Source/SPDataStorage.h index 39d36d5f..d0c1f556 100644 --- a/Source/SPDataStorage.h +++ b/Source/SPDataStorage.h @@ -30,45 +30,54 @@ // // More info at <http://code.google.com/p/sequel-pro/> +#import <SPMySQL/SPMySQLStreamingResultStoreDelegate.h> + +@class SPMySQLStreamingResultStore; + /** - * This class provides a storage mechanism intended to represent tabular - * data, in a 2D array. Data can be added and retrieved either directly - * or via NSArrays; internally, C arrays are used to provide speed and - * memory improvements. - * This class is essentially mutable. + * This class wraps a SPMySQLStreamingResultStore, providing an editable + * data store; on a fresh load all data will be proxied from the underlying + * result store, but if cells or rows are edited, mutable rows are stored + * directly. */ -@interface SPDataStorage : NSObject +@interface SPDataStorage : NSObject <SPMySQLStreamingResultStoreDelegate> { - NSUInteger numColumns; - NSUInteger columnPointerByteSize; - NSUInteger numRows, numRowsCapacity; + SPMySQLStreamingResultStore *dataStorage; + NSPointerArray *editedRows; + BOOL *unloadedColumns; - id **dataStorage; + NSUInteger numberOfColumns; } +/* Setting result store */ +- (void) setDataStorage:(SPMySQLStreamingResultStore *) newDataStorage updatingExisting:(BOOL)updateExistingStore; + /* Retrieving rows and cells */ -- (NSMutableArray *) rowContentsAtIndex:(NSUInteger)index; +- (NSMutableArray *) rowContentsAtIndex:(NSUInteger)anIndex; - (id) cellDataAtRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex; +- (id) cellPreviewAtRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex previewLength:(NSUInteger)previewLength; +- (BOOL) cellIsNullOrUnloadedAtRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex; /* Adding and amending rows and cells */ -- (void) addRowWithContents:(NSArray *)row; -- (void) insertRowContents:(NSArray *)row atIndex:(NSUInteger)index; -- (void) replaceRowAtIndex:(NSUInteger)index withRowContents:(NSArray *)row; -- (void) replaceObjectInRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex withObject:(id)object; -- (void) removeRowAtIndex:(NSUInteger)index; +- (void) addRowWithContents:(NSMutableArray *)aRow; +- (void) insertRowContents:(NSMutableArray *)aRow atIndex:(NSUInteger)anIndex; +- (void) replaceRowAtIndex:(NSUInteger)anIndex withRowContents:(NSMutableArray *)aRow; +- (void) replaceObjectInRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex withObject:(id)anObject; +- (void) removeRowAtIndex:(NSUInteger)anIndex; - (void) removeRowsInRange:(NSRange)rangeToRemove; - (void) removeAllRows; +/* Unloaded columns */ +- (void) setColumnAsUnloaded:(NSUInteger)columnIndex; + /* Basic information */ - (NSUInteger) count; -- (void) setColumnCount:(NSUInteger)columnCount; - (NSUInteger) columnCount; +- (BOOL) dataDownloaded; -/* Initialisation and teardown */ -#pragma mark - -- (id) init; -- (void) dealloc; +/* Delegate callback methods */ +- (void)resultStoreDidFinishLoadingData:(SPMySQLStreamingResultStore *)resultStore; @end @@ -91,14 +100,6 @@ static inline void SPDataStorageReplaceRow(SPDataStorage* self, NSUInteger rowIn SPDSReplaceRow(self, @selector(replaceRowAtIndex:withRowContents:), rowIndex, row); } -static inline void SPDataStorageReplaceObjectAtRowAndColumn(SPDataStorage* self, NSUInteger rowIndex, NSUInteger colIndex, id newObject) -{ - typedef void (*SPDSObjectReplaceMethodPtr)(SPDataStorage*, SEL, NSUInteger, NSUInteger, id); - static SPDSObjectReplaceMethodPtr SPDSObjectReplace; - if (!SPDSObjectReplace) SPDSObjectReplace = (SPDSObjectReplaceMethodPtr)[self methodForSelector:@selector(replaceObjectInRow:column:withObject:)]; - SPDSObjectReplace(self, @selector(replaceObjectInRow:column:withObject:), rowIndex, colIndex, newObject); -} - static inline id SPDataStorageObjectAtRowAndColumn(SPDataStorage* self, NSUInteger rowIndex, NSUInteger colIndex) { typedef id (*SPDSObjectFetchMethodPtr)(SPDataStorage*, SEL, NSUInteger, NSUInteger); @@ -106,3 +107,12 @@ static inline id SPDataStorageObjectAtRowAndColumn(SPDataStorage* self, NSUInteg if (!SPDSObjectFetch) SPDSObjectFetch = (SPDSObjectFetchMethodPtr)[self methodForSelector:@selector(cellDataAtRow:column:)]; return SPDSObjectFetch(self, @selector(cellDataAtRow:column:), rowIndex, colIndex); } + +static inline id SPDataStoragePreviewAtRowAndColumn(SPDataStorage* self, NSUInteger rowIndex, NSUInteger colIndex, NSUInteger previewLength) +{ + typedef id (*SPDSPreviewFetchMethodPtr)(SPDataStorage*, SEL, NSUInteger, NSUInteger, NSUInteger); + static SPDSPreviewFetchMethodPtr SPDSPreviewFetch; + if (!SPDSPreviewFetch) SPDSPreviewFetch = (SPDSPreviewFetchMethodPtr)[self methodForSelector:@selector(cellPreviewAtRow:column:previewLength:)]; + return SPDSPreviewFetch(self, @selector(cellPreviewAtRow:column:previewLength:), rowIndex, colIndex, previewLength); +} + diff --git a/Source/SPDataStorage.m b/Source/SPDataStorage.m index 1b3d1cba..c2119032 100644 --- a/Source/SPDataStorage.m +++ b/Source/SPDataStorage.m @@ -31,45 +31,89 @@ // More info at <http://code.google.com/p/sequel-pro/> #import "SPDataStorage.h" +#import "SPObjectAdditions.h" +#import <SPMySQL/SPMySQLStreamingResultStore.h> -@interface SPDataStorage (PrivateAPI) +@interface SPDataStorage (Private_API) -- (void) _ensureCapacityForAdditionalRowCount:(NSUInteger)numExtraRows; -- (void) _increaseCapacity; +- (void) _checkNewRow:(NSMutableArray *)aRow; @end @implementation SPDataStorage -static inline void SPDataStorageEnsureCapacityForAdditionalRowCount(SPDataStorage* self, NSUInteger numExtraRows) +static inline NSMutableArray* SPDataStorageGetEditedRow(NSPointerArray* rowStore, NSUInteger rowIndex) { - typedef void (*SPDSEnsureCapacityMethodPtr)(SPDataStorage*, SEL, NSUInteger); - static SPDSEnsureCapacityMethodPtr SPDSEnsureCapacity; - if (!SPDSEnsureCapacity) SPDSEnsureCapacity = (SPDSEnsureCapacityMethodPtr)[self methodForSelector:@selector(_ensureCapacityForAdditionalRowCount:)]; - SPDSEnsureCapacity(self, @selector(_ensureCapacityForAdditionalRowCount:), numExtraRows); + typedef NSMutableArray* (*SPDSGetEditedRowMethodPtr)(NSPointerArray*, SEL, NSUInteger); + static SPDSGetEditedRowMethodPtr SPDSGetEditedRow; + if (!SPDSGetEditedRow) SPDSGetEditedRow = (SPDSGetEditedRowMethodPtr)[rowStore methodForSelector:@selector(pointerAtIndex:)]; + return SPDSGetEditedRow(rowStore, @selector(pointerAtIndex:), rowIndex); } +#pragma mark - Setting result store + +/** + * Set the underlying MySQL data storage. + * This will clear all edited rows and unloaded column tracking. + */ +- (void) setDataStorage:(SPMySQLStreamingResultStore *)newDataStorage updatingExisting:(BOOL)updateExistingStore +{ + NSUInteger i; + [editedRows release], editedRows = nil; + if (unloadedColumns) free(unloadedColumns), unloadedColumns = NULL; + + if (dataStorage) { + + // If the table is reloading data, link to the current data store for smoother loads + if (updateExistingStore) { + [newDataStorage replaceExistingResultStore:dataStorage]; + } + + [dataStorage release], dataStorage = nil; + } + + dataStorage = [newDataStorage retain]; + [dataStorage setDelegate:self]; + + numberOfColumns = [dataStorage numberOfFields]; + editedRows = [NSPointerArray new]; + if ([dataStorage dataDownloaded]) { + [self resultStoreDidFinishLoadingData:dataStorage]; + } + + unloadedColumns = malloc(numberOfColumns * sizeof(BOOL)); + for (i = 0; i < numberOfColumns; i++) { + unloadedColumns[i] = NO; + } +} + + #pragma mark - #pragma mark Retrieving rows and cells /** * Return a mutable array containing the data for a specified row. */ -- (NSMutableArray *) rowContentsAtIndex:(NSUInteger)index +- (NSMutableArray *) rowContentsAtIndex:(NSUInteger)anIndex { - // Throw an exception if the index is out of bounds - if (index >= numRows) [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", (unsigned long long)index, (unsigned long long)numRows]; + // If an edited row exists for the supplied index, return it + NSMutableArray *editedRow = SPDataStorageGetEditedRow(editedRows, anIndex); + if (editedRow != NULL) { + return editedRow; + } - // Construct the NSMutableArray - NSMutableArray *rowArray = [NSMutableArray arrayWithCapacity:numColumns]; - id *row = dataStorage[index]; - NSUInteger i; - for (i = 0; i < numColumns; i++) { - CFArrayAppendValue((CFMutableArrayRef)rowArray, row[i]); + // Otherwise, prepare to return the underlying storage row + NSMutableArray *dataArray = SPMySQLResultStoreGetRow(dataStorage, anIndex); + + // Modify unloaded cells as appropriate + for (NSUInteger i = 0; i < numberOfColumns; i++) { + if (unloadedColumns[i]) { + CFArraySetValueAtIndex((CFMutableArrayRef)dataArray, i, [SPNotLoaded notLoaded]); + } } - return rowArray; + return dataArray; } /** @@ -78,11 +122,78 @@ static inline void SPDataStorageEnsureCapacityForAdditionalRowCount(SPDataStorag - (id) cellDataAtRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex { - // Throw an exception if the row or column index is out of bounds - if (rowIndex >= numRows || columnIndex >= numColumns) [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)numRows, (unsigned long long)numColumns]; + // If an edited row exists at the supplied index, return it + NSMutableArray *editedRow = SPDataStorageGetEditedRow(editedRows, rowIndex); + if (editedRow != NULL) { + return CFArrayGetValueAtIndex((CFArrayRef)editedRow, columnIndex); + } + + // Throw an exception if the column index is out of bounds + if (columnIndex >= numberOfColumns) { + [NSException raise:NSRangeException format:@"Requested storage column (col %llu) beyond bounds (%llu)", (unsigned long long)columnIndex, (unsigned long long)numberOfColumns]; + } + + // If the specified column is not loaded, return a SPNotLoaded reference + if (unloadedColumns[columnIndex]) { + return [SPNotLoaded notLoaded]; + } // Return the content - return dataStorage[rowIndex][columnIndex]; + return SPMySQLResultStoreObjectAtRowAndColumn(dataStorage, rowIndex, columnIndex); +} + +/** + * Return a preview of the data at a specified row and column index, limited + * to approximately the supplied length. + */ +- (id) cellPreviewAtRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex previewLength:(NSUInteger)previewLength +{ + + // If an edited row exists at the supplied index, return it + NSMutableArray *editedRow = SPDataStorageGetEditedRow(editedRows, rowIndex); + if (editedRow != NULL) { + id anObject = CFArrayGetValueAtIndex((CFArrayRef)editedRow, columnIndex); + if ([anObject isKindOfClass:[NSString class]] && [(NSString *)anObject length] > 150) { + return ([NSString stringWithFormat:@"%@...", [anObject substringToIndex:147]]); + } + return anObject; + } + + // Throw an exception if the column index is out of bounds + if (columnIndex >= numberOfColumns) { + [NSException raise:NSRangeException format:@"Requested storage column (col %llu) beyond bounds (%llu)", (unsigned long long)columnIndex, (unsigned long long)numberOfColumns]; + } + + // If the specified column is not loaded, return a SPNotLoaded reference + if (unloadedColumns[columnIndex]) { + return [SPNotLoaded notLoaded]; + } + + // Return the content + return SPMySQLResultStorePreviewAtRowAndColumn(dataStorage, rowIndex, columnIndex, previewLength); +} + +/** + * Returns whether the data at a specified row and column index is NULL or unloaded + */ +- (BOOL) cellIsNullOrUnloadedAtRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex +{ + // If an edited row exists at the supplied index, check it for a NULL. + NSMutableArray *editedRow = SPDataStorageGetEditedRow(editedRows, rowIndex); + if (editedRow != NULL) { + return [(id)CFArrayGetValueAtIndex((CFArrayRef)editedRow, columnIndex) isNSNull]; + } + + // Throw an exception if the column index is out of bounds + if (columnIndex >= numberOfColumns) { + [NSException raise:NSRangeException format:@"Requested storage column (col %llu) beyond bounds (%llu)", (unsigned long long)columnIndex, (unsigned long long)numberOfColumns]; + } + + if (unloadedColumns[columnIndex]) { + return YES; + } + + return [dataStorage cellIsNullAtRow:rowIndex column:columnIndex]; } #pragma mark - @@ -90,38 +201,37 @@ static inline void SPDataStorageEnsureCapacityForAdditionalRowCount(SPDataStorag /** * Implementation of the NSFastEnumeration protocol. - * Note that this currently doesn't implement mutation guards. + * 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 { // If the start index is out of bounds, return 0 to indicate end of results - if (state->state >= numRows) return 0; - - // Determine how many objects to return - 128, len, or all items remaining - NSUInteger itemsToReturn = 128; - if (len < 128) itemsToReturn = len; - if (numRows - state->state < itemsToReturn) { - itemsToReturn = numRows - state->state; - } - - // Construct the arrays to return - NSUInteger i, j; - NSMutableArray *rowArray; - id *row; - for (i = 0; i < itemsToReturn; i++) { - row = dataStorage[state->state + i]; - rowArray = [NSMutableArray arrayWithCapacity:numColumns]; - for (j = 0; j < numColumns; j++) { - CFArrayAppendValue((CFMutableArrayRef)rowArray, row[j]); + if (state->state >= SPMySQLResultStoreGetRowCount(dataStorage)) return 0; + + // If an edited row exists for the supplied index, use that; otherwise use the underlying + // storage row + NSMutableArray *targetRow = SPDataStorageGetEditedRow(editedRows, state->state); + if (targetRow == NULL) { + targetRow = SPMySQLResultStoreGetRow(dataStorage, state->state); + + // Modify unloaded cells as appropriate + for (NSUInteger i = 0; i < numberOfColumns; i++) { + if (unloadedColumns[i]) { + CFArraySetValueAtIndex((CFMutableArrayRef)targetRow, i, [SPNotLoaded notLoaded]); + } } - stackbuf[i] = rowArray; } - state->state += itemsToReturn; + // Add the item to the buffer and return the appropriate state + stackbuf[0] = targetRow; + + state->state += 1; state->itemsPtr = stackbuf; - state->mutationsPtr = (unsigned long *)&numRows; - return itemsToReturn; + state->mutationsPtr = (unsigned long *)self; + + return 1; } #pragma mark - @@ -132,29 +242,17 @@ static inline void SPDataStorageEnsureCapacityForAdditionalRowCount(SPDataStorag * of objects. Note that the supplied objects are retained as a reference * rather than copied. */ -- (void) addRowWithContents:(NSArray *)row +- (void) addRowWithContents:(NSMutableArray *)aRow { - - // Ensure that sufficient capacity is available - SPDataStorageEnsureCapacityForAdditionalRowCount(self, 1); - - // Add an empty row array to the data store - id *newRow = (id *)malloc(columnPointerByteSize); - dataStorage[numRows] = newRow; - numRows++; - - // Copy over references to the array contents, and retain the objects - NSUInteger cellsCopied = 0; - for (id cellData in row) { - if (cellData) newRow[cellsCopied] = (id)CFRetain(cellData); - else newRow[cellsCopied] = nil; - if (++cellsCopied == numColumns) break; - } - // If an array shorter than the row width was added, pad with nils - if (cellsCopied < numColumns) { - for ( ; cellsCopied <= numColumns; cellsCopied++) newRow[cellsCopied] = nil; - } + // Verify the row is of the correct length + [self _checkNewRow:aRow]; + + // Add the new row to the editable store + [editedRows addPointer:aRow]; + + // Update the underlying store as well to keep counts correct + [dataStorage addDummyRow]; } /** @@ -162,109 +260,70 @@ static inline void SPDataStorageEnsureCapacityForAdditionalRowCount(SPDataStorag * all later rows the next index. Note that the supplied objects within the * array are retained as a reference rather than copied. */ -- (void) insertRowContents:(NSArray *)row atIndex:(NSUInteger)index +- (void) insertRowContents:(NSMutableArray *)aRow atIndex:(NSUInteger)anIndex { + unsigned long long numberOfRows = SPMySQLResultStoreGetRowCount(dataStorage); + + // Verify the row is of the correct length + [self _checkNewRow:aRow]; // Throw an exception if the index is out of bounds - if (index > numRows) [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", (unsigned long long)index, (unsigned long long)numRows]; + if (anIndex > numberOfRows) { + [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", (unsigned long long)anIndex, numberOfRows]; + } // If "inserting" at the end of the array just add a row - if (index == numRows) return SPDataStorageAddRow(self, row); - - // Ensure that sufficient capacity is available to hold all the rows - SPDataStorageEnsureCapacityForAdditionalRowCount(self, 1); - - // Renumber the specified index, and all subsequent indices, to create a gap - for (NSUInteger j = numRows - 1; j >= index; j--) { - dataStorage[j + 1] = dataStorage[j]; + if (anIndex == numberOfRows) { + return [self addRowWithContents:aRow]; } - // Add a new instantiated row array to the data store at the specified point - id *newRow = (id *)malloc(columnPointerByteSize); - dataStorage[index] = newRow; - numRows++; - - // Copy over references to the array contents, and retain the objects - NSUInteger cellsCopied = 0; - for (id cellData in row) { - if (cellData) newRow[cellsCopied] = (id)CFRetain(cellData); - else newRow[cellsCopied] = nil; - if (++cellsCopied == numColumns) break; - } + // Add the new row to the editable store + [editedRows insertPointer:aRow atIndex:anIndex]; - // If an array shorter than the row width was inserted, pad with nils - if (cellsCopied < numColumns) { - for ( ; cellsCopied <= numColumns; cellsCopied++) newRow[cellsCopied] = nil; - } + // Update the underlying store to keep counts and indices correct + [dataStorage insertDummyRowAtIndex:anIndex]; } /** * Replace a row with contents of the supplied NSArray. */ -- (void) replaceRowAtIndex:(NSUInteger)index withRowContents:(NSArray *)row +- (void) replaceRowAtIndex:(NSUInteger)anIndex withRowContents:(NSMutableArray *)aRow { - NSUInteger cellsProcessed = 0; - - // Throw an exception if the index is out of bounds - if (index >= numRows) [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", (unsigned long long)index, (unsigned long long)numRows]; - - id *storageRow = dataStorage[index]; - - // Iterate through the row replacing the objects - for (id cellData in row) { - if (storageRow[cellsProcessed]) CFRelease(storageRow[cellsProcessed]); - if (cellData) storageRow[cellsProcessed] = (id)CFRetain(cellData); - else storageRow[cellsProcessed] = nil; - if (++cellsProcessed == numColumns) break; - } - - // Ensure all cells are correctly updated if an array shorter than the row width was supplied - if (cellsProcessed < numColumns) { - for ( ; cellsProcessed <= numColumns; cellsProcessed++) { - if (storageRow[cellsProcessed]) CFRelease(storageRow[cellsProcessed]); - storageRow[cellsProcessed] = nil; - } - } + [self _checkNewRow:aRow]; + [editedRows replacePointerAtIndex:anIndex withPointer:aRow]; } /** * Replace the contents of a single cell with a supplied object. */ -- (void) replaceObjectInRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex withObject:(id)object +- (void) replaceObjectInRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex withObject:(id)anObject { - // Throw an exception of either index is out of bounds - if (rowIndex >= numRows || columnIndex >= numColumns) [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)numRows, (unsigned long long)numColumns]; + // Make sure that the row in question is editable + NSMutableArray *editableRow = SPDataStorageGetEditedRow(editedRows, rowIndex); + if (editableRow == NULL) { + editableRow = [self rowContentsAtIndex:rowIndex]; + [editedRows replacePointerAtIndex:rowIndex withPointer:editableRow]; + } - // Release the old object and retain the new one - if (dataStorage[rowIndex][columnIndex]) CFRelease(dataStorage[rowIndex][columnIndex]); - if (object) dataStorage[rowIndex][columnIndex] = (id)CFRetain(object); - else dataStorage[rowIndex][columnIndex] = nil; + // Modify the cell + [editableRow replaceObjectAtIndex:columnIndex withObject:anObject]; } /** * Remove a row, renumbering all elements beyond index. */ -- (void) removeRowAtIndex:(NSUInteger)index +- (void) removeRowAtIndex:(NSUInteger)anIndex { // Throw an exception if the index is out of bounds - if (index >= numRows) [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", (unsigned long long)index, (unsigned long long)numRows]; - - // Free the row - NSUInteger j = numColumns; - id *row = dataStorage[index]; - while (j > 0) { - if (row[--j]) CFRelease(row[j]); + if (anIndex >= SPMySQLResultStoreGetRowCount(dataStorage)) { + [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", (unsigned long long)anIndex, SPMySQLResultStoreGetRowCount(dataStorage)]; } - free(row); - numRows--; - // Renumber all subsequent indices to fill the gap - for (j = index; j < numRows; j++) { - dataStorage[j] = dataStorage[j + 1]; - } - dataStorage[numRows] = NULL; + // Remove the row from the edited list and underlying storage + [editedRows removePointerAtIndex:anIndex]; + [dataStorage removeRowAtIndex:anIndex]; } /** @@ -275,27 +334,16 @@ static inline void SPDataStorageEnsureCapacityForAdditionalRowCount(SPDataStorag { // Throw an exception if the range is out of bounds - if (rangeToRemove.location + rangeToRemove.length > numRows) [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", (unsigned long long)(rangeToRemove.location + rangeToRemove.length), (unsigned long long)numRows]; - - // Free rows in the range - NSUInteger i, j = numColumns; - id *row; - for (i = rangeToRemove.location; i < rangeToRemove.location + rangeToRemove.length; i++) { - row = dataStorage[i]; - while (j > 0) { - if (row[--j]) CFRelease(row[j]); - } - free(row); + if (rangeToRemove.location + rangeToRemove.length > SPMySQLResultStoreGetRowCount(dataStorage)) { + [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", (unsigned long long)(rangeToRemove.location + rangeToRemove.length), SPMySQLResultStoreGetRowCount(dataStorage)]; } - numRows -= rangeToRemove.length; - // Renumber all subsequent indices to fill the gap - for (i = rangeToRemove.location + rangeToRemove.length - 1; i < numRows; i++) { - dataStorage[i] = dataStorage[i + rangeToRemove.length]; - } - for (i = numRows; i < numRows + rangeToRemove.length; i++) { - dataStorage[i] = NULL; + // Remove the rows from the edited list and underlying storage + NSUInteger i = rangeToRemove.location + rangeToRemove.length; + while (--i >= rangeToRemove.location) { + [editedRows removePointerAtIndex:i]; } + [dataStorage removeRowsInRange:rangeToRemove]; } /** @@ -303,81 +351,58 @@ static inline void SPDataStorageEnsureCapacityForAdditionalRowCount(SPDataStorag */ - (void) removeAllRows { - NSUInteger j; - id *row; - - // Free all the data - while (numRows > 0) { - row = dataStorage[--numRows]; - j = numColumns; - while (j > 0) { - if (row[--j]) CFRelease(row[j]); - } - free(row); - } + [editedRows setCount:0]; + [dataStorage removeAllRows]; +} - numRows = 0; +#pragma mark - Unloaded columns + +/** + * Mark a column as unloaded; SPNotLoaded placeholders will be returned for cells requested + * from this store which haven't had their value updated from elsewhere. + */ +- (void) setColumnAsUnloaded:(NSUInteger)columnIndex +{ + if (columnIndex >= numberOfColumns) { + [NSException raise:NSRangeException format:@"Invalid column set as unloaded; requested column index (%llu) beyond bounds (%llu)", (unsigned long long)columnIndex, (unsigned long long)numberOfColumns]; + } + unloadedColumns[columnIndex] = true; } -#pragma mark - -#pragma mark Basic information +#pragma mark - Basic information /** * Returns the number of rows currently held in data storage. */ - (NSUInteger) count { - return numRows; + return (NSUInteger)[dataStorage numberOfRows]; } /** - * Set the number of columns represented by the data storage. + * Return the number of columns represented by the data storage. */ -- (void) setColumnCount:(NSUInteger)columnCount +- (NSUInteger) columnCount { - columnPointerByteSize = columnCount * sizeof(id); - - // If there are rows present in the storage, and the number of - // columns has changed, amend the existing rows to match. - if (columnCount != numColumns && numRows) { - NSUInteger i = numRows, j; - id *row; - - // If the new column count is higher than the old count, iterate through the existing rows - // and pad with nils - if (columnCount > numColumns) { - while (i-- > 0) { - dataStorage[i] = (id *)realloc(dataStorage[i], columnPointerByteSize); - j = numColumns; - while (j < columnCount) { - dataStorage[i][j++] = nil; - } - } - - // If the new column count is lower than the old count, iterate through the existing rows - // freeing any extra objects - } else { - while (i > 0) { - row = dataStorage[--i]; - j = numColumns; - while (j > columnCount) { - if (row[--j]) CFRelease(row[j]); - } - dataStorage[i] = (id *)realloc(row, columnPointerByteSize); - } - } - } + return numberOfColumns; +} - // Update the column count - numColumns = columnCount; +/** + * Return whether all the data has been downloaded into the underlying result store. + */ +- (BOOL) dataDownloaded +{ + return [dataStorage dataDownloaded]; } +#pragma mark - Delegate callback methods + /** - * Return the number of columns represented by the data storage. + * When the underlying result store finishes downloading, update the row store to match */ -- (NSUInteger) columnCount +- (void)resultStoreDidFinishLoadingData:(SPMySQLStreamingResultStore *)resultStore { - return numColumns; + [editedRows setCount:(NSUInteger)[resultStore numberOfRows]]; } /** @@ -387,20 +412,20 @@ static inline void SPDataStorageEnsureCapacityForAdditionalRowCount(SPDataStorag - (id) init { if ((self = [super init])) { - numColumns = 0; - columnPointerByteSize = 0; - numRows = 0; + dataStorage = nil; + editedRows = nil; + unloadedColumns = NULL; - // Initialise the array, initially with space for 100 rows - numRowsCapacity = 100; - dataStorage = (id **)malloc(numRowsCapacity * sizeof(id *)); + numberOfColumns = 0; } return self; } - (void) dealloc { - [self removeAllRows]; - free(dataStorage); + [dataStorage release], dataStorage = nil; + [editedRows release], editedRows = nil; + if (unloadedColumns) free(unloadedColumns), unloadedColumns = NULL; + [super dealloc]; } @@ -408,23 +433,12 @@ static inline void SPDataStorageEnsureCapacityForAdditionalRowCount(SPDataStorag @implementation SPDataStorage (PrivateAPI) -/** - * Private method to ensure the array always has sufficient capacity - * to store any additional rows required. - */ -- (void) _ensureCapacityForAdditionalRowCount:(NSUInteger)numExtraRows +- (void) _checkNewRow:(NSMutableArray *)aRow { - while (numRows + numExtraRows > numRowsCapacity) [self _increaseCapacity]; + if ([aRow count] != numberOfColumns) { + [NSException raise:NSInternalInconsistencyException format:@"New row length (%llu) does not match store column count (%llu)", (unsigned long long)[aRow count], (unsigned long long)numberOfColumns]; + } } -/** - * Private method to increase the storage available for the array; - * currently doubles the capacity as boundaries are reached. - */ -- (void) _increaseCapacity -{ - numRowsCapacity *= 2; - dataStorage = (id **)realloc(dataStorage, numRowsCapacity * sizeof(id *)); -} @end diff --git a/Source/SPPreferencesUpgrade.m b/Source/SPPreferencesUpgrade.m index af639191..55e68157 100644 --- a/Source/SPPreferencesUpgrade.m +++ b/Source/SPPreferencesUpgrade.m @@ -65,11 +65,15 @@ void SPApplyRevisionChanges(void) if ([prefs objectForKey:SPLastUsedVersion]) recordedVersionNumber = [[prefs objectForKey:SPLastUsedVersion] integerValue]; // Skip processing if the current version matches or is less than recorded version - if (currentVersionNumber <= recordedVersionNumber) return; + if (currentVersionNumber <= recordedVersionNumber) { + [importantUpdateNotes release]; + return; + } // If no recorded version, update to current revision and skip processing if (!recordedVersionNumber) { [prefs setObject:[NSNumber numberWithInteger:currentVersionNumber] forKey:SPLastUsedVersion]; + [importantUpdateNotes release]; return; } diff --git a/Source/SPTableContent.h b/Source/SPTableContent.h index cb81a706..ab62b87f 100644 --- a/Source/SPTableContent.h +++ b/Source/SPTableContent.h @@ -40,7 +40,7 @@ @class SPTextView; @class SPFieldEditorController; @class SPMySQLConnection; -@class SPMySQLFastStreamingResult; +@class SPMySQLStreamingResultStore; @class SPTableData; @class SPDatabaseDocument; @class SPTablesList; @@ -118,6 +118,7 @@ BOOL _mainNibLoaded; BOOL isWorking; pthread_mutex_t tableValuesLock; + NSCondition *tableLoadingCondition; #ifndef SP_CODA NSMutableArray *nibObjectsToRelease; #endif @@ -261,7 +262,7 @@ - (void)clickLinkArrow:(SPTextAndLinkCell *)theArrowCell; - (void)clickLinkArrowTask:(SPTextAndLinkCell *)theArrowCell; - (IBAction)setCompareTypes:(id)sender; -- (void)processResultIntoDataStorage:(SPMySQLFastStreamingResult *)theResult approximateRowCount:(NSUInteger)targetRowCount; +- (void)updateResultStore:(SPMySQLStreamingResultStore *)theResultStore approximateRowCount:(NSUInteger)targetRowCount; - (BOOL)saveRowToTable; - (void) addRowErrorSheetDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo; - (NSString *)argumentForRow:(NSInteger)row; diff --git a/Source/SPTableContent.m b/Source/SPTableContent.m index f093f875..cc0102f2 100644 --- a/Source/SPTableContent.m +++ b/Source/SPTableContent.m @@ -72,6 +72,12 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOperator"; #endif +@interface SPTableContent (SPTableContentDataSource_Private_API) + +- (id)_contentValueForTableColumn:(NSUInteger)columnIndex row:(NSUInteger)rowIndex asPreview:(BOOL)asPreview; + +@end + @interface SPTableContent () - (BOOL)cancelRowEditing; @@ -165,6 +171,7 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper usedQuery = [[NSString alloc] initWithString:@""]; tableLoadTimer = nil; + tableLoadingCondition = [NSCondition new]; blackColor = [NSColor blackColor]; lightGrayColor = [NSColor lightGrayColor]; @@ -609,10 +616,9 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper [dataCell setEditable:YES]; - // Set the line break mode and an NSFormatter subclass which truncates long strings for display + // Set the line break mode and an NSFormatter subclass which displays line breaks nicely [dataCell setLineBreakMode:NSLineBreakByTruncatingTail]; [dataCell setFormatter:[[SPDataCellFormatter new] autorelease]]; - [[dataCell formatter] setDisplayLimit:150]; // Set field length limit if field is a varchar to match varchar length if ([[columnDefinition objectForKey:@"typegrouping"] isEqualToString:@"string"] @@ -765,7 +771,7 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper NSMutableString *queryString; NSString *queryStringBeforeLimit = nil; NSString *filterString; - SPMySQLFastStreamingResult *streamingResult; + SPMySQLStreamingResultStore *resultStore; NSInteger rowsToLoad = [[tableDataInstance statusValueForKey:@"Rows"] integerValue]; #ifndef SP_CODA @@ -828,23 +834,23 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper // Perform and process the query [tableContentView performSelectorOnMainThread:@selector(noteNumberOfRowsChanged) withObject:nil waitUntilDone:YES]; [self setUsedQuery:queryString]; - streamingResult = [[mySQLConnection streamingQueryString:queryString] retain]; + resultStore = [[mySQLConnection resultStoreFromQueryString:queryString] retain]; // Ensure the number of columns are unchanged; if the column count has changed, abort the load // and queue a full table reload. BOOL fullTableReloadRequired = NO; - if (streamingResult && [dataColumns count] != [streamingResult numberOfFields]) { + if (resultStore && [dataColumns count] != [resultStore numberOfFields]) { [tableDocumentInstance disableTaskCancellation]; [mySQLConnection cancelCurrentQuery]; - [streamingResult cancelResultLoad]; + [resultStore cancelResultLoad]; fullTableReloadRequired = YES; } // Process the result into the data store - if (!fullTableReloadRequired && streamingResult) { - [self processResultIntoDataStorage:streamingResult approximateRowCount:rowsToLoad]; + if (!fullTableReloadRequired && resultStore) { + [self updateResultStore:resultStore approximateRowCount:rowsToLoad]; } - if (streamingResult) [streamingResult release]; + if (resultStore) [resultStore release]; // If the result is empty, and a late page is selected, reset the page if (!fullTableReloadRequired && [prefs boolForKey:SPLimitResults] && queryStringBeforeLimit && !tableRowsCount && ![mySQLConnection lastQueryWasCancelled]) { @@ -852,10 +858,10 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper previousTableRowsCount = tableRowsCount; queryString = [NSMutableString stringWithFormat:@"%@ LIMIT 0,%ld", queryStringBeforeLimit, (long)[prefs integerForKey:SPLimitResultsValue]]; [self setUsedQuery:queryString]; - streamingResult = [[mySQLConnection streamingQueryString:queryString] retain]; - if (streamingResult) { - [self processResultIntoDataStorage:streamingResult approximateRowCount:[prefs integerForKey:SPLimitResultsValue]]; - [streamingResult release]; + resultStore = [[mySQLConnection resultStoreFromQueryString:queryString] retain]; + if (resultStore) { + [self updateResultStore:resultStore approximateRowCount:[prefs integerForKey:SPLimitResultsValue]]; + [resultStore release]; } } @@ -950,7 +956,7 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper // Retrieve and cache the column definitions for editing views if (cqColumnDefinition) [cqColumnDefinition release]; - cqColumnDefinition = [[streamingResult fieldDefinitions] retain]; + cqColumnDefinition = [[resultStore fieldDefinitions] retain]; // Notify listenters that the query has finished @@ -985,100 +991,57 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper } /** - * Processes a supplied streaming result set, loading it into the data array. + * Processes a supplied streaming result store, monitoring the load and updating the data + * displayed during download. */ -- (void)processResultIntoDataStorage:(SPMySQLFastStreamingResult *)theResult approximateRowCount:(NSUInteger)targetRowCount +- (void)updateResultStore:(SPMySQLStreamingResultStore *)theResultStore approximateRowCount:(NSUInteger)targetRowCount; { NSUInteger i; NSUInteger dataColumnsCount = [dataColumns count]; - BOOL *columnBlobStatuses = malloc(dataColumnsCount * sizeof(BOOL)); tableLoadTargetRowCount = targetRowCount; - // Set the column count on the data store before setting up anything else - - // ensures that SPDataStorage is set up for timer-driven data loads - [tableValues setColumnCount:dataColumnsCount]; + // Update the data storage, updating the current store if appropriate + pthread_mutex_lock(&tableValuesLock); + [tableValues setDataStorage:theResultStore updatingExisting:!![tableValues count]]; + pthread_mutex_unlock(&tableValuesLock); - // Set up the table updates timer - [[self onMainThread] initTableLoadTimer]; + // Start the data downloading + [theResultStore startDownload]; - NSAutoreleasePool *dataLoadingPool; #ifndef SP_CODA NSProgressIndicator *dataLoadingIndicator = [tableDocumentInstance valueForKey:@"queryProgressBar"]; #else NSProgressIndicator *dataLoadingIndicator = [tableDocumentInstance queryProgressBar]; #endif - BOOL prefsLoadBlobsAsNeeded = -#ifndef SP_CODA - [prefs boolForKey:SPLoadBlobsAsNeeded] -#else - NO -#endif - ; - - // Build up an array of which columns are blobs for faster iteration - for ( i = 0; i < dataColumnsCount ; i++ ) { - columnBlobStatuses[i] = [tableDataInstance columnIsBlobOrText:[NSArrayObjectAtIndex(dataColumns, i) objectForKey:@"name"]]; - } - - // Set up an autorelease pool for row processing - dataLoadingPool = [[NSAutoreleasePool alloc] init]; - // Loop through the result rows as they become available - tableRowsCount = 0; - for (NSArray *eachRow in theResult) { - pthread_mutex_lock(&tableValuesLock); - - if (tableRowsCount < previousTableRowsCount) { - SPDataStorageReplaceRow(tableValues, tableRowsCount, eachRow); - } else { - SPDataStorageAddRow(tableValues, eachRow); - } - - // Alter the values for hidden blob and text fields if appropriate - if ( prefsLoadBlobsAsNeeded ) { - for ( i = 0 ; i < dataColumnsCount ; i++ ) { - if (columnBlobStatuses[i]) { - SPDataStorageReplaceObjectAtRowAndColumn(tableValues, tableRowsCount, i, [SPNotLoaded notLoaded]); - } +#ifndef SP_CODA + // Set the column load states on the table values store + if ([prefs boolForKey:SPLoadBlobsAsNeeded]) { + for ( i = 0; i < dataColumnsCount ; i++ ) { + if ([tableDataInstance columnIsBlobOrText:[NSArrayObjectAtIndex(dataColumns, i) objectForKey:@"name"]]) { + [tableValues setColumnAsUnloaded:i]; } } - tableRowsCount++; + } +#endif - pthread_mutex_unlock(&tableValuesLock); + // Set up the table updates timer and wait for it to notify this thread about completion + [[self onMainThread] initTableLoadTimer]; - // Drain and reset the autorelease pool every ~1024 rows - if (!(tableRowsCount % 1024)) { - [dataLoadingPool drain]; - dataLoadingPool = [[NSAutoreleasePool alloc] init]; - } + [tableLoadingCondition lock]; + while (![tableValues dataDownloaded]) { + [tableLoadingCondition waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]]; } - - // Clean up the interface update timer - [[self onMainThread] clearTableLoadTimer]; + [tableLoadingCondition unlock]; // If the final column autoresize wasn't performed, perform it if (tableLoadLastRowCount < 200) [[self onMainThread] autosizeColumns]; - // If the reloaded table is shorter than the previous table, remove the extra values from the storage - if (tableRowsCount < [tableValues count]) { - pthread_mutex_lock(&tableValuesLock); - [tableValues removeRowsInRange:NSMakeRange(tableRowsCount, [tableValues count] - tableRowsCount)]; - pthread_mutex_unlock(&tableValuesLock); - } - // Ensure the table is aware of changes - if ([NSThread isMainThread]) { - [tableContentView performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:YES]; - } else { - [tableContentView performSelectorOnMainThread:@selector(noteNumberOfRowsChanged) withObject:nil waitUntilDone:YES]; - [tableContentView performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO]; - } + [[tableContentView onMainThread] noteNumberOfRowsChanged]; - // Clean up the autorelease pool and reset the progress indicator - [dataLoadingPool drain]; + // Reset the progress indicator [dataLoadingIndicator setIndeterminate:YES]; - - free(columnBlobStatuses); } /** @@ -1397,6 +1360,7 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper */ - (void) tableLoadUpdate:(NSTimer *)theTimer { + tableRowsCount = [tableValues count]; // Update the task interface as necessary if (!isFiltered && tableLoadTargetRowCount != NSUIntegerMax) { @@ -1414,6 +1378,13 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper return; } + if ([tableValues dataDownloaded]) { + [tableLoadingCondition lock]; + [tableLoadingCondition signal]; + [self clearTableLoadTimer]; + [tableLoadingCondition unlock]; + } + // Check whether a table update is required, based on whether new rows are // available to display. if (tableRowsCount == tableLoadLastRowCount) { @@ -1422,7 +1393,6 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper // Update the table display [tableContentView noteNumberOfRowsChanged]; - if (!tableLoadLastRowCount) [tableContentView setNeedsDisplay:YES]; // Update column widths in two cases: on very first rows displayed, and once // more than 200 rows are present. @@ -2452,7 +2422,7 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper for (NSTableColumn *tableColumn in tableColumns) { - [tempRow addObject:[self tableView:tableContentView objectValueForTableColumn:tableColumn row:i]]; + [tempRow addObject:[self _contentValueForTableColumn:[[tableColumn identifier] integerValue] row:i asPreview:NO]]; } [currentResult addObject:[NSArray arrayWithArray:tempRow]]; @@ -4274,6 +4244,7 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper if(fieldEditor) [fieldEditor release], fieldEditor = nil; [self clearTableLoadTimer]; + [tableLoadingCondition release]; [tableValues release]; pthread_mutex_destroy(&tableValuesLock); [dataColumns release]; diff --git a/Source/SPTableContentDataSource.m b/Source/SPTableContentDataSource.m index c49169a0..8ffa2c69 100644 --- a/Source/SPTableContentDataSource.m +++ b/Source/SPTableContentDataSource.m @@ -39,6 +39,12 @@ #import <pthread.h> #import <SPMySQL/SPMySQL.h> +@interface SPTableContent (SPTableContentDataSource_Private_API) + +- (id)_contentValueForTableColumn:(NSUInteger)columnIndex row:(NSUInteger)rowIndex asPreview:(BOOL)asPreview; + +@end + @implementation SPTableContent (SPTableContentDataSource) #pragma mark - @@ -91,7 +97,7 @@ pthread_mutex_lock(&tableValuesLock); if (rowIndex < (NSInteger)tableRowsCount && columnIndex < [tableValues columnCount]) { - value = [[SPDataStorageObjectAtRowAndColumn(tableValues, rowIndex, columnIndex) copy] autorelease]; + value = [self _contentValueForTableColumn:columnIndex row:rowIndex asPreview:YES]; } pthread_mutex_unlock(&tableValuesLock); @@ -99,7 +105,11 @@ if (!value) return @"..."; } else { - value = SPDataStorageObjectAtRowAndColumn(tableValues, rowIndex, columnIndex); + if ([tableView editedColumn] == (NSInteger)columnIndex && [tableView editedRow] == rowIndex) { + value = [self _contentValueForTableColumn:columnIndex row:rowIndex asPreview:NO]; + } else { + value = [self _contentValueForTableColumn:columnIndex row:rowIndex asPreview:YES]; + } } if ([value isKindOfClass:[SPMySQLGeometryData class]]) @@ -185,3 +195,15 @@ } @end + +@implementation SPTableContent (SPTableContentDataSource_Private_API) + +- (id)_contentValueForTableColumn:(NSUInteger)columnIndex row:(NSUInteger)rowIndex asPreview:(BOOL)asPreview +{ + if (asPreview) { + return SPDataStoragePreviewAtRowAndColumn(tableValues, rowIndex, columnIndex, 150); + } + return SPDataStorageObjectAtRowAndColumn(tableValues, rowIndex, columnIndex); +} + +@end diff --git a/Source/SPTableContentDelegate.m b/Source/SPTableContentDelegate.m index d2e7f2d5..8f228aa0 100644 --- a/Source/SPTableContentDelegate.m +++ b/Source/SPTableContentDelegate.m @@ -464,9 +464,18 @@ if (![cell respondsToSelector:@selector(setTextColor:)]) return; - id theValue = nil; + BOOL showCellAsGray = NO; NSUInteger columnIndex = [[tableColumn identifier] integerValue]; + // If user wants to edit 'cell' set text color to black and return to avoid + // writing in gray if value was NULL + if ([tableView editedColumn] != -1 + && [tableView editedRow] == rowIndex + && (NSUInteger)[[NSArrayObjectAtIndex([tableView tableColumns], [tableView editedColumn]) identifier] integerValue] == columnIndex) { + [cell setTextColor:blackColor]; + return; + } + // While the table is being loaded, additional validation is required - data // locks must be used to avoid crashes, and indexes higher than the available // rows or columns may be requested. Use gray to indicate loading in these cases. @@ -474,34 +483,18 @@ pthread_mutex_lock(&tableValuesLock); if (rowIndex < (NSInteger)tableRowsCount && columnIndex < [tableValues columnCount]) { - theValue = SPDataStorageObjectAtRowAndColumn(tableValues, rowIndex, columnIndex); + showCellAsGray = [tableValues cellIsNullOrUnloadedAtRow:rowIndex column:columnIndex]; } pthread_mutex_unlock(&tableValuesLock); - - if (!theValue) { - [cell setTextColor:[NSColor lightGrayColor]]; - return; - } } else { - theValue = SPDataStorageObjectAtRowAndColumn(tableValues, rowIndex, columnIndex); + showCellAsGray = [tableValues cellIsNullOrUnloadedAtRow:rowIndex column:columnIndex]; } - // If user wants to edit 'cell' set text color to black and return to avoid - // writing in gray if value was NULL - if ([tableView editedColumn] != -1 - && [tableView editedRow] == rowIndex - && (NSUInteger)[[NSArrayObjectAtIndex([tableView tableColumns], [tableView editedColumn]) identifier] integerValue] == columnIndex) { - [cell setTextColor:blackColor]; - return; - } - // For null cells and not loaded cells, display the contents in gray. - if ([theValue isNSNull] || [theValue isSPNotLoaded]) { + if (showCellAsGray) { [cell setTextColor:lightGrayColor]; - - // Otherwise, set the color to black - required as NSTableView reuses NSCells. } else { [cell setTextColor:blackColor]; |