//
// $Id$
//
// SPSQLParsing.m
// sequel-pro
//
// Created by Rowan Beentje on 18/01/2009.
// Copyright 2009 Rowan Beentje. All rights reserved.
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
// More info at
#import "SPSQLParser.h"
#import "RegexKitLite.h"
/*
* Include all the extern variables and prototypes required for flex (used for syntax highlighting)
*/
#import "SPSQLTokenizer.h"
extern int tolex();
extern int yyuoffset, yyuleng;
typedef struct to_buffer_state *TO_BUFFER_STATE;
void to_switch_to_buffer(TO_BUFFER_STATE);
TO_BUFFER_STATE to_scan_string (const char *);
/*
* Please see the header files for a general description of the purpose of this class,
* and increased overview detail for the functions below.
*/
@implementation SPSQLParser : NSMutableString
- (void)setIgnoringCommentStrings:(BOOL)ignoringCommentStrings
{
ignoreCommentStrings = ignoringCommentStrings;
}
/*
* Removes comments within the current string, trimming "#", "--[/s]", and "⁄* *⁄" style strings.
*/
- (void) deleteComments
{
long currentStringIndex, commentEndIndex, quotedStringEndIndex;
unichar currentCharacter;
long stringLength = [string length];
// Walk along the string, processing characters.
for (currentStringIndex = 0; currentStringIndex < stringLength; currentStringIndex++) {
currentCharacter = [string characterAtIndex:currentStringIndex];
switch (currentCharacter) {
// When quote characters are encountered walk to the end of the quoted string.
case '\'':
case '"':
case '`':
quotedStringEndIndex = [self endIndexOfStringQuotedByCharacter:currentCharacter startingAtIndex:currentStringIndex+1];
if (quotedStringEndIndex == NSNotFound) {
return;
}
currentStringIndex = quotedStringEndIndex;
break;
// 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 whitespaceCharacterSet] characterIsMember:[string characterAtIndex:currentStringIndex+2]]) break;
commentEndIndex = [self endIndexOfCommentOfType:SPDoubleDashComment startingAtIndex:currentStringIndex];
// Remove the comment
[self deleteCharactersInRange:NSMakeRange(currentStringIndex, commentEndIndex - currentStringIndex + 1)];
stringLength -= commentEndIndex - currentStringIndex + 1;
currentStringIndex--;
break;
case '#':
commentEndIndex = [self endIndexOfCommentOfType:SPHashComment startingAtIndex:currentStringIndex];
// Remove the comment
[self deleteCharactersInRange:NSMakeRange(currentStringIndex, commentEndIndex - currentStringIndex + 1)];
stringLength -= commentEndIndex - currentStringIndex + 1;
currentStringIndex--;
break;
// For comments starting "/*", ensure the start syntax is valid before proceeding.
case '/':
if (stringLength < currentStringIndex + 1) break;
if ([string characterAtIndex:currentStringIndex+1] != '*') break;
commentEndIndex = [self endIndexOfCommentOfType:SPCStyleComment startingAtIndex:currentStringIndex];
// Remove the comment
[self deleteCharactersInRange:NSMakeRange(currentStringIndex, commentEndIndex - currentStringIndex + 1)];
stringLength -= commentEndIndex - currentStringIndex + 1;
currentStringIndex--;
break;
}
}
}
/*
* Removes quotes surrounding the string if present, and un-escapes internal occurrences of the quote character before returning.
*/
- (NSString *) unquotedString
{
NSMutableString *returnString;
long stringEndIndex;
unichar quoteCharacter;
if (![string length]) return nil;
// If the first character is not a quote character, return the entire string.
quoteCharacter = [string characterAtIndex:0];
if (quoteCharacter != '`' && quoteCharacter != '"' && quoteCharacter != '\'') {
return [NSString stringWithString:string];
}
// Get the end of the string
stringEndIndex = [self endIndexOfStringQuotedByCharacter:quoteCharacter startingAtIndex:1];
if (stringEndIndex == NSNotFound) {
return [NSString stringWithString:string];
}
// Trim the string appropriately
returnString = [NSMutableString stringWithString:[string substringWithRange:NSMakeRange(1, stringEndIndex-1)]];
// Remove escaped characters and escaped strings as appropriate
if (quoteCharacter == '`' || quoteCharacter == '"' || quoteCharacter == '\'') {
[returnString replaceOccurrencesOfString:[NSString stringWithFormat:@"%C%C", quoteCharacter, quoteCharacter] withString:[NSString stringWithFormat:@"%C", quoteCharacter] options:0 range:NSMakeRange(0, [returnString length])];
}
if (quoteCharacter == '"') {
[returnString replaceOccurrencesOfString:@"\\\"" withString:@"\"" options:0 range:NSMakeRange(0, [returnString length])];
[returnString replaceOccurrencesOfString:@"\\\\" withString:@"\\" options:0 range:NSMakeRange(0, [returnString length])];
} else if (quoteCharacter == '\'') {
[returnString replaceOccurrencesOfString:@"\\'" withString:@"'" options:0 range:NSMakeRange(0, [returnString length])];
[returnString replaceOccurrencesOfString:@"\\\\" withString:@"\\" options:0 range:NSMakeRange(0, [returnString length])];
}
return returnString;
}
/*
* Removes characters from the string up to the first occurrence of the supplied character.
*/
- (BOOL) trimToCharacter:(unichar)character inclusively:(BOOL)inclusive
{
return [self trimToCharacter:character inclusively:inclusive ignoringQuotedStrings:YES];
}
/*
* As trimToCharacter: ..., but allows control over whether characters within quoted
* strings are ignored.
*/
- (BOOL) trimToCharacter:(unichar)character inclusively:(BOOL)inclusive ignoringQuotedStrings:(BOOL)ignoreQuotedStrings
{
long stringIndex;
// Get the first occurrence of the specified character, returning NO if not found
stringIndex = [self firstOccurrenceOfCharacter:character ignoringQuotedStrings:ignoreQuotedStrings];
if (stringIndex == NSNotFound) return NO;
// If it has been found, trim the string appropriately and return YES
[self deleteCharactersInRange:NSMakeRange(0, stringIndex + (inclusive?1:0))];
return YES;
}
/*
* Returns an NSString containing characters from the string up to the first occurrence of the supplied character.
*/
- (NSString *) stringToCharacter:(unichar)character inclusively:(BOOL)inclusive
{
return [self stringToCharacter:character inclusively:inclusive ignoringQuotedStrings:YES];
}
/*
* As stringToCharacter: ..., but allows control over whether characters within quoted strings
* are ignored.
*/
- (NSString *) stringToCharacter:(unichar)character inclusively:(BOOL)inclusive ignoringQuotedStrings:(BOOL)ignoreQuotedStrings {
long stringIndex;
// Get the first occurrence of the specified character, returning nil if not found
stringIndex = [self firstOccurrenceOfCharacter:character ignoringQuotedStrings:ignoreQuotedStrings];
if (stringIndex == NSNotFound) return nil;
// If it has been found, return the appropriate string range
return [string substringWithRange:NSMakeRange(0, stringIndex + (inclusive?1:0))];
}
/*
* Returns an NSString containing characters from the string up to the first occurrence of the supplied
* character, also removing them from the string.
*/
- (NSString *) trimAndReturnStringToCharacter:(unichar)character trimmingInclusively:(BOOL)inclusiveTrim returningInclusively:(BOOL)inclusiveReturn
{
return [self trimAndReturnStringToCharacter:character trimmingInclusively:inclusiveTrim returningInclusively:inclusiveReturn ignoringQuotedStrings:YES];
}
/*
* As trimAndReturnStringToCharacter: ..., but allows control over whether characters within quoted
* strings are ignored.
*/
- (NSString *) trimAndReturnStringToCharacter:(unichar)character trimmingInclusively:(BOOL)inclusiveTrim returningInclusively:(BOOL)inclusiveReturn ignoringQuotedStrings:(BOOL)ignoreQuotedStrings
{
long stringIndex;
NSString *resultString;
if (character != parsedToChar) {
parsedToChar = character;
parsedToPosition = -1;
}
// Get the first occurrence of the specified character, returning nil if it could not be found
stringIndex = [self firstOccurrenceOfCharacter:character afterIndex:parsedToPosition ignoringQuotedStrings:ignoreQuotedStrings];
if (stringIndex == NSNotFound) return nil;
// 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))]];
[self deleteCharactersInRange:NSMakeRange(0, stringIndex + (inclusiveTrim?1:0))];
return resultString;
}
/*
* Returns characters from the string up to and from the first occurrence of the supplied opening character
* to the appropriate occurrence of the supplied closing character. "inclusively" controls whether the supplied
* characters should also be returned.
*/
- (NSString *) stringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter inclusively:(BOOL)inclusive
{
return [self stringFromCharacter:fromCharacter toCharacter:toCharacter inclusively:inclusive skippingBrackets:NO ignoringQuotedStrings:YES];
}
/*
* As stringFromCharacter: toCharacter: ..., but allows control over whether to skip
* over bracket-enclosed characters, as in subqueries, enums, definitions or groups
*/
- (NSString *) stringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter inclusively:(BOOL)inclusive skippingBrackets:(BOOL)skipBrackets
{
return [self stringFromCharacter:fromCharacter toCharacter:toCharacter inclusively:inclusive skippingBrackets:skipBrackets ignoringQuotedStrings:YES];
}
/*
* As stringFromCharacter: toCharacter: ..., but allows control over whether characters within quoted
* strings are ignored.
*/
- (NSString *) stringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter inclusively:(BOOL)inclusive ignoringQuotedStrings:(BOOL)ignoreQuotedStrings
{
return [self stringFromCharacter:fromCharacter toCharacter:toCharacter inclusively:inclusive skippingBrackets:NO ignoringQuotedStrings:ignoreQuotedStrings];
}
/*
* As stringFromCharacter: toCharacter: ..., but allows control over both bracketing and quoting.
*/
- (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;
// Look for the first/balancing occurrence of the to: character
toCharacterIndex = [self firstOccurrenceOfCharacter:toCharacter afterIndex:fromCharacterIndex skippingBrackets:skipBrackets ignoringQuotedStrings:ignoreQuotedStrings];
if (toCharacterIndex == NSNotFound) return nil;
// Return the correct part of the string.
return [string substringWithRange:NSMakeRange(fromCharacterIndex + (inclusive?0:1), toCharacterIndex + (inclusive?1:-1) - fromCharacterIndex)];
}
/*
* As stringFromCharacter: toCharacter: ..., but also trims the string up to the "to" character and
* up to or including the "from" character, depending on whether "trimmingInclusively" is set.
*/
- (NSString *) trimAndReturnStringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter trimmingInclusively:(BOOL)inclusiveTrim returningInclusively:(BOOL)inclusiveReturn
{
return [self trimAndReturnStringFromCharacter:fromCharacter toCharacter:toCharacter trimmingInclusively:inclusiveTrim returningInclusively:inclusiveReturn skippingBrackets:NO ignoringQuotedStrings:YES];
}
/*
* As trimAndReturnStringFromCharacter: toCharacter: ..., but allows control over whether to
* skip over bracket-enclosed characters, as in subqueries, enums, definitions or groups.
*/
- (NSString *) trimAndReturnStringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter trimmingInclusively:(BOOL)inclusiveTrim returningInclusively:(BOOL)inclusiveReturn skippingBrackets:(BOOL)skipBrackets
{
return [self trimAndReturnStringFromCharacter:fromCharacter toCharacter:toCharacter trimmingInclusively:inclusiveTrim returningInclusively:inclusiveReturn skippingBrackets:skipBrackets ignoringQuotedStrings:YES];
}
/*
* As trimAndReturnStringFromCharacter: toCharacter: ..., but allows control over whether characters
* within quoted strings are ignored.
*/
- (NSString *) trimAndReturnStringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter trimmingInclusively:(BOOL)inclusiveTrim returningInclusively:(BOOL)inclusiveReturn ignoringQuotedStrings:(BOOL)ignoreQuotedStrings
{
return [self trimAndReturnStringFromCharacter:fromCharacter toCharacter:toCharacter trimmingInclusively:inclusiveTrim returningInclusively:inclusiveReturn skippingBrackets:NO ignoringQuotedStrings:ignoreQuotedStrings];
}
/*
* As trimAndReturnStringFromCharacter: toCharacter: ..., but allows control over both bracketing
* and quoting.
*/
- (NSString *) trimAndReturnStringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter trimmingInclusively:(BOOL)inclusiveTrim returningInclusively:(BOOL)inclusiveReturn skippingBrackets:(BOOL)skipBrackets ignoringQuotedStrings:(BOOL)ignoreQuotedStrings
{
long fromCharacterIndex, toCharacterIndex;
NSString *resultString;
// Look for the first occurrence of the from: character
fromCharacterIndex = [self firstOccurrenceOfCharacter:fromCharacter afterIndex:-1 skippingBrackets:skipBrackets ignoringQuotedStrings:ignoreQuotedStrings];
if (fromCharacterIndex == NSNotFound) return nil;
// Look for the first/balancing occurrence of the to: character
toCharacterIndex = [self firstOccurrenceOfCharacter:toCharacter afterIndex:fromCharacterIndex skippingBrackets:skipBrackets ignoringQuotedStrings:ignoreQuotedStrings];
if (toCharacterIndex == NSNotFound) return nil;
// 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)];
[self deleteCharactersInRange:NSMakeRange(fromCharacterIndex + (inclusiveTrim?0:1), toCharacterIndex + (inclusiveTrim?1:-1) - fromCharacterIndex)];
return resultString;
}
/*
* Split a string on the boundaries formed by the supplied character, returning an array of strings.
*/
- (NSArray *) splitStringByCharacter:(unichar)character
{
return [self splitStringByCharacter:character skippingBrackets:NO ignoringQuotedStrings:YES];
}
/*
* As splitStringByCharacter: ..., but allows control over whether to skip over bracket-enclosed
* characters, as in subqueries, enums, definitions or groups.
*/
- (NSArray *) splitStringByCharacter:(unichar)character skippingBrackets:(BOOL)skipBrackets
{
return [self splitStringByCharacter:character skippingBrackets:skipBrackets ignoringQuotedStrings:YES];
}
/*
* As splitStringByCharacter:, but allows control over whether characters
* within quoted strings are ignored.
*/
- (NSArray *) splitStringByCharacter:(unichar)character ignoringQuotedStrings:(BOOL)ignoreQuotedStrings
{
return [self splitStringByCharacter:character skippingBrackets:NO ignoringQuotedStrings:ignoreQuotedStrings];
}
/*
* As splitStringByCharacter: ..., but allows control over both bracketing and quoting.
*/
- (NSArray *) splitStringByCharacter:(unichar)character skippingBrackets:(BOOL)skipBrackets ignoringQuotedStrings:(BOOL)ignoreQuotedStrings
{
NSMutableArray *resultsArray = [NSMutableArray array];
long stringIndex = -1, nextIndex = 0;
// 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;
}
[resultsArray addObject:[string substringWithRange:NSMakeRange(stringIndex+1, nextIndex - stringIndex - 1)]];
stringIndex = nextIndex;
}
// Add the end of the string after the previously matched character where appropriate.
if (stringIndex + 1 < [string length]) {
[resultsArray addObject:[string substringWithRange:NSMakeRange(stringIndex+1, [string length] - stringIndex - 1)]];
}
return resultsArray;
}
/*
* As splitStringByCharacter: ..., but allows control over both bracketing and quoting.
*/
- (NSArray *) splitSqlStringByCharacter:(unichar)character
{
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 it does not contain only white space characters and if the last query is not a
// "delimiter" statement to avoid unnecessary error messages.
if (stringIndex + 1 < [string length]) {
NSString *lastQuery = [[string substringFromIndex:stringIndex + 1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if([lastQuery length] && ![lastQuery isMatchedByRegex:@"(?i)^\\s*delimiter\\s+\\S+"])
[resultsArray addObject:lastQuery];
}
return resultsArray;
}
/*
* As splitStringByCharacter: but it returns only the ranges of queries as NSValues
*/
- (NSArray *) splitSqlStringIntoRangesByCharacter:(unichar)character
{
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:)];
// 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:[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;
}
/*
* A method intended for use by the functions above.
*/
- (long) firstOccurrenceOfCharacter:(unichar)character ignoringQuotedStrings:(BOOL)ignoreQuotedStrings
{
return [self firstOccurrenceOfCharacter:character afterIndex:-1 skippingBrackets:NO ignoringQuotedStrings:ignoreQuotedStrings];
}
/*
* A method intended for use by the functions above.
*/
- (long) firstOccurrenceOfCharacter:(unichar)character afterIndex:(long)startIndex ignoringQuotedStrings:(BOOL)ignoreQuotedStrings
{
return [self firstOccurrenceOfCharacter:character afterIndex:startIndex skippingBrackets:NO ignoringQuotedStrings:ignoreQuotedStrings];
}
- (long) firstOccurrenceOfCharacter:(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.
if (currentCharacter == character) {
if (!skipBrackets || bracketingLevel <= 0) {
parsedToPosition = currentStringIndex;
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) {
parsedToPosition = stringLength - 1;
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 '#':
if(ignoreCommentStrings) break;
currentStringIndex = [self endIndexOfCommentOfType:SPHashComment startingAtIndex:currentStringIndex];
break;
// For comments starting "/*", ensure the start syntax is valid before proceeding.
case '/':
if(ignoreCommentStrings) break;
if (stringLength < currentStringIndex + 1) break;
if ((unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), currentStringIndex+1) != '*') break;
currentStringIndex = [self endIndexOfCommentOfType:SPCStyleComment startingAtIndex:currentStringIndex];
break;
}
}
// If no matches have been made in this string, return NSNotFound.
parsedToPosition = stringLength - 1;
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 '#':
if(ignoreCommentStrings) break;
currentStringIndex = [self endIndexOfCommentOfType:SPHashComment startingAtIndex:currentStringIndex];
break;
// For comments starting "/*", ensure the start syntax is valid before proceeding.
case '/':
if(ignoreCommentStrings) break;
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.
*/
- (long) endIndexOfStringQuotedByCharacter:(unichar)quoteCharacter startingAtIndex:(long)index
{
long currentStringIndex, stringLength, i, quotedStringLength;
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 = (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 && (unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), currentStringIndex+1) == '`') {
currentStringIndex++;
continue;
}
return currentStringIndex;
// Otherwise, prepare to treat the string as ended when meeting the correct boundary character....
} else if (currentCharacter == quoteCharacter) {
// ...but only if the string end isn't escaped with an *odd* number of escaping characters...
characterIsEscaped = NO;
i = 1;
quotedStringLength = currentStringIndex - 1;
while ((quotedStringLength - i) > 0 && (unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), currentStringIndex - i) == '\\') {
characterIsEscaped = !characterIsEscaped;
i++;
}
// 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 && (unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), currentStringIndex+1) == quoteCharacter) {
currentStringIndex++;
continue;
}
// Really is the end of the string.
return currentStringIndex;
}
}
}
return NSNotFound;
}
/*
* A method intended for use by the functions above.
*/
- (long) endIndexOfCommentOfType:(SPCommentType)commentType startingAtIndex:(long)index
{
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,
// then flow into the Hash comment handling (looking for first newline).
case SPDoubleDashComment:
index = index+2;
// For comments starting "--[\s]" and "#", continue until the first newline.
case SPHashComment:
index++;
for ( ; index < stringLength; index++ ) {
currentCharacter = (unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), index);
if (currentCharacter == '\r' || currentCharacter == '\n') {
return index-1;
}
}
break;
// For comments starting "/*", start the comment processing one character in to match the start syntax, then
// continue until the first matching "*/".
case SPCStyleComment:
index = index+2;
for ( ; index < stringLength; index++ ) {
if ((unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), index) == '*') {
if ((stringLength > index + 1) && (unichar)(long)(*charAtIndex)(self, @selector(charAtIndex:), index+1) == '/') {
return (index+1);
}
}
}
}
// If no match has been found, the comment must continue until the very end of the string.
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;
parsedToChar = '\0';
parsedToPosition = -1;
}
- (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 -
- (id) init {
if (self = [super init]) {
string = [[NSMutableString string] retain];
}
parsedToChar = '\0';
parsedToPosition = -1;
charCacheEnd = -1;
ignoreCommentStrings = NO;
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];
}
parsedToChar = '\0';
parsedToPosition = -1;
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];
}
parsedToChar = '\0';
parsedToPosition = -1;
charCacheEnd = -1;
return self;
}
- (id) initWithCapacity:(unsigned int)capacity {
if (self = [super init]) {
string = [[NSMutableString stringWithCapacity:capacity] retain];
}
parsedToChar = '\0';
parsedToPosition = -1;
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];
}
parsedToChar = '\0';
parsedToPosition = -1;
charCacheEnd = -1;
return self;
}
- (id) initWithContentsOfFile:(id)path {
parsedToChar = '\0';
parsedToPosition = -1;
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];
}
parsedToChar = '\0';
parsedToPosition = -1;
charCacheEnd = -1;
return self;
}
- (id) initWithCString:(const char *)nullTerminatedCString encoding:(NSStringEncoding)encoding {
if (self = [super init]) {
string = [[NSMutableString alloc] initWithCString:nullTerminatedCString encoding:encoding];
}
parsedToChar = '\0';
parsedToPosition = -1;
charCacheEnd = -1;
return self;
}
- (id) initWithFormat:(NSString *)format, ... {
va_list argList;
va_start(argList, format);
id str = [self initWithFormat:format arguments:argList];
va_end(argList);
parsedToChar = '\0';
parsedToPosition = -1;
charCacheEnd = -1;
return str;
}
- (id) initWithFormat:(NSString *)format arguments:(va_list)argList {
if (self = [super init]) {
string = [[NSMutableString alloc] initWithFormat:format arguments:argList];
}
parsedToChar = '\0';
parsedToPosition = -1;
charCacheEnd = -1;
return self;
}
- (unsigned int) length {
return [string length];
}
- (unichar) characterAtIndex:(unsigned int)index {
return [string characterAtIndex:index];
}
- (id) description {
return [string description];
}
- (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
/*
* 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