aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorrowanbeentje <rowan@beent.je>2013-08-13 23:49:31 +0000
committerrowanbeentje <rowan@beent.je>2013-08-13 23:49:31 +0000
commitef60b2022d50b99e6de78cc301bf71e8b336ae0e (patch)
tree175e38fc968dec070ca8a872f7b87502b62e8c82
parent80c152501303c0ed7bd530f5e05bc7e5a6fba7f5 (diff)
downloadsequelpro-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.
-rw-r--r--Frameworks/SPMySQLFramework/SPMySQLEmptyResult.m2
-rw-r--r--Frameworks/SPMySQLFramework/SPMySQLFramework.xcodeproj/project.pbxproj22
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQL Private APIs.h17
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQL.h6
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Encoding.m2
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.h1
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.m18
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConstants.h3
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLFastStreamingResult.m2
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Convenience Methods.m5
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Data Conversion.h42
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Data Conversion.m413
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLResult.h4
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLResult.m161
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResult.h2
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResult.m4
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResultStore.h110
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResultStore.m888
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLStreamingResultStoreDelegate.h45
-rw-r--r--Source/SPCopyTable.m4
-rw-r--r--Source/SPCustomQuery.h5
-rw-r--r--Source/SPCustomQuery.m189
-rw-r--r--Source/SPDataCellFormatter.h2
-rw-r--r--Source/SPDataCellFormatter.m14
-rw-r--r--Source/SPDataStorage.h68
-rw-r--r--Source/SPDataStorage.m478
-rw-r--r--Source/SPPreferencesUpgrade.m6
-rw-r--r--Source/SPTableContent.h5
-rw-r--r--Source/SPTableContent.m139
-rw-r--r--Source/SPTableContentDataSource.m26
-rw-r--r--Source/SPTableContentDelegate.m33
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];