From ddf7d62d20614111acdd420075ef762d6deaa8d7 Mon Sep 17 00:00:00 2001 From: rowanbeentje Date: Thu, 19 Mar 2009 23:44:06 +0000 Subject: 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. --- Interfaces/English.lproj/DBView.xib | 212 ++++++++++++--- Source/CMMCPConnection.h | 2 + Source/CMMCPConnection.m | 22 +- Source/CustomQuery.h | 9 +- Source/CustomQuery.m | 505 +++++++++++++++++++++++++----------- Source/SPSQLParser.h | 18 ++ Source/SPSQLParser.m | 116 +++++++-- Source/SPStringAdditions.h | 1 + Source/SPStringAdditions.m | 45 ++++ Source/SPTableData.m | 52 ++-- Source/TableDump.m | 5 + 11 files changed, 748 insertions(+), 239 deletions(-) diff --git a/Interfaces/English.lproj/DBView.xib b/Interfaces/English.lproj/DBView.xib index 74755ef7..f30440be 100644 --- a/Interfaces/English.lproj/DBView.xib +++ b/Interfaces/English.lproj/DBView.xib @@ -44,7 +44,7 @@ {3.40282e+38, 3.40282e+38} {780, 480} - + 256 YES @@ -78,6 +78,7 @@ 4352 {194, 393} + YES @@ -174,6 +175,8 @@ {{1, 1}, {194, 393}} + + 6 @@ -188,6 +191,7 @@ -2147483392 {{175, 1}, {15, 481}} + _doScroller: 9.979253e-01 @@ -197,6 +201,7 @@ 256 {{-100, -100}, {141, 11}} + 257 _doScroller: @@ -205,6 +210,8 @@ {196, 395} + + 530 @@ -226,6 +233,7 @@ 4352 {194, 123} + YES @@ -289,6 +297,8 @@ {{1, 1}, {194, 123}} + + 4 @@ -298,6 +308,7 @@ -2147483392 {{175, 1}, {15, 481}} + _doScroller: 9.979253e-01 @@ -307,6 +318,7 @@ 256 {{-100, -100}, {141, 11}} + 257 _doScroller: @@ -315,6 +327,8 @@ {{0, 404}, {196, 125}} + + 530 @@ -324,12 +338,14 @@ {{0, 22}, {196, 529}} + 292 {{0, -1}, {32, 25}} + YES -2080244224 @@ -358,6 +374,7 @@ 292 {{20, 0}, {46, 25}} + YES -2076049856 @@ -366,7 +383,7 @@ -2042609409 35 - + NSImage button_action @@ -382,7 +399,10 @@ 1048576 2147483647 1 - + + NSImage + button_action + _popUpItemAction: @@ -468,6 +488,7 @@ {{179, 0}, {15, 23}} + YES 130560 @@ -500,6 +521,7 @@ {{60, 0}, {119, 23}} + YES 130560 @@ -518,6 +540,7 @@ {194, 550} + NSView @@ -530,6 +553,7 @@ 274 {{-7, -10}, {672, 564}} + YES @@ -566,6 +590,7 @@ {{608, 6}, {10, 13}} + YES 130560 @@ -586,6 +611,7 @@ 257 {{400, 8}, {55, 11}} + YES 67239424 @@ -606,6 +632,7 @@ 257 {{456, 6}, {135, 15}} + YES -1539178944 @@ -714,12 +741,14 @@ 4352 {625, 282} + YES 256 {625, 17} + @@ -727,6 +756,7 @@ -2147483392 {{-26, 0}, {16, 17}} + YES @@ -1259,6 +1289,8 @@ {{1, 17}, {625, 282}} + + 4 @@ -1268,6 +1300,7 @@ -2147483392 {{636, 17}, {15, 265}} + _doScroller: 8.170732e-01 @@ -1277,6 +1310,7 @@ -2147483392 {{1, 282}, {635, 15}} + 1 _doScroller: @@ -1291,6 +1325,8 @@ {{1, 0}, {625, 17}} + + 4 @@ -1299,6 +1335,8 @@ {{-1, 24}, {627, 300}} + + 562 @@ -1312,6 +1350,7 @@ 290 {{107, 0}, {519, 26}} + YES -2080244224 @@ -1332,6 +1371,7 @@ 260 {{-1, 0}, {28, 26}} + YES 604110336 @@ -1356,6 +1396,7 @@ 260 {{26, 0}, {28, 26}} + YES 604110336 @@ -1380,6 +1421,7 @@ 260 {{53, 0}, {28, 26}} + YES 604110336 @@ -1404,6 +1446,7 @@ 260 {{80, 0}, {28, 26}} + YES 67239424 @@ -1426,6 +1469,7 @@ {626, 324} + NSView @@ -1438,6 +1482,7 @@ 264 {{7, 184}, {46, 14}} + YES 67239424 @@ -1464,12 +1509,14 @@ 4352 {625, 138} + YES 256 {625, 17} + @@ -1477,6 +1524,7 @@ -2147483392 {{-26, 0}, {16, 17}} + YES @@ -1729,6 +1777,8 @@ {{1, 17}, {625, 138}} + + 4 @@ -1738,6 +1788,7 @@ -2147483392 {{84, 17}, {15, 67}} + _doScroller: 8.170732e-01 @@ -1747,6 +1798,7 @@ -2147483392 {{1, 123}, {612, 15}} + 1 _doScroller: @@ -1761,6 +1813,8 @@ {{1, 0}, {625, 17}} + + 4 @@ -1769,6 +1823,8 @@ {{-1, 22}, {627, 156}} + + 562 @@ -1782,6 +1838,7 @@ 258 {{80, -2}, {546, 26}} + YES -2080244224 @@ -1802,6 +1859,7 @@ 260 {{-1, -2}, {28, 26}} + YES 604110336 @@ -1826,6 +1884,7 @@ 260 {{26, -2}, {28, 26}} + YES 604110336 @@ -1850,6 +1909,7 @@ 260 {{53, -2}, {28, 26}} + YES 67239424 @@ -1869,15 +1929,18 @@ {{0, 333}, {626, 198}} + NSView {{7, 10}, {626, 531}} + {{10, 7}, {637, 544}} + Structure @@ -2532,7 +2595,7 @@ 6418 - {626, 14} + {625, 14} @@ -2550,7 +2613,7 @@ - 6.260000e+02 + 6.250000e+02 1 @@ -2590,7 +2653,7 @@ - {{1, 1}, {626, 155}} + {{1, 1}, {625, 155}} @@ -2622,7 +2685,7 @@ 9.456522e-01 - {628, 157} + {627, 157} 530 @@ -2631,7 +2694,7 @@ - {628, 156} + {627, 156} NSView @@ -2653,13 +2716,13 @@ 4352 - {626, 226} + {625, 226} YES 256 - {626, 17} + {625, 17} @@ -2672,7 +2735,7 @@ YES - 6.230000e+02 + 6.220000e+02 4.000000e+01 1.000000e+03 @@ -2709,7 +2772,7 @@ YES - {{1, 17}, {626, 226}} + {{1, 17}, {625, 226}} @@ -2742,7 +2805,7 @@ YES - {{1, 0}, {626, 17}} + {{1, 0}, {625, 17}} @@ -2751,7 +2814,7 @@ - {628, 244} + {627, 244} 562 @@ -2765,13 +2828,13 @@ 265 - {{523, 245}, {90, 28}} + {{522, 245}, {90, 28}} YES -2080244224 134348800 - Run Query + Run All -2034876161 @@ -2785,8 +2848,8 @@ - 266 - {{264, 248}, {259, 22}} + 268 + {{214, 248}, {186, 22}} YES @@ -2835,7 +2898,7 @@ 264 - {{17, 248}, {245, 22}} + {{17, 248}, {195, 22}} YES @@ -2916,8 +2979,28 @@ 1 + + + 265 + {{414, 245}, {110, 28}} + + YES + + 604110336 + 134348800 + Run Current + + + -2038284033 + 129 + + + 200 + 25 + + - {{0, 165}, {628, 269}} + {{0, 165}, {627, 269}} NSView @@ -2929,7 +3012,7 @@ 266 - {{242, 67}, {369, 14}} + {{242, 67}, {368, 14}} YES @@ -2965,7 +3048,7 @@ 274 - {{17, 20}, {594, 43}} + {{17, 20}, {593, 43}} YES @@ -2979,16 +3062,16 @@ - {{0, 443}, {628, 87}} + {{0, 443}, {627, 87}} NSView - {{6, 10}, {628, 530}} + {{6, 10}, {627, 530}} - {{10, 7}, {638, 544}} + {{10, 7}, {637, 544}} Custom Query @@ -3374,16 +3457,20 @@ {{203, 0}, {660, 550}} + NSView {863, 550} + YES DBViewSplitter {863, 550} + + {{0, 0}, {1440, 878}} {780, 502} @@ -9613,14 +9700,6 @@ IGRvIHlvdSB3YW50IHRvIGFkZCBmb3IgdGhpcyBmaWVsZD8 214 - - - performQuery: - - - - 215 - textView @@ -11729,6 +11808,38 @@ IGRvIHlvdSB3YW50IHRvIGFkZCBmb3IgdGhpcyBmaWVsZD8 4822 + + + runSelectedQueries: + + + + 5125 + + + + runAllButton + + + + 5126 + + + + runSelectionButton + + + + 5127 + + + + runAllQueries: + + + + 5128 + @@ -16141,8 +16252,9 @@ IGRvIHlvdSB3YW50IHRvIGFkZCBmb3IgdGhpcyBmaWVsZD8 YES - + + @@ -16608,6 +16720,20 @@ IGRvIHlvdSB3YW50IHRvIGFkZCBmb3IgdGhpcyBmaWVsZD8 + + 5123 + + + YES + + + + + + 5124 + + + @@ -17607,6 +17733,8 @@ IGRvIHlvdSB3YW50IHRvIGFkZCBmb3IgdGhpcyBmaWVsZD8 51.ImportedFromIB2 512.IBPluginDependency 512.ImportedFromIB2 + 5123.IBPluginDependency + 5124.IBPluginDependency 513.IBPluginDependency 513.ImportedFromIB2 514.IBPluginDependency @@ -19026,8 +19154,8 @@ IGRvIHlvdSB3YW50IHRvIGFkZCBmb3IgdGhpcyBmaWVsZD8 com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin - {{318, 360}, {863, 550}} - {{318, 360}, {863, 550}} + {{55, 306}, {863, 550}} + {{55, 306}, {863, 550}} {{62, 352}, {845, 504}} @@ -19051,6 +19179,8 @@ IGRvIHlvdSB3YW50IHRvIGFkZCBmb3IgdGhpcyBmaWVsZD8 com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin @@ -19401,7 +19531,7 @@ Y2hhbmdlIHRoZSBvcmRlcg - 5122 + 5128 @@ -19475,8 +19605,9 @@ Y2hhbmdlIHRoZSBvcmRlcg closeQueryFavoritesSheet: closeSheet: copyQueryFavorite: - performQuery: removeQueryFavorite: + runAllQueries: + runSelectedQueries: YES @@ -19488,6 +19619,7 @@ Y2hhbmdlIHRoZSBvcmRlcg id id id + id @@ -19503,6 +19635,8 @@ Y2hhbmdlIHRoZSBvcmRlcg queryFavoritesView queryHistoryButton removeQueryFavoriteButton + runAllButton + runSelectionButton tableWindow textView valueSheet @@ -19523,6 +19657,8 @@ Y2hhbmdlIHRoZSBvcmRlcg id id id + id + id 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; @@ -384,6 +392,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; @@ -731,6 +868,68 @@ traps enter key and return NO; } +/* + * 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. */ @@ -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 +/* + * 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"]) { -- cgit v1.2.3