aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorrowanbeentje <rowan@beent.je>2009-11-15 23:58:21 +0000
committerrowanbeentje <rowan@beent.je>2009-11-15 23:58:21 +0000
commit50a283b6d1f3ce48e3a06eceeed3a466c6259fe7 (patch)
tree98596427c436f4735ec9baee45da66248e83291c
parent69a4fbc7cf11a35e5bf609cf7a16d2844559997c (diff)
downloadsequelpro-50a283b6d1f3ce48e3a06eceeed3a466c6259fe7.tar.gz
sequelpro-50a283b6d1f3ce48e3a06eceeed3a466c6259fe7.tar.bz2
sequelpro-50a283b6d1f3ce48e3a06eceeed3a466c6259fe7.zip
Implement query cancellation support within MCPKit, and add it to the task functionality:
- MCPKit now supports cancelling the active query; for MySQL servers >= 5.0.0 a query kill is attempted from a new connection, and if that fails or for MySQL < 5 a reconnect is triggered. - TableDocument now supports enabling a cancel task button on the task interface, including an optional callback - Implement query cancellation for custom queries. This addresses Issue #86. - Implement query cancellation for table content loads, filters, and sorts.
-rw-r--r--Frameworks/MCPKit/MCPFoundationKit/MCPConnection.h5
-rw-r--r--Frameworks/MCPKit/MCPFoundationKit/MCPConnection.m138
-rw-r--r--Interfaces/English.lproj/ProgressIndicatorLayer.xib42
-rw-r--r--Source/CustomQuery.m84
-rw-r--r--Source/TableContent.h2
-rw-r--r--Source/TableContent.m29
-rw-r--r--Source/TableDocument.h6
-rw-r--r--Source/TableDocument.m70
-rw-r--r--Source/TableSource.m16
9 files changed, 327 insertions, 65 deletions
diff --git a/Frameworks/MCPKit/MCPFoundationKit/MCPConnection.h b/Frameworks/MCPKit/MCPFoundationKit/MCPConnection.h
index 548425e1..0442bf9f 100644
--- a/Frameworks/MCPKit/MCPFoundationKit/MCPConnection.h
+++ b/Frameworks/MCPKit/MCPFoundationKit/MCPConnection.h
@@ -111,6 +111,8 @@ static inline NSData* NSStringDataUsingLossyEncoding(NSString* self, NSInteger e
uint64_t connectionStartTime;
BOOL retryAllowed;
+ BOOL queryCancelled;
+ BOOL queryCancelUsedReconnect;
BOOL delegateQueryLogging;
BOOL delegateResponseToWillQueryString;
@@ -205,6 +207,9 @@ void performThreadedKeepAlive(void *ptr);
- (id)queryString:(NSString *) query usingEncoding:(NSStringEncoding) encoding streamingResult:(NSInteger) streamResult;
- (my_ulonglong)affectedRows;
- (my_ulonglong)insertId;
+- (void)cancelCurrentQuery;
+- (BOOL)queryCancelled;
+- (BOOL)queryCancellationUsedReconnect;
// Locking
- (void)lockConnection;
diff --git a/Frameworks/MCPKit/MCPFoundationKit/MCPConnection.m b/Frameworks/MCPKit/MCPFoundationKit/MCPConnection.m
index e55568d5..7db3ec2d 100644
--- a/Frameworks/MCPKit/MCPFoundationKit/MCPConnection.m
+++ b/Frameworks/MCPKit/MCPFoundationKit/MCPConnection.m
@@ -101,6 +101,8 @@ static BOOL sTruncateLongFieldInLogs = YES;
connectionProxy = nil;
connectionStartTime = -1;
lastQueryExecutedAtTime = CGFLOAT_MAX;
+ queryCancelled = NO;
+ queryCancelUsedReconnect = NO;
// Initialize ivar defaults
connectionTimeout = 10;
@@ -1261,6 +1263,9 @@ void performThreadedKeepAlive(void *ptr)
NSInteger currentMaxAllowedPacket = -1;
BOOL isQueryRetry = NO;
NSString *queryErrorMessage = nil;
+
+ // Reset the query cancelled boolean
+ queryCancelled = NO;
// If no connection is present, return nil.
if (!mConnected) {
@@ -1290,7 +1295,6 @@ void performThreadedKeepAlive(void *ptr)
// minimising the impact of performing lots of additional checks.
if ([self timeConnected] - lastQueryExecutedAtTime > 30
&& ![self checkConnection]) {
- NSLog(@"returning nil!");
return nil;
}
@@ -1366,7 +1370,7 @@ void performThreadedKeepAlive(void *ptr)
// For normal result sets, fetch the results and unlock the connection
if (streamResultType == MCP_NO_STREAMING) {
theResult = [[MCPResult alloc] initWithMySQLPtr:mConnection encoding:mEncoding timeZone:mTimeZone];
- [queryLock unlock];
+ if (!queryCancelled || !queryCancelUsedReconnect) [queryLock unlock];
// For streaming result sets, fetch the result pointer and leave the connection locked
} else if (streamResultType == MCP_FAST_STREAMING) {
@@ -1394,16 +1398,23 @@ void performThreadedKeepAlive(void *ptr)
// On failure, set the error messages and IDs
} else {
- if (streamResultType == MCP_NO_STREAMING) [queryLock unlock];
- else [self unlockConnection];
+ if (!queryCancelled || !queryCancelUsedReconnect) {
+ if (streamResultType == MCP_NO_STREAMING) [queryLock unlock];
+ else [self unlockConnection];
+ }
- queryErrorMessage = [[NSString alloc] initWithString:[self stringWithCString:mysql_error(mConnection)]];
- queryErrorId = mysql_errno(mConnection);
-
- // If the error was a connection error, retry once
- if (!isQueryRetry && retryAllowed && [MCPConnection isErrorNumberConnectionError:queryErrorId]) {
- isQueryRetry = YES;
- continue;
+ if (queryCancelled) {
+ queryErrorMessage = [[NSString alloc] initWithString:NSLocalizedString(@"Query cancelled.", @"Query cancelled error")];
+ queryErrorId = 1152;
+ } else {
+ queryErrorMessage = [[NSString alloc] initWithString:[self stringWithCString:mysql_error(mConnection)]];
+ queryErrorId = mysql_errno(mConnection);
+
+ // If the error was a connection error, retry once
+ if (!isQueryRetry && retryAllowed && [MCPConnection isErrorNumberConnectionError:queryErrorId]) {
+ isQueryRetry = YES;
+ continue;
+ }
}
}
@@ -1464,6 +1475,104 @@ void performThreadedKeepAlive(void *ptr)
return 0;
}
+/**
+ * Cancel the currently running query. This tries to kill the current query, and if that
+ * isn't possible, resets the connection.
+ */
+- (void) cancelCurrentQuery
+{
+
+ // If not connected, return.
+ if (![self isConnected]) return;
+
+ // Check whether a query is actually being performed - if not, also return.
+ if ([queryLock tryLock]) {
+ [queryLock unlock];
+ return;
+ }
+
+ // Set queryCancelled to prevent query retries
+ queryCancelled = YES;
+
+ // For MySQL server versions >=5, try to kill the connection. This requires
+ // setting up a new connection, and running a KILL QUERY via it.
+ if ([self serverMajorVersion] >= 5) {
+
+ MYSQL *killerConnection = mysql_init(NULL);
+ if (killerConnection) {
+ const char *theLogin = [self cStringFromString:connectionLogin];
+ const char *theHost;
+ const char *thePass;
+ const char *theSocket;
+ void *connectionSetupStatus;
+
+ mysql_options(killerConnection, MYSQL_OPT_CONNECT_TIMEOUT, (const void *)&connectionTimeout);
+
+ // Set up the host, socket and password as per the connect method
+ if (!connectionHost || ![connectionHost length]) {
+ theHost = NULL;
+ } else {
+ theHost = [self cStringFromString:connectionHost];
+ }
+ if (connectionSocket == nil || ![connectionSocket length]) {
+ theSocket = kMCPConnectionDefaultSocket;
+ } else {
+ theSocket = [self cStringFromString:connectionSocket];
+ }
+ if (!connectionPassword) {
+ if (delegate && [delegate respondsToSelector:@selector(keychainPasswordForConnection:)]) {
+ thePass = [self cStringFromString:[delegate keychainPasswordForConnection:self]];
+ }
+ } else {
+ thePass = [self cStringFromString:connectionPassword];
+ }
+
+ // Connect
+ connectionSetupStatus = mysql_real_connect(killerConnection, theHost, theLogin, thePass, NULL, connectionPort, theSocket, mConnectionFlags);
+ thePass = NULL;
+ if (connectionSetupStatus) {
+ NSStringEncoding killerConnectionEncoding = [MCPConnection encodingForMySQLEncoding:mysql_character_set_name(killerConnection)];
+ NSString *killerQueryString = [NSString stringWithFormat:@"KILL QUERY %lu", mConnection->thread_id];
+ NSData *encodedKillerQueryData = NSStringDataUsingLossyEncoding(killerQueryString, killerConnectionEncoding, 1);
+ const char *killerQueryCString = [encodedKillerQueryData bytes];
+ unsigned long killerQueryCStringLength = [encodedKillerQueryData length];
+ if (mysql_real_query(killerConnection, killerQueryCString, killerQueryCStringLength) == 0) {
+ mysql_close(killerConnection);
+ queryCancelUsedReconnect = NO;
+ return;
+ }
+ mysql_close(killerConnection);
+ }
+ }
+ }
+
+ // Reset the connection
+ [self unlockConnection];
+ [self reconnect];
+
+ // Set queryCancelled again to handle requery cleanups, and return.
+ queryCancelled = YES;
+ queryCancelUsedReconnect = YES;
+}
+
+/**
+ * Return whether the last query was cancelled
+ */
+- (BOOL)queryCancelled
+{
+ return queryCancelled;
+}
+
+/**
+ * If the last query was cancelled, returns whether that cancellation
+ * required a connection reset. If the last query was not cancelled
+ * the behaviour is undefined.
+ */
+- (BOOL)queryCancellationUsedReconnect
+{
+ return queryCancelUsedReconnect;
+}
+
#pragma mark -
#pragma mark Connection locking
@@ -1483,6 +1592,13 @@ void performThreadedKeepAlive(void *ptr)
*/
- (void)unlockConnection
{
+
+ // Make sure the unlock is performed safely - eg for reconnected queries
+ if ([queryLock tryLock]) {
+ [queryLock unlock];
+ return;
+ }
+
if ([NSThread isMainThread]) [queryLock unlock];
else [queryLock performSelectorOnMainThread:@selector(unlock) withObject:nil waitUntilDone:YES];
}
diff --git a/Interfaces/English.lproj/ProgressIndicatorLayer.xib b/Interfaces/English.lproj/ProgressIndicatorLayer.xib
index 33adda95..de3df433 100644
--- a/Interfaces/English.lproj/ProgressIndicatorLayer.xib
+++ b/Interfaces/English.lproj/ProgressIndicatorLayer.xib
@@ -3,7 +3,7 @@
<data>
<int key="IBDocument.SystemTarget">1050</int>
<string key="IBDocument.SystemVersion">10B504</string>
- <string key="IBDocument.InterfaceBuilderVersion">732</string>
+ <string key="IBDocument.InterfaceBuilderVersion">740</string>
<string key="IBDocument.AppKitVersion">1038.2</string>
<string key="IBDocument.HIToolboxVersion">437.00</string>
<object class="NSMutableDictionary" key="IBDocument.PluginVersions">
@@ -15,7 +15,7 @@
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
- <string>732</string>
+ <string>740</string>
<string>1.2.1</string>
</object>
</object>
@@ -117,8 +117,8 @@
</object>
<object class="BWTransparentButton" id="540103856">
<reference key="NSNextResponder" ref="950529842"/>
- <int key="NSvFlags">-2147483358</int>
- <string key="NSFrame">{{89, 4}, {81, 28}}</string>
+ <int key="NSvFlags">290</int>
+ <string key="NSFrame">{{64, 4}, {132, 28}}</string>
<reference key="NSSuperview" ref="950529842"/>
<bool key="NSEnabled">YES</bool>
<object class="BWTransparentButtonCell" key="NSCell" id="1030687082">
@@ -217,6 +217,14 @@
</object>
<int key="connectionID">34</int>
</object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">cancelTask:</string>
+ <reference key="source" ref="1001"/>
+ <reference key="destination" ref="540103856"/>
+ </object>
+ <int key="connectionID">35</int>
+ </object>
</object>
<object class="IBMutableOrderedSet" key="objectRecords">
<object class="NSArray" key="orderedObjects">
@@ -259,22 +267,13 @@
<reference key="object" ref="450247213"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
- <reference ref="540103856"/>
<reference ref="545295640"/>
+ <reference ref="540103856"/>
<reference ref="602680912"/>
</object>
<reference key="parent" ref="1005"/>
</object>
<object class="IBObjectRecord">
- <int key="objectID">17</int>
- <reference key="object" ref="540103856"/>
- <object class="NSMutableArray" key="children">
- <bool key="EncodedWithXMLCoder">YES</bool>
- <reference ref="1030687082"/>
- </object>
- <reference key="parent" ref="450247213"/>
- </object>
- <object class="IBObjectRecord">
<int key="objectID">15</int>
<reference key="object" ref="545295640"/>
<object class="NSMutableArray" key="children">
@@ -294,6 +293,15 @@
<reference key="parent" ref="545295640"/>
</object>
<object class="IBObjectRecord">
+ <int key="objectID">17</int>
+ <reference key="object" ref="540103856"/>
+ <object class="NSMutableArray" key="children">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <reference ref="1030687082"/>
+ </object>
+ <reference key="parent" ref="450247213"/>
+ </object>
+ <object class="IBObjectRecord">
<int key="objectID">18</int>
<reference key="object" ref="1030687082"/>
<reference key="parent" ref="540103856"/>
@@ -373,7 +381,7 @@
</object>
</object>
<nil key="sourceID"/>
- <int key="maxID">34</int>
+ <int key="maxID">37</int>
</object>
<object class="IBClassDescriber" key="IBDocument.Classes">
<object class="NSMutableArray" key="referencedPartialClassDescriptions">
@@ -555,6 +563,7 @@
<string>addDatabase:</string>
<string>analyzeTable:</string>
<string>backForwardInHistory:</string>
+ <string>cancelTask:</string>
<string>checkTable:</string>
<string>checksumTable:</string>
<string>chooseDatabase:</string>
@@ -636,6 +645,7 @@
<string>id</string>
<string>id</string>
<string>id</string>
+ <string>id</string>
</object>
</object>
<object class="NSMutableDictionary" key="outlets">
@@ -690,6 +700,7 @@
<string>tableWindow</string>
<string>tablesListInstance</string>
<string>taskCancelButton</string>
+ <string>taskCancellationCallbackObject</string>
<string>taskDescriptionText</string>
<string>taskProgressIndicator</string>
<string>taskProgressLayer</string>
@@ -754,6 +765,7 @@
<string>NSButton</string>
<string>id</string>
<string>id</string>
+ <string>id</string>
<string>NSBox</string>
<string>id</string>
<string>id</string>
diff --git a/Source/CustomQuery.m b/Source/CustomQuery.m
index f515f335..dc0d9b76 100644
--- a/Source/CustomQuery.m
+++ b/Source/CustomQuery.m
@@ -388,6 +388,7 @@
MCPStreamingResult *streamingResult = nil;
NSMutableString *errors = [NSMutableString string];
SEL callbackMethod = NULL;
+ NSString *taskButtonString;
int i, totalQueriesRun = 0, totalAffectedRows = 0;
double executionTime = 0;
@@ -425,6 +426,13 @@
long queryCount = [queries count];
NSMutableArray *tempQueries = [NSMutableArray arrayWithCapacity:queryCount];
+ // Enable task cancellation
+ if (queryCount > 1)
+ taskButtonString = NSLocalizedString(@"Stop queries", @"Stop queries string");
+ else
+ taskButtonString = NSLocalizedString(@"Stop query", @"Stop query string");
+ [tableDocumentInstance enableTaskCancellationWithTitle:taskButtonString callbackObject:nil callbackFunction:NULL];
+
// Perform the supplied queries in series
for ( i = 0 ; i < queryCount ; i++ ) {
@@ -450,7 +458,7 @@
// If this is the last query, retrieve and store the result; otherwise,
// discard the result without fully loading.
- if (totalQueriesRun == queryCount) {
+ if (totalQueriesRun == queryCount || [mySQLConnection queryCancelled]) {
// get column definitions for the result array
if (cqColumnDefinition) [cqColumnDefinition release];
@@ -507,7 +515,17 @@
totalAffectedRows += [streamingResult numOfRows];
// Store any error messages
- if ( ![[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) {
+ if ( ![[mySQLConnection getLastErrorMessage] isEqualToString:@""] || [mySQLConnection queryCancelled]) {
+
+ NSString *errorString;
+ if ([mySQLConnection queryCancelled]) {
+ if ([mySQLConnection queryCancellationUsedReconnect])
+ errorString = NSLocalizedString(@"Query cancelled. Please note that to cancel the query the connection had to be reset; transactions and connection variables were reset.", @"Query cancel by resetting connection error");
+ else
+ errorString = NSLocalizedString(@"Query cancelled.", @"Query cancelled error");
+ } else {
+ errorString = [mySQLConnection getLastErrorMessage];
+ }
// If the query errored, append error to the error log for display at the end
if ( queryCount > 1 ) {
@@ -519,35 +537,37 @@
// Update error text for the user
[errors appendString:[NSString stringWithFormat:NSLocalizedString(@"[ERROR in query %d] %@\n", @"error text when multiple custom query failed"),
i+1,
- [mySQLConnection getLastErrorMessage]]];
+ errorString]];
[errorText setStringValue:errors];
+
// ask the user to continue after detecting an error
- NSAlert *alert = [[[NSAlert alloc] init] autorelease];
- [alert addButtonWithTitle:NSLocalizedString(@"Run All", @"run all button")];
- [alert addButtonWithTitle:NSLocalizedString(@"Continue", @"continue button")];
- [alert addButtonWithTitle:NSLocalizedString(@"Stop", @"stop button")];
- [alert setMessageText:NSLocalizedString(@"MySQL Error", @"mysql error message")];
- [alert setInformativeText:[mySQLConnection getLastErrorMessage]];
- [alert setAlertStyle:NSWarningAlertStyle];
- int choice = [alert runModal];
- switch (choice){
- case NSAlertFirstButtonReturn:
- suppressErrorSheet = YES;
- case NSAlertSecondButtonReturn:
- break;
- default:
- if(i < queryCount-1) // output that message only if it was not the last one
- [errors appendString:NSLocalizedString(@"Execution stopped!\n", @"execution stopped message")];
- i = queryCount; // break for loop; for safety reasons stop the execution of the following queries
+ if (![mySQLConnection queryCancelled]) {
+ NSAlert *alert = [[[NSAlert alloc] init] autorelease];
+ [alert addButtonWithTitle:NSLocalizedString(@"Run All", @"run all button")];
+ [alert addButtonWithTitle:NSLocalizedString(@"Continue", @"continue button")];
+ [alert addButtonWithTitle:NSLocalizedString(@"Stop", @"stop button")];
+ [alert setMessageText:NSLocalizedString(@"MySQL Error", @"mysql error message")];
+ [alert setInformativeText:[mySQLConnection getLastErrorMessage]];
+ [alert setAlertStyle:NSWarningAlertStyle];
+ int choice = [alert runModal];
+ switch (choice){
+ case NSAlertFirstButtonReturn:
+ suppressErrorSheet = YES;
+ case NSAlertSecondButtonReturn:
+ break;
+ default:
+ if(i < queryCount-1) // output that message only if it was not the last one
+ [errors appendString:NSLocalizedString(@"Execution stopped!\n", @"execution stopped message")];
+ i = queryCount; // break for loop; for safety reasons stop the execution of the following queries
+ }
}
-
} else {
[errors appendString:[NSString stringWithFormat:NSLocalizedString(@"[ERROR in query %d] %@\n", @"error text when multiple custom query failed"),
i+1,
- [mySQLConnection getLastErrorMessage]]];
+ errorString]];
}
} else {
- [errors setString:[mySQLConnection getLastErrorMessage]];
+ [errors setString:errorString];
}
} else {
// Check if table/db list needs an update
@@ -557,6 +577,9 @@
if(!databaseWasChanged && [query isMatchedByRegex:@"(?i)\\b(use|drop\\s+database|drop\\s+schema)\\b\\s+."])
databaseWasChanged = YES;
}
+
+ // If the query was cancelled, end all queries.
+ if ([mySQLConnection queryCancelled]) break;
}
// Reload table list if at least one query began with drop, alter, rename, or create
@@ -605,7 +628,7 @@
}
// Error checking
- if ( [errors length] && !queryIsTableSorter ) {
+ if ( [mySQLConnection queryCancelled] || ([errors length] && !queryIsTableSorter)) {
// set the error text
[errorText setStringValue:errors];
// select the line x of the first error if error message contains "at line x"
@@ -656,7 +679,18 @@
}
// Set up the status string
- if ( totalQueriesRun > 1 ) {
+ if ( [mySQLConnection queryCancelled] ) {
+ if (totalQueriesRun > 1) {
+ [affectedRowsText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Cancelled in query %i, after %@", @"text showing multiple queries were cancelled"),
+ totalQueriesRun,
+ [NSString stringForTimeInterval:executionTime]
+ ]];
+ } else {
+ [affectedRowsText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Cancelled after %@", @"text showing a query was cancelled"),
+ [NSString stringForTimeInterval:executionTime]
+ ]];
+ }
+ } else if ( totalQueriesRun > 1 ) {
if (totalAffectedRows==1) {
[affectedRowsText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"1 row affected in total, by %i queries taking %@", @"text showing one row has been affected by multiple queries"),
totalQueriesRun,
diff --git a/Source/TableContent.h b/Source/TableContent.h
index 889a79e5..d0cc0ec3 100644
--- a/Source/TableContent.h
+++ b/Source/TableContent.h
@@ -65,7 +65,7 @@
NSString *compareType;
NSNumber *sortCol;
BOOL isEditingRow, isEditingNewRow, isSavingRow, isDesc, setLimit;
- BOOL isFiltered, isLimited, maxNumRowsIsEstimate;
+ BOOL isFiltered, isLimited, isInterruptedLoad, maxNumRowsIsEstimate;
NSUserDefaults *prefs;
int currentlyEditingRow, maxNumRows;
diff --git a/Source/TableContent.m b/Source/TableContent.m
index 552fbd41..d5c9e8d1 100644
--- a/Source/TableContent.m
+++ b/Source/TableContent.m
@@ -86,6 +86,7 @@
isFiltered = NO;
isLimited = NO;
+ isInterruptedLoad = NO;
prefs = [NSUserDefaults standardUserDefaults];
@@ -532,7 +533,10 @@
rowsToLoad = rowsToLoad - ([limitRowsField intValue]-1);
if (rowsToLoad > [prefs integerForKey:SPLimitResultsValue]) rowsToLoad = [prefs integerForKey:SPLimitResultsValue];
}
-
+
+ // If within a task, allow this query to be cancelled
+ [tableDocumentInstance enableTaskCancellationWithTitle:NSLocalizedString(@"Stop", @"Stop load") callbackObject:nil callbackFunction:NULL];
+
// Perform and process the query
[tableContentView performSelectorOnMainThread:@selector(noteNumberOfRowsChanged) withObject:nil waitUntilDone:YES];
[self setUsedQuery:queryString];
@@ -541,7 +545,7 @@
[streamingResult release];
// If the result is empty, and a limit is active, reset the limit
- if ([prefs boolForKey:SPLimitResults] && queryStringBeforeLimit && !tableRowsCount) {
+ if ([prefs boolForKey:SPLimitResults] && queryStringBeforeLimit && !tableRowsCount && ![mySQLConnection queryCancelled]) {
[limitRowsField setStringValue:@"1"];
queryString = [NSMutableString stringWithFormat:@"%@ LIMIT 0,%d", queryStringBeforeLimit, [prefs integerForKey:SPLimitResultsValue]];
[self setUsedQuery:queryString];
@@ -549,6 +553,14 @@
[self processResultIntoDataStorage:streamingResult approximateRowCount:[prefs integerForKey:SPLimitResultsValue]];
[streamingResult release];
}
+
+ if ([mySQLConnection queryCancelled] || ![[mySQLConnection getLastErrorMessage] isEqualToString:@""])
+ isInterruptedLoad = YES;
+ else
+ isInterruptedLoad = NO;
+
+ // End cancellation ability
+ [tableDocumentInstance disableTaskCancellation];
if ([prefs boolForKey:SPLimitResults]
&& ([limitRowsField intValue] > 1
@@ -821,8 +833,15 @@
NSString *rowString;
NSMutableString *countString = [NSMutableString string];
+ // If the result is partial due to an error or query cancellation, show a very basic count
+ if (isInterruptedLoad) {
+ if (tableRowsCount == 1)
+ [countString appendFormat:NSLocalizedString(@"%d row in partial load", @"text showing a single row a partially loaded result"), tableRowsCount];
+ else
+ [countString appendFormat:NSLocalizedString(@"%d rows in partial load", @"text showing how many rows are in a partially loaded result"), tableRowsCount];
+
// If no filter or limit is active, show just the count of rows in the table
- if (!isFiltered && !isLimited) {
+ } else if (!isFiltered && !isLimited) {
if (tableRowsCount == 1)
[countString appendFormat:NSLocalizedString(@"%d row in table", @"text showing a single row in the result"), tableRowsCount];
else
@@ -1128,7 +1147,7 @@
NSString *contextInfo = @"removerow";
- if (([tableContentView numberOfSelectedRows] == [tableContentView numberOfRows]) && !isFiltered && !isLimited) {
+ if (([tableContentView numberOfSelectedRows] == [tableContentView numberOfRows]) && !isFiltered && !isLimited && !isInterruptedLoad) {
contextInfo = @"removeallrows";
@@ -2288,7 +2307,7 @@
BOOL checkStatusCount = NO;
// For unfiltered and non-limited tables, use the result count - and update the status count
- if (!isLimited && !isFiltered) {
+ if (!isLimited && !isFiltered && !isInterruptedLoad) {
maxNumRows = tableRowsCount;
maxNumRowsIsEstimate = NO;
[tableDataInstance setStatusValue:[NSString stringWithFormat:@"%d", maxNumRows] forKey:@"Rows"];
diff --git a/Source/TableDocument.h b/Source/TableDocument.h
index f9e96730..db4b48ae 100644
--- a/Source/TableDocument.h
+++ b/Source/TableDocument.h
@@ -138,6 +138,9 @@
float taskProgressValueDisplayInterval;
NSTimer *taskDrawTimer;
NSViewAnimation *taskFadeAnimator;
+ BOOL taskCanBeCancelled;
+ id taskCancellationCallbackObject;
+ SEL taskCancellationCallbackSelector;
NSToolbar *mainToolbar;
NSToolbarItem *chooseDatabaseToolbarItem;
@@ -186,6 +189,9 @@
- (void) setTaskPercentage:(float)taskPercentage;
- (void) setTaskProgressToIndeterminate;
- (void) endTask;
+- (void) enableTaskCancellationWithTitle:(NSString *)buttonTitle callbackObject:(id)callbackObject callbackFunction:(SEL)callbackFunction;
+- (void) disableTaskCancellation;
+- (IBAction) cancelTask:(id)sender;
- (BOOL) isWorking;
- (void) setDatabaseListIsSelectable:(BOOL)isSelectable;
- (void) centerTaskWindow;
diff --git a/Source/TableDocument.m b/Source/TableDocument.m
index fa07e143..78df5e16 100644
--- a/Source/TableDocument.m
+++ b/Source/TableDocument.m
@@ -103,6 +103,9 @@
taskProgressValueDisplayInterval = 1;
taskDrawTimer = nil;
taskFadeAnimator = nil;
+ taskCanBeCancelled = NO;
+ taskCancellationCallbackObject = nil;
+ taskCancellationCallbackSelector = NULL;
keyChainID = nil;
}
@@ -223,15 +226,17 @@
NSLog(@"Progress indicator layer could not be loaded; progress display will not function correctly.");
}
- // Set up the progress indicator child window and later - add to main window, change indicator color and size
+ // Set up the progress indicator child window and layer - add to main window, change indicator color and size
+ [taskProgressIndicator setForeColor:[NSColor whiteColor]];
taskProgressWindow = [[NSWindow alloc] initWithContentRect:[taskProgressLayer bounds] styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO];
[taskProgressWindow setOpaque:NO];
- [taskProgressWindow setIgnoresMouseEvents:YES];
[taskProgressWindow setBackgroundColor:[NSColor clearColor]];
[taskProgressWindow setAlphaValue:0.0];
- [[taskProgressWindow contentView] addSubview:taskProgressLayer];
+ [taskProgressWindow orderWindow:NSWindowAbove relativeTo:[tableWindow windowNumber]];
[tableWindow addChildWindow:taskProgressWindow ordered:NSWindowAbove];
- [taskProgressIndicator setForeColor:[NSColor whiteColor]];
+ [taskProgressWindow release];
+ [taskProgressWindow setContentView:taskProgressLayer];
+ [self centerTaskWindow];
}
/**
@@ -1258,7 +1263,7 @@
[historyControl setEnabled:NO];
databaseListIsSelectable = NO;
[[NSNotificationCenter defaultCenter] postNotificationName:SPDocumentTaskStartNotification object:self];
-
+
// Schedule appearance of the task window in the near future
taskDrawTimer = [[NSTimer scheduledTimerWithTimeInterval:0.25 target:self selector:@selector(showTaskProgressWindow:) userInfo:nil repeats:NO] retain];
}
@@ -1333,6 +1338,9 @@
// Decrement the working level
_isWorkingLevel--;
+ // Ensure cancellation interface is reset
+ [self disableTaskCancellation];
+
// If all tasks have ended, re-enable the interface
if (!_isWorkingLevel) {
@@ -1358,6 +1366,57 @@
}
/**
+ * Allow a task to be cancelled, enabling the button with a supplied title
+ * and optionally supplying a callback object and function.
+ */
+- (void) enableTaskCancellationWithTitle:(NSString *)buttonTitle callbackObject:(id)callbackObject callbackFunction:(SEL)callbackFunction
+{
+
+ // If no task is active, return
+ if (!_isWorkingLevel) return;
+
+ if (callbackObject && callbackFunction) {
+ taskCancellationCallbackObject = callbackObject;
+ taskCancellationCallbackSelector = callbackFunction;
+ }
+ taskCanBeCancelled = YES;
+
+ [taskCancelButton setTitle:buttonTitle];
+ [taskCancelButton setEnabled:YES];
+ [taskCancelButton setHidden:NO];
+}
+
+/**
+ * Disable task cancellation. Called automatically at the end of a task.
+ */
+- (void) disableTaskCancellation
+{
+
+ // If no task is active, return
+ if (!_isWorkingLevel) return;
+
+ taskCanBeCancelled = NO;
+ taskCancellationCallbackObject = nil;
+ taskCancellationCallbackSelector = NULL;
+ [taskCancelButton setHidden:YES];
+}
+
+/**
+ * Action sent by the cancel button when it's active.
+ */
+- (IBAction) cancelTask:(id)sender
+{
+ if (!taskCanBeCancelled) return;
+
+ [taskCancelButton setEnabled:NO];
+ [mySQLConnection cancelCurrentQuery];
+
+ if (taskCancellationCallbackObject && taskCancellationCallbackSelector) {
+ [taskCancellationCallbackObject performSelector:taskCancellationCallbackSelector];
+ }
+}
+
+/**
* Returns whether the document is busy performing a task - allows UI or actions
* to be restricted as appropriate.
*/
@@ -3553,7 +3612,6 @@
if (spfSession) [spfSession release];
if (spfDocData) [spfDocData release];
if (keyChainID) [keyChainID release];
- if (taskProgressWindow) [taskProgressWindow release];
[super dealloc];
}
diff --git a/Source/TableSource.m b/Source/TableSource.m
index 932d9cdd..6228e6f1 100644
--- a/Source/TableSource.m
+++ b/Source/TableSource.m
@@ -1033,8 +1033,20 @@ returns a dictionary containing enum/set field names as key and possible values
}
- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex
-{
- return [(aTableView == tableSourceView) ? [tableFields objectAtIndex:rowIndex] : [indexes objectAtIndex:rowIndex] objectForKey:[aTableColumn identifier]];
+{
+ NSDictionary *theRow;
+
+ if (aTableView == tableSourceView) {
+
+ // Return a placeholder if the table is reloading
+ if (rowIndex >= [tableFields count]) return @"...";
+
+ theRow = [tableFields objectAtIndex:rowIndex];
+ } else {
+ theRow = [indexes objectAtIndex:rowIndex];
+ }
+
+ return [theRow objectForKey:[aTableColumn identifier]];
}
- (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex