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