aboutsummaryrefslogtreecommitdiffstats
path: root/Source/CustomQuery.m
diff options
context:
space:
mode:
Diffstat (limited to 'Source/CustomQuery.m')
-rw-r--r--Source/CustomQuery.m505
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;
{