From fa3a82bd89eee142c575e13e47eb6a7bd5e1fc89 Mon Sep 17 00:00:00 2001 From: Bibiko Date: Thu, 4 Jun 2009 14:11:55 +0000 Subject: =?UTF-8?q?=E2=80=A2=20added=20support=20for=20the=20=E2=80=9Cdeli?= =?UTF-8?q?miter=E2=80=9D=20command=20-=20added=20(NSArray=20*)=20splitSql?= =?UTF-8?q?StringByCharacter:(unichar)character;=20(NSArray=20*)=20splitSq?= =?UTF-8?q?lStringIntoRangesByCharacter:(unichar)character;=20(long)=20fir?= =?UTF-8?q?stOccurrenceInSqlOfCharacter:=20to=20the=20SQLParser=20which=20?= =?UTF-8?q?recognize=20a=20=E2=80=9Cdelimiter=E2=80=9D=20command=20?= =?UTF-8?q?=E2=80=A2=20queryAtPosition=20now=20works=20with=20ranges=20to?= =?UTF-8?q?=20speed=20it=20up=20-=20the=20current=20query=20ranges=20resp.?= =?UTF-8?q?=20the=20just=20activated=20query=20range=20are=20cached=20in?= =?UTF-8?q?=20order=20to=20avoid=20parsing=20if=20the=20user=20only=20navi?= =?UTF-8?q?gates=20through=20the=20textView=20buffer,=20or=20if=20the=20us?= =?UTF-8?q?er=20calls=20Run=20Prev/Current=20Query=20only=20=E2=80=A2=20th?= =?UTF-8?q?e=20"import=20dump"=20function=20makes=20usage=20of=20that=20ne?= =?UTF-8?q?w=20=E2=80=9Cdelimiter=E2=80=9D=20support=20-=20i.e.=20dumps=20?= =?UTF-8?q?with=20procs/funcs=20declaration=20could=20be=20imported?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IMPORTANT: Please check the new SQLParser exhaustively in order to prove that new approach --- Source/CustomQuery.h | 2 + Source/CustomQuery.m | 206 ++++++++++++++++------------ Source/SPSQLParser.h | 27 ++-- Source/SPSQLParser.m | 380 ++++++++++++++++++++++++++++++++++++--------------- Source/TableDump.m | 4 +- 5 files changed, 399 insertions(+), 220 deletions(-) diff --git a/Source/CustomQuery.h b/Source/CustomQuery.h index 39b405f5..9c4e1a1e 100644 --- a/Source/CustomQuery.h +++ b/Source/CustomQuery.h @@ -78,6 +78,8 @@ CMMCPConnection *mySQLConnection; NSString *usedQuery; + NSRange currentQueryRange; + NSArray *currentQueryRanges; NSString *mySQLversion; int queryStartPosition; diff --git a/Source/CustomQuery.m b/Source/CustomQuery.m index bbcd6f2f..8e7d3325 100644 --- a/Source/CustomQuery.m +++ b/Source/CustomQuery.m @@ -59,7 +59,7 @@ // Retrieve the custom query string and split it into separate SQL queries queryParser = [[SPSQLParser alloc] initWithString:[textView string]]; - queries = [queryParser parseQueries]; + queries = [queryParser splitSqlStringByCharacter:';']; [queryParser release]; NSRange curRange = [textView selectedRange]; @@ -97,8 +97,10 @@ // If the current selection is a single caret position, run the current query. if (selectedRange.length == 0) { - BOOL doLookBehind = YES; - query = [self queryAtPosition:selectedRange.location lookBehind:&doLookBehind]; + // BOOL doLookBehind = YES; + // query = [self queryAtPosition:selectedRange.location lookBehind:&doLookBehind]; + if(currentQueryRange.length) + query = [[textView string] substringWithRange:currentQueryRange]; if (!query) { NSBeep(); return; @@ -108,7 +110,7 @@ // Otherwise, run the selected text. } else { queryParser = [[SPSQLParser alloc] initWithString:[[textView string] substringWithRange:selectedRange]]; - queries = [queryParser parseQueries]; + queries = [queryParser splitSqlStringByCharacter:';']; [queryParser release]; } @@ -388,6 +390,8 @@ float executionTime = 0; int firstErrorOccuredInQuery = -1; BOOL suppressErrorSheet = NO; + // NSString *delimiterMatch = @"^\\s*delimiter\\s*$|^\\s*delimiter\\s+\\S+\\s*$"; + NSCharacterSet *whitespaceAndNewlineSet = [NSCharacterSet whitespaceAndNewlineCharacterSet]; // Notify listeners that a query has started [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self]; @@ -403,11 +407,13 @@ [customQueryView removeTableColumn:[theColumns objectAtIndex:0]]; } + BOOL queriesSeparatedByDelimiter = NO; + // 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) + if ([[[queries objectAtIndex:i] stringByTrimmingCharactersInSet:whitespaceAndNewlineSet] length] == 0) continue; // Run the query, timing execution (note this also includes network and overhead) @@ -467,7 +473,10 @@ if(usedQuery) [usedQuery release]; - usedQuery = [[NSString stringWithString:[queries componentsJoinedByString:@";\n"]] retain]; + if(!queriesSeparatedByDelimiter) + usedQuery = [[NSString stringWithString:[queries componentsJoinedByString:@";\n"]] retain]; + else // TODO how to combine the query array if “delimiter command” was used? + usedQuery = @""; //perform empty query if no query is given if ( [queries count] == 0 ) { @@ -489,15 +498,17 @@ } //add query to history - [queryHistoryButton insertItemWithTitle:[queries componentsJoinedByString:@"; "] atIndex:1]; - while ( [queryHistoryButton numberOfItems] > [[prefs objectForKey:@"CustomQueryMaxHistoryItems"] intValue] + 1 ) { - [queryHistoryButton removeItemAtIndex:[queryHistoryButton numberOfItems]-1]; - } - for ( i = 1 ; i < [queryHistoryButton numberOfItems] ; i++ ) - { - [menuItems addObject:[queryHistoryButton itemTitleAtIndex:i]]; + if(!queriesSeparatedByDelimiter) { // TODO only add to history if no “delimiter” command was used + [queryHistoryButton insertItemWithTitle:[queries componentsJoinedByString:@"; "] atIndex:1]; + while ( [queryHistoryButton numberOfItems] > [[prefs objectForKey:@"CustomQueryMaxHistoryItems"] intValue] + 1 ) { + [queryHistoryButton removeItemAtIndex:[queryHistoryButton numberOfItems]-1]; + } + for ( i = 1 ; i < [queryHistoryButton numberOfItems] ; i++ ) + { + [menuItems addObject:[queryHistoryButton itemTitleAtIndex:i]]; + } + [prefs setObject:menuItems forKey:@"queryHistory"]; } - [prefs setObject:menuItems forKey:@"queryHistory"]; // Error checking if ( [errors length] ) { @@ -745,7 +756,7 @@ return NSMakeRange(NSNotFound, 0); } - NSString * theQuery = [queries objectAtIndex:anIndex]; + NSString *theQuery = [queries objectAtIndex:anIndex]; // Calculate the text length before that query at index anIndex long prevQueriesLength = 0; @@ -772,38 +783,44 @@ - (NSString *)queryAtPosition:(long)position lookBehind:(BOOL *)doLookBehind { SPSQLParser *customQueryParser; - NSArray *queries; - NSString *query = nil; - NSRange queryRange; - int i, j, lastQueryStartPosition, queryPosition = 0; + NSArray *queries; + NSString *query = nil; + NSRange queryRange; + + long i, j, lastQueryStartPosition, queryPosition = 0; + long queryCount; + + NSCharacterSet *whitespaceAndNewlineSet = [NSCharacterSet whitespaceAndNewlineCharacterSet]; + NSCharacterSet *whitespaceSet = [NSCharacterSet whitespaceCharacterSet]; // 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:';']]; - - // ========= test case for SQL splitting into ranges == START - // NSArray *qs; int qc; - // qs = [[NSArray alloc] initWithArray:[customQueryParser splitStringIntoRangesOfSQLQueries]]; - // NSLog(@"---START---"); - // for(qc=0;qc<[qs count];qc++) { - // NSLog(@"%d:", qc); - // NSLog(@"%@", [[textView string] substringWithRange:NSRangeFromString([qs objectAtIndex:qc])]); - // } - // ========= test case for SQL splitting into ranges == END - [customQueryParser release]; + // Split the current text into queries and ranges + // only if the textView was really changed, otherwise use the cache + if([[textView textStorage] editedMask] != 0) { + customQueryParser = [[SPSQLParser alloc] initWithString:[textView string]]; + queries = [[NSArray alloc] initWithArray:[customQueryParser splitSqlStringIntoRangesByCharacter:';']]; + if(currentQueryRanges) + [currentQueryRanges release]; + currentQueryRanges = [[NSArray arrayWithArray:queries] retain]; + [customQueryParser release]; + } else { + queries = [[NSArray alloc] initWithArray:currentQueryRanges]; + } - NSCharacterSet *newlineSet = [NSCharacterSet whitespaceAndNewlineCharacterSet]; + queryCount = [queries count]; // 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++ ) { + for (i = 0; i < queryCount; i++ ) { + lastQueryStartPosition = queryStartPosition; - queryStartPosition = queryPosition; - queryPosition += [[queries objectAtIndex:i] length]; + queryRange = [[queries objectAtIndex:i] rangeValue]; + queryStartPosition = queryRange.location; + queryPosition = NSMaxRange(queryRange); + if (queryPosition >= position) { // If lookbehind is enabled, check whether the current position could be considered to @@ -815,70 +832,83 @@ // If the caret is at the very start of the string, always associate if (position == queryStartPosition) positionAssociatedWithPreviousQuery = YES; - + + // If the caret is in between a user-defined delimiter whose length is >1, always associate + if (!positionAssociatedWithPreviousQuery && i && NSMaxRange([[queries objectAtIndex:i-1] rangeValue]) < position && position < queryStartPosition) positionAssociatedWithPreviousQuery = YES; + // Otherwise associate if only whitespace since previous, and a newline before next. if (!positionAssociatedWithPreviousQuery) { + @try{ NSString *stringToPrevious = [[textView string] substringWithRange:NSMakeRange(queryStartPosition, position - queryStartPosition)]; NSString *stringToEnd = [[textView string] substringWithRange:NSMakeRange(position, queryPosition - position)]; - NSCharacterSet *whitespaceSet = [NSCharacterSet whitespaceCharacterSet]; - if (![[stringToPrevious stringByTrimmingCharactersInSet:newlineSet] length]) { + if (![[stringToPrevious stringByTrimmingCharactersInSet:whitespaceAndNewlineSet] length]) { for (j = 0; j < [stringToEnd length]; j++) { if ([whitespaceSet characterIsMember:[stringToEnd characterAtIndex:j]]) continue; - if ([newlineSet characterIsMember:[stringToEnd characterAtIndex:j]]) { + if ([whitespaceAndNewlineSet characterIsMember:[stringToEnd characterAtIndex:j]]) { positionAssociatedWithPreviousQuery = YES; } break; } } + } @catch(id ae) {} } // If there is a previous query and the position should be associated with it, do so. - if (i && positionAssociatedWithPreviousQuery && [[[queries objectAtIndex:i-1] stringByTrimmingCharactersInSet:newlineSet] length]) { - query = [NSString stringWithString:[queries objectAtIndex:i-1]]; + if (i && positionAssociatedWithPreviousQuery && [[[[textView string] substringWithRange:[[queries objectAtIndex:i-1] rangeValue]] stringByTrimmingCharactersInSet:whitespaceAndNewlineSet] length]) { queryStartPosition = lastQueryStartPosition; - queryRange=NSMakeRange(queryStartPosition, [[queries objectAtIndex:i-1] length]); + queryRange = [[queries objectAtIndex:i-1] rangeValue]; break; } // Lookbehind failed - set the pointer to NO so the parent knows. *doLookBehind = NO; } - - query = [NSString stringWithString:[queries objectAtIndex:i]]; - queryRange=NSMakeRange(queryStartPosition, [[queries objectAtIndex:i] length]); break; } - queryPosition++; } // For lookbehinds catch position at the very end of a string ending in a semicolon - if (*doLookBehind && position == [[textView string] length] && !query) + if (*doLookBehind && position == [[textView string] length]) { - query = [queries lastObject]; - queryRange=NSMakeRange(queryStartPosition, [[queries lastObject] length]); + queryRange = [[queries lastObject] rangeValue]; } [queries release]; + // Remove all background color attributes - [[textView textStorage] removeAttribute:NSBackgroundColorAttributeName range:NSMakeRange(0,[[textView string] length])]; + NSRange textRange = NSMakeRange(0,[[textView string] length]); + [[textView textStorage] removeAttribute:NSBackgroundColorAttributeName range:textRange]; - // 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) + queryRange = NSIntersectionRange(queryRange, textRange); + if (!queryRange.length) { + currentQueryRange = NSMakeRange(0,0); return nil; + } + + query = [[textView string] substringWithRange:queryRange]; // Highlight by setting a background color the current query - // and delete leading/trailing white spaces + // and ignore leading/trailing white spaces int biasStart = [query rangeOfRegex:@"^\\s*"].length; - int biasEnd = [query rangeOfRegex:@"\\s*$"].length; + int biasEnd = [query rangeOfRegex:@"\\s*$"].length; queryRange.location += biasStart; - queryRange.length -= biasEnd+biasStart; + queryRange.length -= biasEnd+biasStart; + + // Ensure the string isn't empty. + // (We could also strip comments for this check, but that prevents use of conditional comments) + if(queryRange.length < 1 || queryRange.length > [query length]) { + currentQueryRange = NSMakeRange(0,0); + return nil; + } + [[textView textStorage] addAttribute: NSBackgroundColorAttributeName value: [NSColor colorWithDeviceRed:0.95 green:0.95 blue:0.95 alpha:1] range: queryRange ]; - // Return the located string. + currentQueryRange = queryRange; + + // Return the located string return query; } @@ -934,6 +964,7 @@ mySQLConnection = theConnection; prefs = [NSUserDefaults standardUserDefaults]; + currentQueryRanges = nil; if ( [prefs objectForKey:@"queryFavorites"] ) { queryFavorites = [[NSMutableArray alloc] initWithArray:[prefs objectForKey:@"queryFavorites"]]; @@ -1310,41 +1341,42 @@ // whether the caret is inside a valid query. if ([textView selectedRange].length == 0) { int selectionPosition = [textView selectedRange].location; - int movedRangeStart, movedRangeLength; - BOOL updateQueryButtons = FALSE; - NSRange oldSelection; - NSCharacterSet *whitespaceAndNewlineCharset = [NSCharacterSet whitespaceAndNewlineCharacterSet]; + // int movedRangeStart, movedRangeLength; + BOOL updateQueryButtons = TRUE; + // NSRange oldSelection; + // NSCharacterSet *whitespaceAndNewlineCharset = [NSCharacterSet whitespaceAndNewlineCharacterSet]; // Retrieve the old selection position - [[[aNotification userInfo] objectForKey:@"NSOldSelectedCharacterRange"] getValue:&oldSelection]; + // [[[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) updateQueryButtons = TRUE; - if (!updateQueryButtons && movedRangeLength > 100) updateQueryButtons = TRUE; - if (!updateQueryButtons && oldSelection.location > [[textView string] length]) updateQueryButtons = TRUE; - if (!updateQueryButtons && [[textView string] rangeOfString:@";" options:0 range:NSMakeRange(movedRangeStart, movedRangeLength)].location != NSNotFound) updateQueryButtons = TRUE; - if (!updateQueryButtons && ![runSelectionButton isEnabled] && selectionPosition > oldSelection.location - && [[[[textView string] substringWithRange:NSMakeRange(movedRangeStart, movedRangeLength)] stringByTrimmingCharactersInSet:whitespaceAndNewlineCharset] length]) updateQueryButtons = TRUE; - if (!updateQueryButtons && [[runSelectionButton title] isEqualToString:NSLocalizedString(@"Run Current", @"Title of button to run current query in custom query view")]) { - int charPosition; - unichar theChar; - for (charPosition = selectionPosition; charPosition > 0; charPosition--) { - theChar = [[textView string] characterAtIndex:charPosition-1]; - if (theChar == ';') { - updateQueryButtons = TRUE; - break; - } - if (![whitespaceAndNewlineCharset characterIsMember:theChar]) break; - } - } - if (!updateQueryButtons && [[runSelectionButton title] isEqualToString:NSLocalizedString(@"Run Previous", @"Title of button to run query just before text caret in custom query view")]) { - updateQueryButtons = TRUE; - } + // movedRangeStart = (selectionPosition < oldSelection.location)?selectionPosition:oldSelection.location; + // movedRangeLength = abs(selectionPosition - oldSelection.location); + // updateQueryButtons = TRUE; + // if (oldSelection.length > 0) updateQueryButtons = TRUE; + // if (!updateQueryButtons && movedRangeLength > 100) updateQueryButtons = TRUE; + // if (!updateQueryButtons && oldSelection.location > [[textView string] length]) updateQueryButtons = TRUE; + // if (!updateQueryButtons && [[textView string] rangeOfString:@";" options:0 range:NSMakeRange(movedRangeStart, movedRangeLength)].location != NSNotFound) updateQueryButtons = TRUE; + // if (!updateQueryButtons && ![runSelectionButton isEnabled] && selectionPosition > oldSelection.location + // && [[[[textView string] substringWithRange:NSMakeRange(movedRangeStart, movedRangeLength)] stringByTrimmingCharactersInSet:whitespaceAndNewlineCharset] length]) updateQueryButtons = TRUE; + // if (!updateQueryButtons && [[runSelectionButton title] isEqualToString:NSLocalizedString(@"Run Current", @"Title of button to run current query in custom query view")]) { + // int charPosition; + // unichar theChar; + // for (charPosition = selectionPosition; charPosition > 0; charPosition--) { + // theChar = [[textView string] characterAtIndex:charPosition-1]; + // if (theChar == ';') { + // updateQueryButtons = TRUE; + // break; + // } + // if (![whitespaceAndNewlineCharset characterIsMember:theChar]) break; + // } + // } + // if (!updateQueryButtons && [[runSelectionButton title] isEqualToString:NSLocalizedString(@"Run Previous", @"Title of button to run query just before text caret in custom query view")]) { + // updateQueryButtons = TRUE; + // } if (updateQueryButtons) { [runSelectionButton setTitle:NSLocalizedString(@"Run Current", @"Title of button to run current query in custom query view")]; diff --git a/Source/SPSQLParser.h b/Source/SPSQLParser.h index af60645a..022e17bb 100644 --- a/Source/SPSQLParser.h +++ b/Source/SPSQLParser.h @@ -64,6 +64,10 @@ unichar *stringCharCache; long charCacheStart; long charCacheEnd; + NSString *delimiter; + int delimiterLength; + BOOL charIsDelimiter; + BOOL isDelimiterCommand; } @@ -74,6 +78,7 @@ typedef enum _SPCommentTypes { } SPCommentType; + /* * Removes comments within the current string, trimming "#", "--[/s]", and "/* * /" style strings. */ @@ -212,25 +217,16 @@ typedef enum _SPCommentTypes { */ - (NSArray *) splitStringByCharacter:(unichar)character skippingBrackets:(BOOL)skipBrackets ignoringQuotedStrings:(BOOL)ignoreQuotedStrings; -/* - * As splitStringByCharacter: ..., but allows control over quoting - * - it recognises CREATE ... BEGIN ... END statements - * - it can detect a SINGLE SQL statement in between - * delimiter foo ... foo delimiter ; - * ['delimiter ;' MUST be given!] - * - it returns an array of ranges (as NSString "{loc, length}"). - * FromPosition: is needed if a subrange is passed to sync the ranges - * according to the CQ textView ones. - */ -- (NSArray *) splitStringIntoRangesOfSQLQueries; -- (NSArray *) splitStringIntoRangesOfSQLQueriesFromPosition:(long)position; +- (NSArray *) splitSqlStringByCharacter:(unichar)character; +- (NSArray *) splitSqlStringIntoRangesByCharacter:(unichar)character; /* * Methods used internally by this class to power the methods above: */ - (long) firstOccurrenceOfCharacter:(unichar)character ignoringQuotedStrings:(BOOL)ignoreQuotedStrings; - (long) firstOccurrenceOfCharacter:(unichar)character afterIndex:(long)startIndex ignoringQuotedStrings:(BOOL)ignoreQuotedStrings; -- (long) firstOccurrenceOfCharacter:(unichar)character afterIndex:(long)startIndex skippingBrackets:(BOOL)skipBrackets ignoringQuotedStrings:(BOOL)ignoreQuotedStrings ; +- (long) firstOccurrenceOfCharacter:(unichar)character afterIndex:(long)startIndex skippingBrackets:(BOOL)skipBrackets ignoringQuotedStrings:(BOOL)ignoreQuotedStrings; +- (long) firstOccurrenceInSqlOfCharacter:(unichar)character afterIndex:(long)startIndex skippingBrackets:(BOOL)skipBrackets ignoringQuotedStrings:(BOOL)ignoreQuotedStrings; - (long) endIndexOfStringQuotedByCharacter:(unichar)quoteCharacter startingAtIndex:(long)index; - (long) endIndexOfCommentOfType:(SPCommentType)commentType startingAtIndex:(long)index; @@ -242,11 +238,6 @@ typedef enum _SPCommentTypes { - (void) deleteCharactersInRange:(NSRange)aRange; - (void) insertString:(NSString *)aString atIndex:(NSUInteger)anIndex; -/* - * return an array of queries - */ -- (NSArray *) parseQueries; - /* Required and primitive methods to allow subclassing class cluster */ #pragma mark - - (id) init; diff --git a/Source/SPSQLParser.m b/Source/SPSQLParser.m index 281c1b3d..ac59c2fc 100644 --- a/Source/SPSQLParser.m +++ b/Source/SPSQLParser.m @@ -43,54 +43,6 @@ TO_BUFFER_STATE to_scan_string (const char *); @implementation SPSQLParser : NSMutableString -/* - * return an array of queries - */ -- (NSArray *) parseQueries -{ - [self deleteComments]; - - /* this is a hack so I could test the funcs and procs viewing, needed a way to get those in. - * all this does is look for 'delimiter' in the text to trigger this parser. basically - * it runs through the query, line by line, and sets the delimiter accordingly to break - * out the individual queries. this is not very rebust but works for testing purposes. - * I believe Hans is currently working on a more robust parser. :mtv - */ - if( [string rangeOfString:@"delimiter" options:NSCaseInsensitiveSearch].location != NSNotFound ) { - NSString *delim = @";"; - NSString *thisLine = @""; - NSMutableArray *nq = [[NSMutableArray alloc] init]; - NSArray *lines = [self splitStringByCharacter:'\n']; - for( NSString *line in lines ) { - line = [line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; - if( [line hasPrefix:@"delimiter"] ) { - delim = [line substringFromIndex:9]; - delim = [delim stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; - NSLog( @"delimiter now [%@]", delim ); - continue; - } - if( [line hasSuffix:delim] ) { - thisLine = [thisLine stringByAppendingString:line]; - [nq addObject:[thisLine substringWithRange:NSMakeRange(0,[thisLine length]-[delim length])]]; - NSLog( @"query: [%@]", [thisLine substringWithRange:NSMakeRange(0,[thisLine length]-[delim length])] ); - thisLine = @""; - } - else { - thisLine = [thisLine stringByAppendingString:line]; - thisLine = [thisLine stringByAppendingString:@"\n"]; - } - } - if( thisLine != @"" ) { - [nq addObject:thisLine]; - NSLog( @"query: [%@]", thisLine ); - } - return nq; - } else { - // just split as normal - return [self splitStringByCharacter:';']; - } -} - /* * Removes comments within the current string, trimming "#", "--[/s]", and "/* * /" style strings. */ @@ -409,7 +361,6 @@ TO_BUFFER_STATE to_scan_string (const char *); return [self splitStringByCharacter:character skippingBrackets:NO ignoringQuotedStrings:ignoreQuotedStrings]; } - /* * As splitStringByCharacter: ..., but allows control over both bracketing and quoting. */ @@ -438,73 +389,84 @@ TO_BUFFER_STATE to_scan_string (const char *); } /* - * As splitStringByCharacter: ..., but allows control over quoting - * - it recognises CREATE ... BEGIN ... END statements - * - it can detect a SINGLE SQL statement in between - * delimiter foo ... foo delimiter ; - * ['delimiter ;' MUST be given!] - * - it returns an array of ranges (as NSString "{loc, length}"). - * FromPosition: is needed if a subrange is passed to sync the ranges - * according to the CQ textView ones. + * As splitStringByCharacter: ..., but allows control over both bracketing and quoting. */ -- (NSArray *) splitStringIntoRangesOfSQLQueries +- (NSArray *) splitSqlStringByCharacter:(unichar)character { - return [self splitStringIntoRangesOfSQLQueriesFromPosition:0]; + NSMutableArray *resultsArray = [NSMutableArray arrayWithCapacity:2000]; + + long stringIndex = -1, nextIndex = 0; + int queryLength; + + // these delimiter variables will be set in firstOccurrenceOfCharacter: + delimiter = nil; + delimiterLength = 0; // is delimiter length minus 1 + charIsDelimiter = YES; // flag if passed character is the current delimiter + isDelimiterCommand = NO; + + IMP firstOccOfChar = [self methodForSelector:@selector(firstOccurrenceInSqlOfCharacter:afterIndex:skippingBrackets:ignoringQuotedStrings:)]; + IMP subString = [string methodForSelector:@selector(substringWithRange:)]; + + // Walk through the string finding the character to split by, and add all strings to the array. + while (1) { + + nextIndex = (long)(*firstOccOfChar)(self, @selector(firstOccurrenceInSqlOfCharacter:afterIndex:skippingBrackets:ignoringQuotedStrings:), character, stringIndex, NO, YES); + if (nextIndex == NSNotFound) + break; + + stringIndex += 1; + // Ignore a delimiter command and check range length + queryLength = nextIndex - stringIndex - delimiterLength; + if(!isDelimiterCommand && queryLength > 0) + [resultsArray addObject:(NSString *)(*subString)(string, @selector(substringWithRange:), NSMakeRange(stringIndex, queryLength))]; + if(isDelimiterCommand) isDelimiterCommand = NO; + stringIndex = nextIndex; + } + + // Add the end of the string after the previously matched character where appropriate. + if (stringIndex + 1 < [string length]) + [resultsArray addObject:[string substringFromIndex:stringIndex + 1]]; + + return resultsArray; } -- (NSArray *) splitStringIntoRangesOfSQLQueriesFromPosition:(long)position + +/* + * As splitStringByCharacter: but it returns only the ranges of queries as NSValues + */ +- (NSArray *) splitSqlStringIntoRangesByCharacter:(unichar)character { - NSMutableArray *resultsArray = [NSMutableArray array]; + NSMutableArray *resultsArray = [NSMutableArray arrayWithCapacity:2000]; + + long stringIndex = -1, nextIndex = 0; + int queryLength; - //initialise flex - yyuoffset = 0; yyuleng = 0; - to_switch_to_buffer(to_scan_string([string UTF8String])); + // these delimiter variables will be set in firstOccurrenceOfCharacter: + delimiter = nil; + delimiterLength = 0; // is delimiter length minus 1 + charIsDelimiter = YES; // flag if passed character is the current delimiter + isDelimiterCommand = NO; - unsigned long token; - unsigned long lastFoundToken = 0; - unsigned long delimLength = 0; - unsigned long commentStart = 0; - unsigned long commentLength = 0; + IMP firstOccOfChar = [self methodForSelector:@selector(firstOccurrenceInSqlOfCharacter:afterIndex:skippingBrackets:ignoringQuotedStrings:)]; - NSString *delimString; + // Walk through the string finding the character to split by, and add all strings to the array. + while (1) { - //now loop through all queries - while (token=tolex()){ - switch (token) { - case SP_SQL_TOKEN_SEMICOLON: - [resultsArray addObject:[NSString stringWithFormat:@"{%d,%d}", lastFoundToken+position, yyuoffset-lastFoundToken]]; - break; - case SP_SQL_TOKEN_SINGLE_LINE_COMMENT: - commentStart = yyuoffset+position; - commentLength = yyuleng; - break; - case SP_SQL_TOKEN_DELIM_END: - [resultsArray addObject:[NSString stringWithFormat:@"{%d,%d}", lastFoundToken+position, yyuoffset-lastFoundToken-delimLength]]; - delimLength = 0; - delimString = nil; - break; - case SP_SQL_TOKEN_DELIM_VALUE: - delimString = [string substringWithRange:NSMakeRange(yyuoffset,yyuleng)]; - // NSLog(@"del: %@", delimString); - delimLength = yyuleng; - break; - case SP_SQL_TOKEN_COMPOUND_END: - [resultsArray addObject:[NSString stringWithFormat:@"{%d,%d}", lastFoundToken+position, yyuoffset+yyuleng-lastFoundToken]]; - break; - default: - continue; - } - if(token 0) + [resultsArray addObject:[NSValue valueWithRange:NSMakeRange(stringIndex, queryLength)]]; + if(isDelimiterCommand) isDelimiterCommand = NO; + stringIndex = nextIndex; + } + + // Add the end of the string after the previously matched character where appropriate. + if (stringIndex + 1 < [string length]) + [resultsArray addObject:[NSValue valueWithRange:NSMakeRange(stringIndex + 1, [string length] - stringIndex - 1)]]; return resultsArray; } @@ -526,10 +488,7 @@ TO_BUFFER_STATE to_scan_string (const char *); return [self firstOccurrenceOfCharacter:character afterIndex:startIndex skippingBrackets:NO ignoringQuotedStrings:ignoreQuotedStrings]; } - -/* - * A method intended for use by the functions above. - */ + - (long) firstOccurrenceOfCharacter:(unichar)character afterIndex:(long)startIndex skippingBrackets:(BOOL)skipBrackets ignoringQuotedStrings:(BOOL)ignoreQuotedStrings { long currentStringIndex, quotedStringEndIndex; @@ -604,6 +563,130 @@ TO_BUFFER_STATE to_scan_string (const char *); return NSNotFound; } +/* + * Look for the first occurence of a char and reset the split char on runtime + * via “delimiter” command for splitSqlStringIntoRangesByCharacter: and splitSqlStringByCharacter. + */ +- (long) firstOccurrenceInSqlOfCharacter:(unichar)character afterIndex:(long)startIndex skippingBrackets:(BOOL)skipBrackets ignoringQuotedStrings:(BOOL)ignoreQuotedStrings +{ + long currentStringIndex, quotedStringEndIndex; + unichar currentCharacter; + 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 = (unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), currentStringIndex); + + // Check for the ending character, and if it has been found and quoting/brackets is valid, return. + // no “delimiter” is set by the user + if (charIsDelimiter) + { + if(currentCharacter == character) + if (!skipBrackets || bracketingLevel <= 0) + return currentStringIndex; + } + // a “delimiter” other than 'character' is set by the user + else + { + if([[self substringWithRange:NSMakeRange(currentStringIndex - delimiterLength, delimiterLength + 1)] isEqualToString:delimiter]) + if (!skipBrackets || bracketingLevel <= 0) + return currentStringIndex; + } + + // Process strings and comments as appropriate + switch (currentCharacter) { + + // When quote characters are encountered and strings are not being ignored, walk to the end of the quoted string. + case '\'': + case '"': + case '`': + if (!ignoreQuotedStrings) break; + quotedStringEndIndex = (long)(*endIndex)(self, @selector(endIndexOfStringQuotedByCharacter:startingAtIndex:), currentCharacter, currentStringIndex+1); + if (quotedStringEndIndex == NSNotFound) { + return NSNotFound; + } + currentStringIndex = quotedStringEndIndex; + break; + + // For opening brackets increment the bracket count + case '(': + bracketingLevel++; + break; + + // For closing brackets decrement the bracket count + case ')': + bracketingLevel--; + + // For comments starting "--[\s]", ensure the start syntax is valid before proceeding. + case '-': + if (stringLength < 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; + + case '#': + currentStringIndex = [self endIndexOfCommentOfType:SPHashComment startingAtIndex:currentStringIndex]; + break; + + // For comments starting "/*", ensure the start syntax is valid before proceeding. + case '/': + if (stringLength < currentStringIndex + 1) break; + if ((unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), currentStringIndex+1) != '*') break; + currentStringIndex = [self endIndexOfCommentOfType:SPCStyleComment startingAtIndex:currentStringIndex]; + break; + case 'd': + case 'D': // only parse to “deli” because there's no default command which begins with it; then check via regex + // Check for length of “elimiter x\s” + if (stringLength >= currentStringIndex + 11) { + // Check for “(^|\s)delimiter” + if(currentStringIndex == 0 + || (currentStringIndex && [[NSCharacterSet whitespaceAndNewlineCharacterSet] characterIsMember:(unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), currentStringIndex-1)])) { + NSArray *delimiterString; + switch((unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), currentStringIndex+1)) { + case 'e': + case 'E': + switch((unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), currentStringIndex+2)) { + case 'l': + case 'L': + switch((unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), currentStringIndex+3)) { + case 'i': + case 'I': + if([self isMatchedByRegex:@"^(delimiter[ \\t]+(\\S+))(?=\\s)" + options:RKLCaseless + inRange:NSMakeRange(currentStringIndex, stringLength - currentStringIndex) + error:nil]) { + isDelimiterCommand = YES; + delimiterString = [[self arrayOfCaptureComponentsMatchedByRegex:@"(?i)^(delimiter[ \\t]+(\\S+))(?=\\s)" + range:NSMakeRange(currentStringIndex, stringLength - currentStringIndex)] objectAtIndex:0]; + delimiter = [delimiterString objectAtIndex:2]; + delimiterLength = [delimiter length] - 1; + charIsDelimiter = ([delimiter isEqualToString:[NSString stringWithFormat:@"%C", character]]); + return currentStringIndex + [[delimiterString objectAtIndex:1] length] - delimiterLength; + } + default: break; + } + default: break; + } + default: break; + } + } + } + } + } + + // If no matches have been made in this string, return NSNotFound. + return NSNotFound; +} + /* * A method intended for use by the functions above. */ @@ -753,8 +836,6 @@ TO_BUFFER_STATE to_scan_string (const char *); [self clearCharCache]; } - - /* Required and primitive methods to allow subclassing class cluster */ #pragma mark - - (id) init { @@ -853,3 +934,76 @@ TO_BUFFER_STATE to_scan_string (const char *); } @end + + +/* + * As splitStringByCharacter: ..., but allows control over quoting + * - it recognises CREATE ... BEGIN ... END statements + * - it can detect a SINGLE SQL statement in between + * delimiter foo ... foo delimiter ; + * ['delimiter ;' MUST be given!] + * - it returns an array of ranges (as NSString "{loc, length}"). + * FromPosition: is needed if a subrange is passed to sync the ranges + * according to the CQ textView ones. + */ +// - (NSArray *) splitStringIntoRangesOfSQLQueries +// { +// return [self splitStringIntoRangesOfSQLQueriesFromPosition:0]; +// } +// - (NSArray *) splitStringIntoRangesOfSQLQueriesFromPosition:(long)position +// { +// NSMutableArray *resultsArray = [NSMutableArray array]; +// +// //initialise flex +// yyuoffset = 0; yyuleng = 0; +// to_switch_to_buffer(to_scan_string([string UTF8String])); +// +// unsigned long token; +// unsigned long lastFoundToken = 0; +// unsigned long delimLength = 0; +// unsigned long commentStart = 0; +// unsigned long commentLength = 0; +// +// NSString *delimString; +// +// //now loop through all queries +// while (token=tolex()){ +// switch (token) { +// case SP_SQL_TOKEN_SEMICOLON: +// [resultsArray addObject:[NSString stringWithFormat:@"{%d,%d}", lastFoundToken+position, yyuoffset-lastFoundToken]]; +// break; +// case SP_SQL_TOKEN_SINGLE_LINE_COMMENT: +// commentStart = yyuoffset+position; +// commentLength = yyuleng; +// break; +// case SP_SQL_TOKEN_DELIM_END: +// [resultsArray addObject:[NSString stringWithFormat:@"{%d,%d}", lastFoundToken+position, yyuoffset-lastFoundToken-delimLength]]; +// delimLength = 0; +// delimString = nil; +// break; +// case SP_SQL_TOKEN_DELIM_VALUE: +// delimString = [string substringWithRange:NSMakeRange(yyuoffset,yyuleng)]; +// // NSLog(@"del: %@", delimString); +// delimLength = yyuleng; +// break; +// case SP_SQL_TOKEN_COMPOUND_END: +// [resultsArray addObject:[NSString stringWithFormat:@"{%d,%d}", lastFoundToken+position, yyuoffset+yyuleng-lastFoundToken]]; +// break; +// default: +// continue; +// } +// if(token