diff options
author | rowanbeentje <rowan@beent.je> | 2009-03-19 23:44:06 +0000 |
---|---|---|
committer | rowanbeentje <rowan@beent.je> | 2009-03-19 23:44:06 +0000 |
commit | ddf7d62d20614111acdd420075ef762d6deaa8d7 (patch) | |
tree | cf5ce70dde9c25d1b21da1cded5cee9f3e562704 /Source/CustomQuery.m | |
parent | 4a8d0f731bbb03b9ace041d421800e5c6470326a (diff) | |
download | sequelpro-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/CustomQuery.m')
-rw-r--r-- | Source/CustomQuery.m | 505 |
1 files changed, 354 insertions, 151 deletions
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; { |