aboutsummaryrefslogtreecommitdiffstats
path: root/Source
diff options
context:
space:
mode:
authorrowanbeentje <rowan@beent.je>2009-03-19 23:44:06 +0000
committerrowanbeentje <rowan@beent.je>2009-03-19 23:44:06 +0000
commitddf7d62d20614111acdd420075ef762d6deaa8d7 (patch)
treecf5ce70dde9c25d1b21da1cded5cee9f3e562704 /Source
parent4a8d0f731bbb03b9ace041d421800e5c6470326a (diff)
downloadsequelpro-ddf7d62d20614111acdd420075ef762d6deaa8d7.tar.gz
sequelpro-ddf7d62d20614111acdd420075ef762d6deaa8d7.tar.bz2
sequelpro-ddf7d62d20614111acdd420075ef762d6deaa8d7.zip
SPSQLParser changes:
- Use method caches for oft-called functions, and support caching of chunks of the underlying string for string walking, resulting in an overall 1.3x-1.4x parsing speedup. - Improve handling of multi-character comment starts (eg / or -) at the very end of strings - When running splitString... methods return even empty strings for consistency. - Update TableDump and TableData to match new usage SPStringAddition changes: - Add a formatter for time intervals. CMMCPConnection changes: - Add support for timing queries CustomQuery and nib changes: - Change the "Run Queries" button to "Run All". - Add a "Run Current" button, which runs the query the text caret is currently positioned inside; if text is actually selected, this changes to "Run Selection". This addresses Issue #43. - Amend the "rows affected" string to better reflect the actual number of rows altered by several queries, show the query count if > 1, and display the overall execution time of the queries. This addresses Issue #142. - No longer execute blank strings as part of the custom query, preventing errors.
Diffstat (limited to 'Source')
-rw-r--r--Source/CMMCPConnection.h2
-rw-r--r--Source/CMMCPConnection.m22
-rw-r--r--Source/CustomQuery.h9
-rw-r--r--Source/CustomQuery.m505
-rw-r--r--Source/SPSQLParser.h18
-rw-r--r--Source/SPSQLParser.m116
-rw-r--r--Source/SPStringAdditions.h1
-rw-r--r--Source/SPStringAdditions.m45
-rw-r--r--Source/SPTableData.m52
-rw-r--r--Source/TableDump.m5
10 files changed, 574 insertions, 201 deletions
diff --git a/Source/CMMCPConnection.h b/Source/CMMCPConnection.h
index e6479eb1..b26319ee 100644
--- a/Source/CMMCPConnection.h
+++ b/Source/CMMCPConnection.h
@@ -49,6 +49,7 @@
NSString *connectionHost;
int connectionPort;
NSString *connectionSocket;
+ float lastQueryExecutionTime;
NSTimer *keepAliveTimer;
NSDate *lastKeepAliveSuccess;
@@ -66,6 +67,7 @@
- (void) setParentWindow:(NSWindow *)theWindow;
- (BOOL) selectDB:(NSString *) dbName;
- (CMMCPResult *) queryString:(NSString *) query;
+- (float) lastQueryExecutionTime;
- (MCPResult *) listDBsLike:(NSString *) dbsName;
- (BOOL) checkConnection;
- (void) setDelegate:(id)object;
diff --git a/Source/CMMCPConnection.m b/Source/CMMCPConnection.m
index 109ab280..64315ff6 100644
--- a/Source/CMMCPConnection.m
+++ b/Source/CMMCPConnection.m
@@ -68,6 +68,7 @@ static void forcePingTimeout(int signalNumber);
connectionSocket = nil;
keepAliveTimer = nil;
lastKeepAliveSuccess = nil;
+ lastQueryExecutionTime = 0;
if (![NSBundle loadNibNamed:@"ConnectionErrorDialog" owner:self]) {
NSLog(@"Connection error dialog could not be loaded; connection failure handling will not function correctly.");
}
@@ -345,6 +346,7 @@ static void forcePingTimeout(int signalNumber);
CMMCPResult *theResult;
const char *theCQuery = [self cStringFromString:query];
int theQueryCode;
+ NSDate *queryStartDate;
// If no connection is present, return nil.
if (!mConnected) return nil;
@@ -360,10 +362,16 @@ static void forcePingTimeout(int signalNumber);
[delegate willQueryString:query];
}
- if (0 == (theQueryCode = mysql_query(mConnection, theCQuery))) {
+ // Run the query, storing run time (note this will include some network and overhead)
+ queryStartDate = [NSDate date];
+ theQueryCode = mysql_query(mConnection, theCQuery);
+ lastQueryExecutionTime = [[NSDate date] timeIntervalSinceDate:queryStartDate];
+
+ // Retrieve the result or error appropriately.
+ if (0 == theQueryCode) {
if (mysql_field_count(mConnection) != 0) {
- // Use CMMCPResult instad of MCPResult
+ // Use CMMCPResult instead of MCPResult
theResult = [[CMMCPResult alloc] initWithMySQLPtr:mConnection encoding:mEncoding timeZone:mTimeZone];
} else {
return nil;
@@ -385,6 +393,16 @@ static void forcePingTimeout(int signalNumber);
/*
+ * Return the time taken to execute the last query. This should be close to the time it took
+ * the server to run the query, but will include network lag and some client library overhead.
+ */
+- (float) lastQueryExecutionTime
+{
+ return lastQueryExecutionTime;
+}
+
+
+/*
* Modified version of selectDB to be used in Sequel Pro.
* Checks the connection exists, and handles keepalive, otherwise calling the parent implementation.
*/
diff --git a/Source/CustomQuery.h b/Source/CustomQuery.h
index 8e81c7e5..c3a61b56 100644
--- a/Source/CustomQuery.h
+++ b/Source/CustomQuery.h
@@ -43,6 +43,8 @@
IBOutlet id queryFavoritesView;
IBOutlet id removeQueryFavoriteButton;
IBOutlet id copyQueryFavoriteButton;
+ IBOutlet id runSelectionButton;
+ IBOutlet id runAllButton;
NSArray *queryResult;
NSUserDefaults *prefs;
@@ -52,7 +54,8 @@
}
// IBAction methods
-- (IBAction)performQuery:(id)sender;
+- (IBAction)runAllQueries:(id)sender;
+- (IBAction)runSelectedQueries:(id)sender;
- (IBAction)chooseQueryFavorite:(id)sender;
- (IBAction)chooseQueryHistory:(id)sender;
- (IBAction)closeSheet:(id)sender;
@@ -63,6 +66,10 @@
- (IBAction)copyQueryFavorite:(id)sender;
- (IBAction)closeQueryFavoritesSheet:(id)sender;
+// Query actions
+- (void)performQueries:(NSArray *)queries;
+- (NSString *)queryAtPosition:(long)position;
+
// Accessors
- (NSArray *)currentResult;
diff --git a/Source/CustomQuery.m b/Source/CustomQuery.m
index e917e984..94ec6cba 100644
--- a/Source/CustomQuery.m
+++ b/Source/CustomQuery.m
@@ -25,177 +25,77 @@
#import "CustomQuery.h"
#import "SPSQLParser.h"
#import "SPGrowlController.h"
+#import "SPStringAdditions.h"
+
@implementation CustomQuery
-//IBAction methods
-- (IBAction)performQuery:(id)sender;
+
+
+#pragma mark IBAction methods
+
+
/*
-performs the mysql-query given by the user
-sets the tableView columns corresponding to the mysql-result
-*/
-{
+ * Split all the queries in the text view, split them into individual queries,
+ * and run sequentially.
+ */
+- (IBAction)runAllQueries:(id)sender
+{
+ SPSQLParser *queryParser;
+ NSArray *queries;
+
// Fixes bug in key equivalents.
- if ([[NSApp currentEvent] type] == NSKeyUp)
- {
+ if ([[NSApp currentEvent] type] == NSKeyUp) {
return;
}
-
- NSArray *theColumns;
- NSTableColumn *theCol;
- CMMCPResult *theResult = nil;
- NSArray *queries;
- NSMutableArray *menuItems = [NSMutableArray array];
- NSMutableArray *tempResult = [NSMutableArray array];
- NSMutableString *errors = [NSMutableString string];
- SPSQLParser *queryParser;
- int i;
-
- // Notify listeners that a query has started
- [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self];
// Retrieve the custom query string and split it into separate SQL queries
queryParser = [[SPSQLParser alloc] initWithString:[textView string]];
queries = [queryParser splitStringByCharacter:';'];
[queryParser release];
- // Perform the queries in series
- for ( i = 0 ; i < [queries count] ; i++ ) {
- theResult = [mySQLConnection queryString:[queries objectAtIndex:i]];
- if ( ![[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) {
-
- // If the query errored, append error to the error log for display at the end
- if ( [queries count] > 1 ) {
- [errors appendString:[NSString stringWithFormat:NSLocalizedString(@"[ERROR in query %d] %@\n", @"error text when multiple custom query failed"),
- i+1,
- [mySQLConnection getLastErrorMessage]]];
- } else {
- [errors setString:[mySQLConnection getLastErrorMessage]];
- }
- }
- }
-
- //perform empty query if no query is given
- if ( [queries count] == 0 ) {
- theResult = [mySQLConnection queryString:@""];
- [errors setString:[mySQLConnection getLastErrorMessage]];
- }
-
-//put result in array
- [queryResult release];
- queryResult = nil;
- if ( nil != theResult )
- {
- int r = [theResult numOfRows];
- if (r) [theResult dataSeek:0];
- for ( i = 0 ; i < r ; i++ ) {
- [tempResult addObject:[theResult fetchRowAsArray]];
- }
- queryResult = [[NSArray arrayWithArray:tempResult] retain];
- }
-
-//add query to history
- [queryHistoryButton insertItemWithTitle:[textView string] atIndex:1];
- while ( [queryHistoryButton numberOfItems] > 21 ) {
- [queryHistoryButton removeItemAtIndex:[queryHistoryButton numberOfItems]-1];
- }
- for ( i = 1 ; i < [queryHistoryButton numberOfItems] ; i++ )
- {
- [menuItems addObject:[queryHistoryButton itemTitleAtIndex:i]];
- }
- [prefs setObject:menuItems forKey:@"queryHistory"];
+ [self performQueries:queries];
-//select the text of the query textView and set standard font
+ // Select the text of the query textView for re-editing and set standard font
[textView selectAll:self];
- if ( [errors length] ) {
- [errorText setStringValue:errors];
- } else {
- [errorText setStringValue:NSLocalizedString(@"There were no errors.", @"text shown when query was successfull")];
- }
- if ( [mySQLConnection affectedRows] != -1 ) {
- [affectedRowsText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"%@ row(s) affected", @"text showing how many rows have been affected"),
- [[NSNumber numberWithLongLong:[mySQLConnection affectedRows]] stringValue]]];
- } else {
- [affectedRowsText setStringValue:@""];
- }
if ( [prefs boolForKey:@"useMonospacedFonts"] ) {
[textView setFont:[NSFont fontWithName:@"Monaco" size:[NSFont smallSystemFontSize]]];
} else {
[textView setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
}
+}
- if ( !theResult || ![theResult numOfRows] ) {
-//no rows in result
- //free tableView
- theColumns = [customQueryView tableColumns];
- while ([theColumns count]) {
- [customQueryView removeTableColumn:[theColumns objectAtIndex:0]];
- }
-// theCol = [[NSTableColumn alloc] initWithIdentifier:@""];
-// [[theCol headerCell] setStringValue:@""];
-// [customQueryView addTableColumn:theCol];
-// [customQueryView sizeLastColumnToFit];
- [customQueryView reloadData];
-// [theCol release];
-
- //query finished
- [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self];
-
- // Query finished Growl notification
- [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Query Finished"
- description:[NSString stringWithFormat:NSLocalizedString(@"%@",@"description for query finished growl notification"), [errorText stringValue]]
- notificationName:@"Query Finished"];
-
- return;
- }
-
-//set columns
-//remove all columns
- theColumns = [customQueryView tableColumns];
-// i=0;
- while ([theColumns count]) {
- [customQueryView removeTableColumn:[theColumns objectAtIndex:0]];
-// i++;
- }
+/*
+ * Depending on selection, run either the query containing the selection caret (if the caret is
+ * at a single point within the text view), or run the selected text (if a text range is selected).
+ */
+- (IBAction)runSelectedQueries:(id)sender
+{
+ NSArray *queries;
+ NSString *query;
+ NSRange selectedRange = [textView selectedRange];
+ SPSQLParser *queryParser;
-//add columns, corresponding to the query result
- theColumns = [theResult fetchFieldNames];
- for ( i = 0 ; i < [theResult numOfFields] ; i++) {
- theCol = [[NSTableColumn alloc] initWithIdentifier:[NSNumber numberWithInt:i]];
- [theCol setResizingMask:NSTableColumnUserResizingMask];
- NSTextFieldCell *dataCell = [[[NSTextFieldCell alloc] initTextCell:@""] autorelease];
- [dataCell setEditable:NO];
- if ( [prefs boolForKey:@"useMonospacedFonts"] ) {
- [dataCell setFont:[NSFont fontWithName:@"Monaco" size:10]];
- } else {
- [dataCell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
+ // If the current selection is a single caret position, run the current query.
+ if (selectedRange.length == 0) {
+ query = [self queryAtPosition:selectedRange.location];
+ if (!query) {
+ NSBeep();
+ return;
}
- [dataCell setLineBreakMode:NSLineBreakByTruncatingTail];
- [theCol setDataCell:dataCell];
- [[theCol headerCell] setStringValue:[theColumns objectAtIndex:i]];
+ queries = [NSArray arrayWithObject:query];
- [customQueryView addTableColumn:theCol];
- [theCol release];
+ // Otherwise, run the selected text.
+ } else {
+ queryParser = [[SPSQLParser alloc] initWithString:[[textView string] substringWithRange:selectedRange]];
+ queries = [queryParser splitStringByCharacter:';'];
+ [queryParser release];
}
- [customQueryView sizeLastColumnToFit];
- //tries to fix problem with last row (otherwise to small)
- //sets last column to width of the first if smaller than 30
- //problem not fixed for resizing window
- if ( [[customQueryView tableColumnWithIdentifier:[NSNumber numberWithInt:[theColumns count]-1]] width] < 30 )
- [[customQueryView tableColumnWithIdentifier:[NSNumber numberWithInt:[theColumns count]-1]]
- setWidth:[[customQueryView tableColumnWithIdentifier:[NSNumber numberWithInt:0]] width]];
- [customQueryView reloadData];
-
- //query finished
- [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self];
-
- // Query finished Growl notification
- [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Query Finished"
- description:[NSString stringWithFormat:NSLocalizedString(@"%@",@"description for query finished growl notification"), [errorText stringValue]]
- notificationName:@"Query Finished"];
+ [self performQueries:queries];
}
+
- (IBAction)chooseQueryFavorite:(id)sender
/*
insert the choosen favorite query in the query textView or save query to favorites or opens window to edit favorites
@@ -255,7 +155,10 @@ closes the sheet
}
-//queryFavoritesSheet methods
+#pragma mark -
+#pragma mark queryFavoritesSheet methods
+
+
- (IBAction)addQueryFavorite:(id)sender
/*
adds a query favorite
@@ -352,7 +255,229 @@ closes queryFavoritesSheet and saves favorites to preferences
}
-//getter methods
+#pragma mark -
+#pragma mark Query actions
+
+
+- (void)performQueries:(NSArray *)queries;
+/*
+performs the mysql-query given by the user
+sets the tableView columns corresponding to the mysql-result
+*/
+{
+
+ NSArray *theColumns;
+ NSTableColumn *theCol;
+ CMMCPResult *theResult = nil;
+ NSMutableArray *menuItems = [NSMutableArray array];
+ NSMutableArray *tempResult = [NSMutableArray array];
+ NSMutableString *errors = [NSMutableString string];
+ int i, totalQueriesRun = 0, totalAffectedRows = 0;
+ float executionTime = 0;
+
+ // Notify listeners that a query has started
+ [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self];
+
+ // Perform the supplied queries in series
+ for ( i = 0 ; i < [queries count] ; i++ ) {
+
+ // Don't run blank queries, or queries which only contain whitespace.
+ if ([[[queries objectAtIndex:i] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] length] == 0)
+ continue;
+
+ // Run the query, timing execution (note this also includes network and overhead)
+ theResult = [mySQLConnection queryString:[queries objectAtIndex:i]];
+ executionTime += [mySQLConnection lastQueryExecutionTime];
+ totalQueriesRun++;
+
+ // Record any affected rows
+ if ( [mySQLConnection affectedRows] != -1 )
+ totalAffectedRows += [mySQLConnection affectedRows];
+
+ // Store any error messages
+ if ( ![[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) {
+
+ // If the query errored, append error to the error log for display at the end
+ if ( [queries count] > 1 ) {
+ [errors appendString:[NSString stringWithFormat:NSLocalizedString(@"[ERROR in query %d] %@\n", @"error text when multiple custom query failed"),
+ i+1,
+ [mySQLConnection getLastErrorMessage]]];
+ } else {
+ [errors setString:[mySQLConnection getLastErrorMessage]];
+ }
+ }
+ }
+
+ //perform empty query if no query is given
+ if ( [queries count] == 0 ) {
+ theResult = [mySQLConnection queryString:@""];
+ [errors setString:[mySQLConnection getLastErrorMessage]];
+ }
+
+//put result in array
+ [queryResult release];
+ queryResult = nil;
+ if ( nil != theResult )
+ {
+ int r = [theResult numOfRows];
+ if (r) [theResult dataSeek:0];
+ for ( i = 0 ; i < r ; i++ ) {
+ [tempResult addObject:[theResult fetchRowAsArray]];
+ }
+ queryResult = [[NSArray arrayWithArray:tempResult] retain];
+ }
+
+//add query to history
+ [queryHistoryButton insertItemWithTitle:[queries componentsJoinedByString:@"; "] atIndex:1];
+ while ( [queryHistoryButton numberOfItems] > 21 ) {
+ [queryHistoryButton removeItemAtIndex:[queryHistoryButton numberOfItems]-1];
+ }
+ for ( i = 1 ; i < [queryHistoryButton numberOfItems] ; i++ )
+ {
+ [menuItems addObject:[queryHistoryButton itemTitleAtIndex:i]];
+ }
+ [prefs setObject:menuItems forKey:@"queryHistory"];
+
+ if ( [errors length] ) {
+ [errorText setStringValue:errors];
+ } else {
+ [errorText setStringValue:NSLocalizedString(@"There were no errors.", @"text shown when query was successfull")];
+ }
+
+ // Set up the status string
+ if ( totalQueriesRun > 1 ) {
+ [affectedRowsText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"%i total row(s) affected, by %i queries taking %@", @"text showing how many rows have been affected by multiple queries"),
+ totalAffectedRows,
+ totalQueriesRun,
+ [NSString stringForTimeInterval:executionTime]
+ ]];
+ } else {
+ [affectedRowsText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"%i row(s) affected, taking %@", @"text showing how many rows have been affected by a single query"),
+ totalAffectedRows,
+ [NSString stringForTimeInterval:executionTime]
+ ]];
+ }
+
+ if ( !theResult || ![theResult numOfRows] ) {
+//no rows in result
+ //free tableView
+ theColumns = [customQueryView tableColumns];
+ while ([theColumns count]) {
+ [customQueryView removeTableColumn:[theColumns objectAtIndex:0]];
+ }
+// theCol = [[NSTableColumn alloc] initWithIdentifier:@""];
+// [[theCol headerCell] setStringValue:@""];
+// [customQueryView addTableColumn:theCol];
+// [customQueryView sizeLastColumnToFit];
+ [customQueryView reloadData];
+// [theCol release];
+
+ //query finished
+ [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self];
+
+ // Query finished Growl notification
+ [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Query Finished"
+ description:[NSString stringWithFormat:NSLocalizedString(@"%@",@"description for query finished growl notification"), [errorText stringValue]]
+ notificationName:@"Query Finished"];
+
+ return;
+ }
+
+//set columns
+//remove all columns
+ theColumns = [customQueryView tableColumns];
+// i=0;
+ while ([theColumns count]) {
+ [customQueryView removeTableColumn:[theColumns objectAtIndex:0]];
+// i++;
+ }
+
+//add columns, corresponding to the query result
+ theColumns = [theResult fetchFieldNames];
+ for ( i = 0 ; i < [theResult numOfFields] ; i++) {
+ theCol = [[NSTableColumn alloc] initWithIdentifier:[NSNumber numberWithInt:i]];
+ [theCol setResizingMask:NSTableColumnUserResizingMask];
+ NSTextFieldCell *dataCell = [[[NSTextFieldCell alloc] initTextCell:@""] autorelease];
+ [dataCell setEditable:NO];
+ if ( [prefs boolForKey:@"useMonospacedFonts"] ) {
+ [dataCell setFont:[NSFont fontWithName:@"Monaco" size:10]];
+ } else {
+ [dataCell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
+ }
+ [dataCell setLineBreakMode:NSLineBreakByTruncatingTail];
+ [theCol setDataCell:dataCell];
+ [[theCol headerCell] setStringValue:[theColumns objectAtIndex:i]];
+
+ [customQueryView addTableColumn:theCol];
+ [theCol release];
+ }
+
+ [customQueryView sizeLastColumnToFit];
+ //tries to fix problem with last row (otherwise to small)
+ //sets last column to width of the first if smaller than 30
+ //problem not fixed for resizing window
+ if ( [[customQueryView tableColumnWithIdentifier:[NSNumber numberWithInt:[theColumns count]-1]] width] < 30 )
+ [[customQueryView tableColumnWithIdentifier:[NSNumber numberWithInt:[theColumns count]-1]]
+ setWidth:[[customQueryView tableColumnWithIdentifier:[NSNumber numberWithInt:0]] width]];
+ [customQueryView reloadData];
+
+ //query finished
+ [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self];
+
+ // Query finished Growl notification
+ [[SPGrowlController sharedGrowlController] notifyWithTitle:@"Query Finished"
+ description:[NSString stringWithFormat:NSLocalizedString(@"%@",@"description for query finished growl notification"), [errorText stringValue]]
+ notificationName:@"Query Finished"];
+}
+
+/*
+ * Retrieve the query at a position specified within the custom query
+ * text view. This will return nil if the position specified is beyond
+ * the available string or if an empty query would be returned.
+ */
+- (NSString *)queryAtPosition:(long)position
+{
+ SPSQLParser *customQueryParser;
+ NSArray *queries;
+ NSString *query = nil;
+ int i, queryPosition = 0;
+
+ // If the supplied position is negative or beyond the end of the string, return nil.
+ if (position < 0 || position > [[textView string] length])
+ return nil;
+
+ // Split the current text into queries
+ customQueryParser = [[SPSQLParser alloc] initWithString:[textView string]];
+ queries = [[NSArray alloc] initWithArray:[customQueryParser splitStringByCharacter:';']];
+ [customQueryParser release];
+
+ // Walk along the array of queries to identify the current query - taking into account
+ // the extra semicolon at the end of each query
+ for (i = 0; i < [queries count]; i++ ) {
+ queryPosition += [[queries objectAtIndex:i] length];
+ if (queryPosition >= position) {
+ query = [NSString stringWithString:[queries objectAtIndex:i]];
+ break;
+ }
+ queryPosition++;
+ }
+
+ [queries release];
+
+ // Ensure the string isn't empty.
+ // (We could also strip comments for this check, but that prevents use of conditional comments)
+ if ([[query stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] length] == 0)
+ return nil;
+
+ // Return the located string.
+ return query;
+}
+
+
+#pragma mark -
+#pragma mark Accessors
+
+
- (NSArray *)currentResult
/*
returns the current result (as shown in custom result view) as array, the first object containing the field names as array, the following objects containing the rows as array
@@ -384,7 +509,10 @@ returns the current result (as shown in custom result view) as array, the first
}
-//additional methods
+#pragma mark -
+#pragma mark Additional methods
+
+
- (void)setConnection:(CMMCPConnection *)theConnection
/*
sets the connection (received from TableDocument) and makes things that have to be done only once
@@ -447,11 +575,14 @@ inserts the query in the textView and performs query
*/
{
[textView setString:query];
- [self performQuery:self];
+ [self runAllQueries:self];
}
-//tableView datasource methods
+#pragma mark -
+#pragma mark TableView datasource methods
+
+
- (int)numberOfRowsInTableView:(NSTableView *)aTableView
{
if ( aTableView == customQueryView ) {
@@ -668,7 +799,10 @@ opens sheet with value when double clicking on a field
}
-//splitView delegate methods
+#pragma mark -
+#pragma mark SplitView delegate methods
+
+
- (BOOL)splitView:(NSSplitView *)sender canCollapseSubview:(NSView *)subview
/*
tells the splitView that it can collapse views
@@ -702,7 +836,10 @@ defines min position of splitView
}
-//textView delegate methods
+#pragma mark -
+#pragma mark TextView delegate methods
+
+
- (BOOL)textView:(NSTextView *)aTextView doCommandBySelector:(SEL)aSelector
/*
traps enter key and
@@ -714,7 +851,7 @@ traps enter key and
if ( [aTextView methodForSelector:aSelector] == [aTextView methodForSelector:@selector(insertNewline:)] &&
[[[NSApp currentEvent] characters] isEqualToString:@"\003"] )
{
- [self performQuery:self];
+ [self runAllQueries:self];
return YES;
} else {
return NO;
@@ -732,6 +869,68 @@ traps enter key and
}
/*
+ * A notification posted when the selection changes within the text view;
+ * used to control the run-currentrun-selection button state and action.
+ */
+- (void)textViewDidChangeSelection:(NSNotification *)aNotification
+{
+
+ // Ensure that the notification is from the custom query text view
+ if ( [aNotification object] != textView ) return;
+
+ // If no text is selected, disable the button.
+ if ( [textView selectedRange].location == NSNotFound ) {
+ [runSelectionButton setEnabled:NO];
+ return;
+ }
+
+ // If the current selection is a single caret position, update the button based on
+ // whether the caret is inside a valid query.
+ if ([textView selectedRange].length == 0) {
+ int selectionPosition = [textView selectedRange].location;
+ int movedRangeStart, movedRangeLength;
+ NSRange oldSelection;
+
+ // Retrieve the old selection position
+ [[[aNotification userInfo] objectForKey:@"NSOldSelectedCharacterRange"] getValue:&oldSelection];
+
+ // Only process the query text if the selection previously had length, or moved more than 100 characters,
+ // or the intervening space contained a semicolon, or typing has been performed with no current query.
+ // This adds more checks to every keypress, but ensures the majority of the actions don't incur a
+ // parsing overhead - which is cheap on small text strings but heavy of large queries.
+ movedRangeStart = (selectionPosition < oldSelection.location)?selectionPosition:oldSelection.location;
+ movedRangeLength = abs(selectionPosition - oldSelection.location);
+ if (oldSelection.length > 0
+ || movedRangeLength > 100
+ || oldSelection.location > [[textView string] length]
+ || [[textView string] rangeOfString:@";" options:0 range:NSMakeRange(movedRangeStart, movedRangeLength)].location != NSNotFound
+ || (![runSelectionButton isEnabled] && selectionPosition > oldSelection.location
+ && [[[[textView string] substringWithRange:NSMakeRange(movedRangeStart, movedRangeLength)] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] length])
+ ) {
+
+ [runSelectionButton setTitle:NSLocalizedString(@"Run Current", @"Title of button to run current query in custom query view")];
+
+ // If a valid query is present at the cursor position, enable the button
+ if ([self queryAtPosition:selectionPosition]) {
+ [runSelectionButton setEnabled:YES];
+ } else {
+ [runSelectionButton setEnabled:NO];
+ }
+ }
+
+ // For selection ranges, enable the button.
+ } else {
+ [runSelectionButton setTitle:NSLocalizedString(@"Run Selection", @"Title of button to run selected text in custom query view")];
+ [runSelectionButton setEnabled:YES];
+ }
+}
+
+
+#pragma mark -
+#pragma mark TableView notifications
+
+
+/*
* Updates various interface elements based on the current table view selection.
*/
- (void)tableViewSelectionDidChange:(NSNotification *)notification
@@ -744,6 +943,10 @@ traps enter key and
}
}
+
+#pragma mark -
+
+
// Last but not least
- (id)init;
{
diff --git a/Source/SPSQLParser.h b/Source/SPSQLParser.h
index 14ac6f9d..3e68501c 100644
--- a/Source/SPSQLParser.h
+++ b/Source/SPSQLParser.h
@@ -23,6 +23,12 @@
#import <Cocoa/Cocoa.h>
+/*
+ * Define the length of the character cache to use when parsing instead of accessing
+ * via characterAtIndex:. There is a balance here between updating the cache very
+ * often and access penalties; 1500 appears a reasonable compromise.
+ */
+#define CHARACTER_CACHE_LENGTH 1500
/*
* This class provides a string class intended for SQL parsing. It extends NSMutableString,
@@ -53,6 +59,9 @@
@interface SPSQLParser : NSMutableString
{
id string;
+ unichar *stringCharCache;
+ long charCacheStart;
+ long charCacheEnd;
}
@@ -210,6 +219,15 @@ typedef enum _SPCommentTypes {
- (long) endIndexOfStringQuotedByCharacter:(unichar)quoteCharacter startingAtIndex:(long)index;
- (long) endIndexOfCommentOfType:(SPCommentType)commentType startingAtIndex:(long)index;
+/*
+ * Cacheing methods to enable a faster alternative to characterAtIndex: when walking strings, and overrides to update.
+ */
+- (unichar) charAtIndex:(long)index;
+- (void) clearCharCache;
+- (void) deleteCharactersInRange:(NSRange)aRange;
+- (void) insertString:(NSString *)aString atIndex:(NSUInteger)anIndex;
+
+
/* Required and primitive methods to allow subclassing class cluster */
#pragma mark -
diff --git a/Source/SPSQLParser.m b/Source/SPSQLParser.m
index 9828f529..e5c490da 100644
--- a/Source/SPSQLParser.m
+++ b/Source/SPSQLParser.m
@@ -60,11 +60,11 @@
case '-':
if (stringLength < currentStringIndex + 2) break;
if ([string characterAtIndex:currentStringIndex+1] != '-') break;
- if (![[NSCharacterSet whitespaceAndNewlineCharacterSet] characterIsMember:[string characterAtIndex:currentStringIndex+2]]) break;
+ if (![[NSCharacterSet whitespaceCharacterSet] characterIsMember:[string characterAtIndex:currentStringIndex+2]]) break;
commentEndIndex = [self endIndexOfCommentOfType:SPDoubleDashComment startingAtIndex:currentStringIndex];
// Remove the comment
- [string deleteCharactersInRange:NSMakeRange(currentStringIndex, commentEndIndex - currentStringIndex + 1)];
+ [self deleteCharactersInRange:NSMakeRange(currentStringIndex, commentEndIndex - currentStringIndex + 1)];
stringLength -= commentEndIndex - currentStringIndex + 1;
currentStringIndex--;
break;
@@ -73,7 +73,7 @@
commentEndIndex = [self endIndexOfCommentOfType:SPHashComment startingAtIndex:currentStringIndex];
// Remove the comment
- [string deleteCharactersInRange:NSMakeRange(currentStringIndex, commentEndIndex - currentStringIndex + 1)];
+ [self deleteCharactersInRange:NSMakeRange(currentStringIndex, commentEndIndex - currentStringIndex + 1)];
stringLength -= commentEndIndex - currentStringIndex + 1;
currentStringIndex--;
break;
@@ -85,7 +85,7 @@
commentEndIndex = [self endIndexOfCommentOfType:SPCStyleComment startingAtIndex:currentStringIndex];
// Remove the comment
- [string deleteCharactersInRange:NSMakeRange(currentStringIndex, commentEndIndex - currentStringIndex + 1)];
+ [self deleteCharactersInRange:NSMakeRange(currentStringIndex, commentEndIndex - currentStringIndex + 1)];
stringLength -= commentEndIndex - currentStringIndex + 1;
currentStringIndex--;
break;
@@ -158,7 +158,7 @@
if (stringIndex == NSNotFound) return NO;
// If it has been found, trim the string appropriately and return YES
- [string deleteCharactersInRange:NSMakeRange(0, stringIndex + (inclusive?1:0))];
+ [self deleteCharactersInRange:NSMakeRange(0, stringIndex + (inclusive?1:0))];
return YES;
}
@@ -213,7 +213,7 @@
// Select the appropriate string range, truncate the current string, and return the selected string
resultString = [NSString stringWithString:[string substringWithRange:NSMakeRange(0, stringIndex + (inclusiveReturn?1:0))]];
- [string deleteCharactersInRange:NSMakeRange(0, stringIndex + (inclusiveTrim?1:0))];
+ [self deleteCharactersInRange:NSMakeRange(0, stringIndex + (inclusiveTrim?1:0))];
return resultString;
}
@@ -255,7 +255,7 @@
- (NSString *) stringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter inclusively:(BOOL)inclusive skippingBrackets:(BOOL)skipBrackets ignoringQuotedStrings:(BOOL)ignoreQuotedStrings
{
long fromCharacterIndex, toCharacterIndex;
-
+
// Look for the first occurrence of the from: character
fromCharacterIndex = [self firstOccurrenceOfCharacter:fromCharacter afterIndex:-1 skippingBrackets:skipBrackets ignoringQuotedStrings:ignoreQuotedStrings];
if (fromCharacterIndex == NSNotFound) return nil;
@@ -318,7 +318,7 @@
// Select the correct part of the string, truncate the current string, and return the selected string.
resultString = [string substringWithRange:NSMakeRange(fromCharacterIndex + (inclusiveReturn?0:1), toCharacterIndex + (inclusiveReturn?1:-1) - fromCharacterIndex)];
- [string deleteCharactersInRange:NSMakeRange(fromCharacterIndex + (inclusiveTrim?0:1), toCharacterIndex + (inclusiveTrim?1:-1) - fromCharacterIndex)];
+ [self deleteCharactersInRange:NSMakeRange(fromCharacterIndex + (inclusiveTrim?0:1), toCharacterIndex + (inclusiveTrim?1:-1) - fromCharacterIndex)];
return resultString;
}
@@ -358,16 +358,14 @@
NSMutableArray *resultsArray = [NSMutableArray array];
long stringIndex = -1, nextIndex = 0;
- // Walk through the string finding the character to split by, and add non-zero length strings.
+ // Walk through the string finding the character to split by, and add all strings to the array.
while (1) {
nextIndex = [self firstOccurrenceOfCharacter:character afterIndex:stringIndex skippingBrackets:skipBrackets ignoringQuotedStrings:ignoreQuotedStrings];
if (nextIndex == NSNotFound) {
break;
}
- if (nextIndex - stringIndex - 1 > 0) {
- [resultsArray addObject:[string substringWithRange:NSMakeRange(stringIndex+1, nextIndex - stringIndex - 1)]];
- }
+ [resultsArray addObject:[string substringWithRange:NSMakeRange(stringIndex+1, nextIndex - stringIndex - 1)]];
stringIndex = nextIndex;
}
@@ -408,12 +406,16 @@
long stringLength = [string length];
int bracketingLevel = 0;
+ // Cache frequently used selectors, avoiding dynamic binding overhead
+ IMP charAtIndex = [self methodForSelector:@selector(charAtIndex:)];
+ IMP endIndex = [self methodForSelector:@selector(endIndexOfStringQuotedByCharacter:startingAtIndex:)];
+
// Sanity check inputs
if (startIndex < -1) startIndex = -1;
// Walk along the string, processing characters
for (currentStringIndex = startIndex + 1; currentStringIndex < stringLength; currentStringIndex++) {
- currentCharacter = [string characterAtIndex:currentStringIndex];
+ currentCharacter = (unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), currentStringIndex);
// Check for the ending character, and if it has been found and quoting/brackets is valid, return.
if (currentCharacter == character) {
@@ -430,7 +432,7 @@
case '"':
case '`':
if (!ignoreQuotedStrings) break;
- quotedStringEndIndex = [self endIndexOfStringQuotedByCharacter:currentCharacter startingAtIndex:currentStringIndex+1];
+ quotedStringEndIndex = (long)(*endIndex)(self, @selector(endIndexOfStringQuotedByCharacter:startingAtIndex:), currentCharacter, currentStringIndex+1);
if (quotedStringEndIndex == NSNotFound) {
return NSNotFound;
}
@@ -449,8 +451,8 @@
// For comments starting "--[\s]", ensure the start syntax is valid before proceeding.
case '-':
if (stringLength < currentStringIndex + 2) break;
- if ([string characterAtIndex:currentStringIndex+1] != '-') break;
- if (![[NSCharacterSet whitespaceAndNewlineCharacterSet] characterIsMember:[string characterAtIndex:currentStringIndex+2]]) break;
+ if ((unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), currentStringIndex+1) != '-') break;
+ if (![[NSCharacterSet whitespaceCharacterSet] characterIsMember:(unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), currentStringIndex+2)]) break;
currentStringIndex = [self endIndexOfCommentOfType:SPDoubleDashComment startingAtIndex:currentStringIndex];
break;
@@ -461,7 +463,7 @@
// For comments starting "/*", ensure the start syntax is valid before proceeding.
case '/':
if (stringLength < currentStringIndex + 1) break;
- if ([string characterAtIndex:currentStringIndex+1] != '*') break;
+ if ((unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), currentStringIndex+1) != '*') break;
currentStringIndex = [self endIndexOfCommentOfType:SPCStyleComment startingAtIndex:currentStringIndex];
break;
}
@@ -480,17 +482,20 @@
BOOL characterIsEscaped;
unichar currentCharacter;
+ // Cache the charAtIndex selector, avoiding dynamic binding overhead
+ IMP charAtIndex = [self methodForSelector:@selector(charAtIndex:)];
+
stringLength = [string length];
// Walk the string looking for the string end
for ( currentStringIndex = index; currentStringIndex < stringLength; currentStringIndex++) {
- currentCharacter = [string characterAtIndex:currentStringIndex];
+ currentCharacter = (unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), currentStringIndex);
// If the string end is a backtick and one has been encountered, treat it as end of string
if (quoteCharacter == '`' && currentCharacter == '`') {
// ...as long as the next character isn't also a backtick, in which case it's being quoted. Skip both.
- if ((currentStringIndex + 1) < stringLength && [string characterAtIndex:currentStringIndex+1] == '`') {
+ if ((currentStringIndex + 1) < stringLength && (unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), currentStringIndex+1) == '`') {
currentStringIndex++;
continue;
}
@@ -504,7 +509,7 @@
characterIsEscaped = NO;
i = 1;
quotedStringLength = currentStringIndex - 1;
- while ((quotedStringLength - i) > 0 && [string characterAtIndex:currentStringIndex - i] == '\\') {
+ while ((quotedStringLength - i) > 0 && (unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), currentStringIndex - i) == '\\') {
characterIsEscaped = !characterIsEscaped;
i++;
}
@@ -512,7 +517,7 @@
// If an even number have been found, it may be the end of the string - as long as the subsequent character
// isn't also the same character, in which case it's another form of escaping.
if (!characterIsEscaped) {
- if ((currentStringIndex + 1) < stringLength && [string characterAtIndex:currentStringIndex+1] == quoteCharacter) {
+ if ((currentStringIndex + 1) < stringLength && (unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), currentStringIndex+1) == quoteCharacter) {
currentStringIndex++;
continue;
}
@@ -534,6 +539,9 @@
long stringLength = [string length];
unichar currentCharacter;
+ // Cache the charAtIndex selector, avoiding dynamic binding overhead
+ IMP charAtIndex = [self methodForSelector:@selector(charAtIndex:)];
+
switch (commentType) {
// For comments of type "--[\s]", start the comment processing two characters in to match the start syntax,
@@ -545,7 +553,7 @@
case SPHashComment:
index++;
for ( ; index < stringLength; index++ ) {
- currentCharacter = [string characterAtIndex:index];
+ currentCharacter = (unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), index);
if (currentCharacter == '\r' || currentCharacter == '\n') {
return index-1;
}
@@ -557,8 +565,8 @@
case SPCStyleComment:
index = index+2;
for ( ; index < stringLength; index++ ) {
- if ([string characterAtIndex:index] == '*') {
- if ((stringLength > index + 1) && [string characterAtIndex:index+1] == '/') {
+ if ((unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), index) == '*') {
+ if ((stringLength > index + 1) && (unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), index+1) == '/') {
return (index+1);
}
}
@@ -569,6 +577,52 @@
return (stringLength-1);
}
+/*
+ * Provide a method to retrieve a character from the local cache.
+ * Does no bounds checking on the underlying string, and so is kept
+ * separate for characterAtIndex:.
+ */
+- (unichar) charAtIndex:(long)index
+{
+
+ // If the current cache doesn't include the current character, update it.
+ if (index > charCacheEnd || index < charCacheStart) {
+ if (charCacheEnd > -1) {
+ free(stringCharCache);
+ }
+ unsigned int remainingStringLength = [string length] - index;
+ unsigned int newcachelength = (CHARACTER_CACHE_LENGTH < remainingStringLength)?CHARACTER_CACHE_LENGTH:remainingStringLength;
+ stringCharCache = (unichar *)calloc(newcachelength, sizeof(unichar));
+ [string getCharacters:stringCharCache range:NSMakeRange(index, newcachelength)];
+ charCacheEnd = index + newcachelength - 1;
+ charCacheStart = index;
+ }
+ return stringCharCache[index - charCacheStart];
+}
+
+/*
+ * Provide a method to cleat the cache, and use it when updating the string.
+ */
+- (void) clearCharCache
+{
+ if (charCacheEnd > -1) {
+ free(stringCharCache);
+ }
+ charCacheEnd = -1;
+ charCacheStart = 0;
+}
+- (void) deleteCharactersInRange:(NSRange)aRange
+{
+ [super deleteCharactersInRange:aRange];
+ [self clearCharCache];
+}
+- (void) insertString:(NSString *)aString atIndex:(NSUInteger)anIndex
+{
+ [super insertString:aString atIndex:anIndex];
+ [self clearCharCache];
+}
+
+
/* Required and primitive methods to allow subclassing class cluster */
#pragma mark -
@@ -576,45 +630,53 @@
if (self = [super init]) {
string = [[NSMutableString string] retain];
}
+ charCacheEnd = -1;
return self;
}
- (id) initWithBytes:(const void *)bytes length:(unsigned int)length encoding:(NSStringEncoding)encoding {
if (self = [super init]) {
string = [[NSMutableString alloc] initWithBytes:bytes length:length encoding:encoding];
}
+ charCacheEnd = -1;
return self;
}
- (id) initWithBytesNoCopy:(void *)bytes length:(unsigned int)length encoding:(NSStringEncoding)encoding freeWhenDone:(BOOL)flag {
if (self = [super init]) {
string = [[NSMutableString alloc] initWithBytesNoCopy:bytes length:length encoding:encoding freeWhenDone:flag];
}
+ charCacheEnd = -1;
return self;
}
- (id) initWithCapacity:(unsigned int)capacity {
if (self = [super init]) {
string = [[NSMutableString stringWithCapacity:capacity] retain];
}
+ charCacheEnd = -1;
return self;
}
- (id) initWithCharactersNoCopy:(unichar *)characters length:(unsigned int)length freeWhenDone:(BOOL)flag {
if (self = [super init]) {
string = [[NSMutableString alloc] initWithCharactersNoCopy:characters length:length freeWhenDone:flag];
}
+ charCacheEnd = -1;
return self;
}
- (id) initWithContentsOfFile:(id)path {
+ charCacheEnd = -1;
return [self initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL];
}
- (id) initWithContentsOfFile:(NSString *)path encoding:(NSStringEncoding)encoding error:(NSError **)error {
if (self = [super init]) {
string = [[NSMutableString alloc] initWithContentsOfFile:path encoding:encoding error:error];
}
+ charCacheEnd = -1;
return self;
}
- (id) initWithCString:(const char *)nullTerminatedCString encoding:(NSStringEncoding)encoding {
if (self = [super init]) {
string = [[NSMutableString alloc] initWithCString:nullTerminatedCString encoding:encoding];
}
+ charCacheEnd = -1;
return self;
}
- (id) initWithFormat:(NSString *)format, ... {
@@ -622,12 +684,14 @@
va_start(argList, format);
id str = [self initWithFormat:format arguments:argList];
va_end(argList);
+ charCacheEnd = -1;
return str;
}
- (id) initWithFormat:(NSString *)format arguments:(va_list)argList {
if (self = [super init]) {
string = [[NSMutableString alloc] initWithFormat:format arguments:argList];
}
+ charCacheEnd = -1;
return self;
}
- (unsigned int) length {
@@ -641,15 +705,19 @@
}
- (unsigned int) replaceOccurrencesOfString:(NSString *)target withString:(NSString *)replacement options:(unsigned)options range:(NSRange)searchRange {
return [string replaceOccurrencesOfString:target withString:replacement options:options range:searchRange];
+ [self clearCharCache];
}
- (void) setString:(NSString *)aString {
[string setString:aString];
+ [self clearCharCache];
}
- (void) replaceCharactersInRange:(NSRange)range withString:(NSString *)aString {
[string replaceCharactersInRange:range withString:aString];
+ [self clearCharCache];
}
- (void) dealloc {
[string release];
+ if (charCacheEnd != -1) free(stringCharCache);
[super dealloc];
}
@end \ No newline at end of file
diff --git a/Source/SPStringAdditions.h b/Source/SPStringAdditions.h
index 4acd748c..0a323cf3 100644
--- a/Source/SPStringAdditions.h
+++ b/Source/SPStringAdditions.h
@@ -25,6 +25,7 @@
@interface NSString (SPStringAdditions)
+ (NSString *)stringForByteSize:(int)byteSize;
++ (NSString *)stringForTimeInterval:(float)timeInterval;
#if MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_5
- (NSArray *)componentsSeparatedByCharactersInSet:(NSCharacterSet *)set;
diff --git a/Source/SPStringAdditions.m b/Source/SPStringAdditions.m
index 2916611d..ad6972ce 100644
--- a/Source/SPStringAdditions.m
+++ b/Source/SPStringAdditions.m
@@ -66,6 +66,51 @@
return [numberFormatter stringFromNumber:[NSNumber numberWithFloat:size]];
}
+
+// -------------------------------------------------------------------------------
+// stringForTimeInterval:
+//
+// Returns a human readable version string of the supplied time interval.
+// -------------------------------------------------------------------------------
++ (NSString *)stringForTimeInterval:(float)timeInterval
+{
+ NSNumberFormatter *numberFormatter = [[[NSNumberFormatter alloc] init] autorelease];
+
+ [numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
+
+ if (timeInterval < 1) {
+ timeInterval = (timeInterval * 1000);
+ [numberFormatter setFormat:@"#,##0 ms"];
+
+ return [numberFormatter stringFromNumber:[NSNumber numberWithFloat:timeInterval]];
+ }
+
+ if (timeInterval < 100) {
+ [numberFormatter setFormat:@"#,##0.0 s"];
+
+ return [numberFormatter stringFromNumber:[NSNumber numberWithFloat:timeInterval]];
+ }
+
+ if (timeInterval < 300) {
+ [numberFormatter setFormat:@"#,##0 s"];
+
+ return [numberFormatter stringFromNumber:[NSNumber numberWithFloat:timeInterval]];
+ }
+
+ if (timeInterval < 3600) {
+ timeInterval = (timeInterval / 60);
+ [numberFormatter setFormat:@"#,##0 min"];
+
+ return [numberFormatter stringFromNumber:[NSNumber numberWithFloat:timeInterval]];
+ }
+
+ timeInterval = (timeInterval / 3600);
+ [numberFormatter setFormat:@"#,##0 hours"];
+
+ return [numberFormatter stringFromNumber:[NSNumber numberWithFloat:timeInterval]];
+}
+
+
#if MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_5
// -------------------------------------------------------------------------------
// componentsSeparatedByCharactersInSet:
diff --git a/Source/SPTableData.m b/Source/SPTableData.m
index 905a8ec5..1391e355 100644
--- a/Source/SPTableData.m
+++ b/Source/SPTableData.m
@@ -508,12 +508,18 @@
NSMutableDictionary *fieldDetails = [[NSMutableDictionary alloc] init];
NSMutableArray *detailParts;
NSString *detailString;
- int i, partsArrayLength;
+ int i, definitionPartsIndex = 0, partsArrayLength;
if (![definitionParts count]) return [NSDictionary dictionary];
+ // Skip blank items within the definition parts
+ while (definitionPartsIndex < [definitionParts count]
+ && ![[[definitionParts objectAtIndex:definitionPartsIndex] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] length])
+ definitionPartsIndex++;
+
// The first item is always the data type.
- [fieldParser setString:[definitionParts objectAtIndex:0]];
+ [fieldParser setString:[definitionParts objectAtIndex:definitionPartsIndex]];
+ definitionPartsIndex++;
// If no field length definition is present, store only the type
if ([fieldParser firstOccurrenceOfCharacter:'(' ignoringQuotedStrings:YES] == NSNotFound) {
@@ -595,8 +601,8 @@
// Walk through the remaining column definition parts storing recognised details
partsArrayLength = [definitionParts count];
- for (i = 1; i < partsArrayLength; i++) {
- detailString = [[NSString alloc] initWithString:[[definitionParts objectAtIndex:i] uppercaseString]];
+ for ( ; definitionPartsIndex < partsArrayLength; definitionPartsIndex++) {
+ detailString = [[NSString alloc] initWithString:[[definitionParts objectAtIndex:definitionPartsIndex] uppercaseString]];
// Whether numeric fields are unsigned
if ([detailString isEqualToString:@"UNSIGNED"]) {
@@ -611,30 +617,30 @@
[fieldDetails setValue:[NSNumber numberWithBool:YES] forKey:@"binary"];
// Whether text types have a different encoding to the table
- } else if ([detailString isEqualToString:@"CHARSET"] && (i + 1 < partsArrayLength)) {
- if (![[[definitionParts objectAtIndex:i+1] uppercaseString] isEqualToString:@"DEFAULT"]) {
- [fieldDetails setValue:[definitionParts objectAtIndex:i+1] forKey:@"encoding"];
+ } else if ([detailString isEqualToString:@"CHARSET"] && (definitionPartsIndex + 1 < partsArrayLength)) {
+ if (![[[definitionParts objectAtIndex:definitionPartsIndex+1] uppercaseString] isEqualToString:@"DEFAULT"]) {
+ [fieldDetails setValue:[definitionParts objectAtIndex:definitionPartsIndex+1] forKey:@"encoding"];
}
- i++;
- } else if ([detailString isEqualToString:@"CHARACTER"] && (i + 2 < partsArrayLength)
- && [[[definitionParts objectAtIndex:i+1] uppercaseString] isEqualToString:@"SET"]) {
- if (![[[definitionParts objectAtIndex:i+2] uppercaseString] isEqualToString:@"DEFAULT"]) {;
- [fieldDetails setValue:[definitionParts objectAtIndex:i+2] forKey:@"encoding"];
+ definitionPartsIndex++;
+ } else if ([detailString isEqualToString:@"CHARACTER"] && (definitionPartsIndex + 2 < partsArrayLength)
+ && [[[definitionParts objectAtIndex:definitionPartsIndex+1] uppercaseString] isEqualToString:@"SET"]) {
+ if (![[[definitionParts objectAtIndex:definitionPartsIndex+2] uppercaseString] isEqualToString:@"DEFAULT"]) {;
+ [fieldDetails setValue:[definitionParts objectAtIndex:definitionPartsIndex+2] forKey:@"encoding"];
}
- i = i + 2;
+ definitionPartsIndex += 2;
// Whether text types have a different collation to the table
- } else if ([detailString isEqualToString:@"COLLATE"] && (i + 1 < partsArrayLength)) {
- if (![[[definitionParts objectAtIndex:i+1] uppercaseString] isEqualToString:@"DEFAULT"]) {
- [fieldDetails setValue:[definitionParts objectAtIndex:i+1] forKey:@"collation"];
+ } else if ([detailString isEqualToString:@"COLLATE"] && (definitionPartsIndex + 1 < partsArrayLength)) {
+ if (![[[definitionParts objectAtIndex:definitionPartsIndex+1] uppercaseString] isEqualToString:@"DEFAULT"]) {
+ [fieldDetails setValue:[definitionParts objectAtIndex:definitionPartsIndex+1] forKey:@"collation"];
}
- i++;
+ definitionPartsIndex++;
// Whether fields are NOT NULL
- } else if ([detailString isEqualToString:@"NOT"] && (i + 1 < partsArrayLength)
- && [[[definitionParts objectAtIndex:i+1] uppercaseString] isEqualToString:@"NULL"]) {
+ } else if ([detailString isEqualToString:@"NOT"] && (definitionPartsIndex + 1 < partsArrayLength)
+ && [[[definitionParts objectAtIndex:definitionPartsIndex+1] uppercaseString] isEqualToString:@"NULL"]) {
[fieldDetails setValue:[NSNumber numberWithBool:NO] forKey:@"null"];
- i++;
+ definitionPartsIndex++;
// Whether fields are NULL
} else if ([detailString isEqualToString:@"NULL"]) {
@@ -645,11 +651,11 @@
[fieldDetails setValue:[NSNumber numberWithBool:YES] forKey:@"autoincrement"];
// Field defaults
- } else if ([detailString isEqualToString:@"DEFAULT"] && (i + 1 < partsArrayLength)) {
- detailParser = [[SPSQLParser alloc] initWithString:[definitionParts objectAtIndex:i+1]];
+ } else if ([detailString isEqualToString:@"DEFAULT"] && (definitionPartsIndex + 1 < partsArrayLength)) {
+ detailParser = [[SPSQLParser alloc] initWithString:[definitionParts objectAtIndex:definitionPartsIndex+1]];
[fieldDetails setValue:[detailParser unquotedString] forKey:@"default"];
[detailParser release];
- i++;
+ definitionPartsIndex++;
}
// TODO: Currently unhandled: [UNIQUE | PRIMARY] KEY | COMMENT 'foo' | COLUMN_FORMAT bar | STORAGE q | REFERENCES...
diff --git a/Source/TableDump.m b/Source/TableDump.m
index fb0504a8..a59aad73 100644
--- a/Source/TableDump.m
+++ b/Source/TableDump.m
@@ -428,6 +428,11 @@
for ( i = 0 ; i < [queries count] ; i++ ) {
[singleProgressBar setDoubleValue:((i+1)*100/[queries count])];
[singleProgressBar displayIfNeeded];
+
+ // Skip blank or whitespace-only queries to avoid errors
+ if ([[[queries objectAtIndex:i] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] length] == 0)
+ continue;
+
[mySQLConnection queryString:[queries objectAtIndex:i]];
if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""] && ![[mySQLConnection getLastErrorMessage] isEqualToString:@"Query was empty"]) {