diff options
Diffstat (limited to 'Source')
-rw-r--r-- | Source/CMMCPConnection.h | 2 | ||||
-rw-r--r-- | Source/CMMCPConnection.m | 22 | ||||
-rw-r--r-- | Source/CustomQuery.h | 9 | ||||
-rw-r--r-- | Source/CustomQuery.m | 505 | ||||
-rw-r--r-- | Source/SPSQLParser.h | 18 | ||||
-rw-r--r-- | Source/SPSQLParser.m | 116 | ||||
-rw-r--r-- | Source/SPStringAdditions.h | 1 | ||||
-rw-r--r-- | Source/SPStringAdditions.m | 45 | ||||
-rw-r--r-- | Source/SPTableData.m | 52 | ||||
-rw-r--r-- | Source/TableDump.m | 5 |
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"]) { |