aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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];