Browse Source

added batch sql splitter

openthread 11 years ago
parent
commit
2b7f7adff4

+ 55 - 0
fmdb.xcodeproj/project.pbxproj

@@ -7,6 +7,22 @@
 	objects = {
 
 /* Begin PBXBuildFile section */
+		42C753F518F2D2A400F79E14 /* FMSQLStatementSplitter.h in Headers */ = {isa = PBXBuildFile; fileRef = 42C753EE18F2D2A400F79E14 /* FMSQLStatementSplitter.h */; };
+		42C753F618F2D2A400F79E14 /* FMSQLStatementSplitter.m in Sources */ = {isa = PBXBuildFile; fileRef = 42C753EF18F2D2A400F79E14 /* FMSQLStatementSplitter.m */; };
+		42C753F718F2D2A400F79E14 /* FMSQLStatementSplitter.m in Sources */ = {isa = PBXBuildFile; fileRef = 42C753EF18F2D2A400F79E14 /* FMSQLStatementSplitter.m */; };
+		42C753F818F2D2A400F79E14 /* FMSQLStatementSplitter.m in Sources */ = {isa = PBXBuildFile; fileRef = 42C753EF18F2D2A400F79E14 /* FMSQLStatementSplitter.m */; };
+		42C753F918F2D2A400F79E14 /* FMSQLStatementSplitter.m in Sources */ = {isa = PBXBuildFile; fileRef = 42C753EF18F2D2A400F79E14 /* FMSQLStatementSplitter.m */; };
+		42C753FA18F2D2A400F79E14 /* FMStatementKeywordRecogniser.h in Headers */ = {isa = PBXBuildFile; fileRef = 42C753F018F2D2A400F79E14 /* FMStatementKeywordRecogniser.h */; };
+		42C753FB18F2D2A400F79E14 /* FMStatementKeywordRecogniser.m in Sources */ = {isa = PBXBuildFile; fileRef = 42C753F118F2D2A400F79E14 /* FMStatementKeywordRecogniser.m */; };
+		42C753FC18F2D2A400F79E14 /* FMStatementKeywordRecogniser.m in Sources */ = {isa = PBXBuildFile; fileRef = 42C753F118F2D2A400F79E14 /* FMStatementKeywordRecogniser.m */; };
+		42C753FD18F2D2A400F79E14 /* FMStatementKeywordRecogniser.m in Sources */ = {isa = PBXBuildFile; fileRef = 42C753F118F2D2A400F79E14 /* FMStatementKeywordRecogniser.m */; };
+		42C753FE18F2D2A400F79E14 /* FMStatementKeywordRecogniser.m in Sources */ = {isa = PBXBuildFile; fileRef = 42C753F118F2D2A400F79E14 /* FMStatementKeywordRecogniser.m */; };
+		42C753FF18F2D2A400F79E14 /* FMStatementQuotedRecogniser.h in Headers */ = {isa = PBXBuildFile; fileRef = 42C753F218F2D2A400F79E14 /* FMStatementQuotedRecogniser.h */; };
+		42C7540018F2D2A400F79E14 /* FMStatementQuotedRecogniser.m in Sources */ = {isa = PBXBuildFile; fileRef = 42C753F318F2D2A400F79E14 /* FMStatementQuotedRecogniser.m */; };
+		42C7540118F2D2A400F79E14 /* FMStatementQuotedRecogniser.m in Sources */ = {isa = PBXBuildFile; fileRef = 42C753F318F2D2A400F79E14 /* FMStatementQuotedRecogniser.m */; };
+		42C7540218F2D2A400F79E14 /* FMStatementQuotedRecogniser.m in Sources */ = {isa = PBXBuildFile; fileRef = 42C753F318F2D2A400F79E14 /* FMStatementQuotedRecogniser.m */; };
+		42C7540318F2D2A400F79E14 /* FMStatementQuotedRecogniser.m in Sources */ = {isa = PBXBuildFile; fileRef = 42C753F318F2D2A400F79E14 /* FMStatementQuotedRecogniser.m */; };
+		42C7540418F2D2A400F79E14 /* FMStatementTokenRecogniser.h in Headers */ = {isa = PBXBuildFile; fileRef = 42C753F418F2D2A400F79E14 /* FMStatementTokenRecogniser.h */; };
 		621721B21892BFE30006691F /* FMDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = CCC24EBB0A13E34D00A6D3E3 /* FMDatabase.m */; };
 		621721B31892BFE30006691F /* FMResultSet.m in Sources */ = {isa = PBXBuildFile; fileRef = CCC24EC00A13E34D00A6D3E3 /* FMResultSet.m */; };
 		621721B41892BFE30006691F /* FMDatabaseQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = CC47A00E148581E9002CCDAB /* FMDatabaseQueue.m */; };
@@ -82,6 +98,13 @@
 /* Begin PBXFileReference section */
 		08FB779EFE84155DC02AAC07 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = "<absolute>"; };
 		32A70AAB03705E1F00C91783 /* fmdb_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = fmdb_Prefix.pch; path = src/sample/fmdb_Prefix.pch; sourceTree = SOURCE_ROOT; };
+		42C753EE18F2D2A400F79E14 /* FMSQLStatementSplitter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FMSQLStatementSplitter.h; sourceTree = "<group>"; };
+		42C753EF18F2D2A400F79E14 /* FMSQLStatementSplitter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FMSQLStatementSplitter.m; sourceTree = "<group>"; };
+		42C753F018F2D2A400F79E14 /* FMStatementKeywordRecogniser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FMStatementKeywordRecogniser.h; sourceTree = "<group>"; };
+		42C753F118F2D2A400F79E14 /* FMStatementKeywordRecogniser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FMStatementKeywordRecogniser.m; sourceTree = "<group>"; };
+		42C753F218F2D2A400F79E14 /* FMStatementQuotedRecogniser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FMStatementQuotedRecogniser.h; sourceTree = "<group>"; };
+		42C753F318F2D2A400F79E14 /* FMStatementQuotedRecogniser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FMStatementQuotedRecogniser.m; sourceTree = "<group>"; };
+		42C753F418F2D2A400F79E14 /* FMStatementTokenRecogniser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FMStatementTokenRecogniser.h; sourceTree = "<group>"; };
 		6290CBB5188FE836009790F8 /* libFMDB-IOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libFMDB-IOS.a"; sourceTree = BUILT_PRODUCTS_DIR; };
 		6290CBB6188FE836009790F8 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
 		6290CBC6188FE837009790F8 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; };
@@ -208,6 +231,21 @@
 			name = Products;
 			sourceTree = "<group>";
 		};
+		42C753ED18F2D2A400F79E14 /* FMStatementSplitter */ = {
+			isa = PBXGroup;
+			children = (
+				42C753EE18F2D2A400F79E14 /* FMSQLStatementSplitter.h */,
+				42C753EF18F2D2A400F79E14 /* FMSQLStatementSplitter.m */,
+				42C753F018F2D2A400F79E14 /* FMStatementKeywordRecogniser.h */,
+				42C753F118F2D2A400F79E14 /* FMStatementKeywordRecogniser.m */,
+				42C753F218F2D2A400F79E14 /* FMStatementQuotedRecogniser.h */,
+				42C753F318F2D2A400F79E14 /* FMStatementQuotedRecogniser.m */,
+				42C753F418F2D2A400F79E14 /* FMStatementTokenRecogniser.h */,
+			);
+			name = FMStatementSplitter;
+			path = src/fmdb/FMStatementSplitter;
+			sourceTree = "<group>";
+		};
 		8314AF3018CD737D00EC0E25 /* fmdb */ = {
 			isa = PBXGroup;
 			children = (
@@ -222,6 +260,7 @@
 				CC50F2CB0DF9183600E4AAAE /* FMDatabaseAdditions.m */,
 				CC9E4EB713B31188005F9210 /* FMDatabasePool.h */,
 				CC9E4EB813B31188005F9210 /* FMDatabasePool.m */,
+				42C753ED18F2D2A400F79E14 /* FMStatementSplitter */,
 			);
 			name = fmdb;
 			sourceTree = "<group>";
@@ -284,11 +323,15 @@
 			isa = PBXHeadersBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				42C753FF18F2D2A400F79E14 /* FMStatementQuotedRecogniser.h in Headers */,
 				EE42910712B42FC90088BD94 /* FMDatabase.h in Headers */,
+				42C753F518F2D2A400F79E14 /* FMSQLStatementSplitter.h in Headers */,
+				42C753FA18F2D2A400F79E14 /* FMStatementKeywordRecogniser.h in Headers */,
 				EE42910612B42FC30088BD94 /* FMDatabaseAdditions.h in Headers */,
 				EE42910912B42FD00088BD94 /* FMResultSet.h in Headers */,
 				8314AF3318CD73D600EC0E25 /* FMDB.h in Headers */,
 				CC9E4EBA13B31188005F9210 /* FMDatabasePool.h in Headers */,
+				42C7540418F2D2A400F79E14 /* FMStatementTokenRecogniser.h in Headers */,
 				CC47A00F148581E9002CCDAB /* FMDatabaseQueue.h in Headers */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -420,8 +463,11 @@
 			files = (
 				621721B31892BFE30006691F /* FMResultSet.m in Sources */,
 				621721B21892BFE30006691F /* FMDatabase.m in Sources */,
+				42C753FE18F2D2A400F79E14 /* FMStatementKeywordRecogniser.m in Sources */,
+				42C7540318F2D2A400F79E14 /* FMStatementQuotedRecogniser.m in Sources */,
 				621721B61892BFE30006691F /* FMDatabasePool.m in Sources */,
 				621721B41892BFE30006691F /* FMDatabaseQueue.m in Sources */,
+				42C753F918F2D2A400F79E14 /* FMSQLStatementSplitter.m in Sources */,
 				621721B51892BFE30006691F /* FMDatabaseAdditions.m in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -433,9 +479,12 @@
 				CCC24EC20A13E34D00A6D3E3 /* FMDatabase.m in Sources */,
 				CCC24EC50A13E34D00A6D3E3 /* main.m in Sources */,
 				CCC24EC70A13E34D00A6D3E3 /* FMResultSet.m in Sources */,
+				42C7540018F2D2A400F79E14 /* FMStatementQuotedRecogniser.m in Sources */,
 				CC50F2CD0DF9183600E4AAAE /* FMDatabaseAdditions.m in Sources */,
+				42C753F618F2D2A400F79E14 /* FMSQLStatementSplitter.m in Sources */,
 				CC9E4EB913B31188005F9210 /* FMDatabasePool.m in Sources */,
 				CC47A010148581E9002CCDAB /* FMDatabaseQueue.m in Sources */,
+				42C753FB18F2D2A400F79E14 /* FMStatementKeywordRecogniser.m in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -446,9 +495,12 @@
 				BFC152B118417F0D00605DF7 /* FMDatabaseAdditions.m in Sources */,
 				BF940F5C18417D490001E077 /* FMDBTempDBTests.m in Sources */,
 				BF940F5E18417DEA0001E077 /* FMDatabaseAdditionsTests.m in Sources */,
+				42C7540218F2D2A400F79E14 /* FMStatementQuotedRecogniser.m in Sources */,
 				BF5D042118416BB2008C5AA9 /* FMDatabaseTests.m in Sources */,
+				42C753F818F2D2A400F79E14 /* FMSQLStatementSplitter.m in Sources */,
 				BFE55E131841C9A000CB3A63 /* FMDatabasePoolTests.m in Sources */,
 				BFE55E151841D38800CB3A63 /* FMDatabaseQueueTests.m in Sources */,
+				42C753FD18F2D2A400F79E14 /* FMStatementKeywordRecogniser.m in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -458,8 +510,11 @@
 			files = (
 				EE42910812B42FCC0088BD94 /* FMDatabase.m in Sources */,
 				EE42910512B42FBC0088BD94 /* FMDatabaseAdditions.m in Sources */,
+				42C753FC18F2D2A400F79E14 /* FMStatementKeywordRecogniser.m in Sources */,
+				42C7540118F2D2A400F79E14 /* FMStatementQuotedRecogniser.m in Sources */,
 				EE42910A12B42FD20088BD94 /* FMResultSet.m in Sources */,
 				CC9E4EBB13B31188005F9210 /* FMDatabasePool.m in Sources */,
+				42C753F718F2D2A400F79E14 /* FMSQLStatementSplitter.m in Sources */,
 				CC47A011148581E9002CCDAB /* FMDatabaseQueue.m in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;

+ 21 - 0
src/fmdb/FMStatementSplitter/FMSQLStatementSplitter.h

@@ -0,0 +1,21 @@
+//
+//  FMSQLStatementSplitter.h
+//  FMDB
+//
+//  Created by openthread on 3/5/14.
+//  Copyright (c) 2014 openthread. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+@interface FMSplittedStatement : NSObject
+@property (nonatomic, retain) NSString *statementString;//statement string
+@end
+
+@interface FMSQLStatementSplitter : NSObject
+
++ (instancetype)sharedInstance;
+
+- (NSArray *)statementsFromBatchSqlStatement:(NSString *)batchStatement;
+
+@end

+ 186 - 0
src/fmdb/FMStatementSplitter/FMSQLStatementSplitter.m

@@ -0,0 +1,186 @@
+//
+//  FMSQLStatementSplitter.m
+//  FMDB
+//
+//  Created by openthread on 3/5/14.
+//  Copyright (c) 2014 openthread. All rights reserved.
+//
+
+#import "FMSQLStatementSplitter.h"
+#import "FMStatementKeywordRecogniser.h"
+#import "FMStatementQuotedRecogniser.h"
+#import "FMDatabase.h"
+
+@implementation FMSplittedStatement
+
+- (NSString *)description
+{
+    NSString *description = [super description];
+    description = [description stringByAppendingFormat:@" %@", self.statementString];
+    return description;
+}
+
+@end
+
+@implementation FMSQLStatementSplitter
+{
+    NSMutableArray *_tokenRecognisers;
+}
+
+- (id)init
+{
+    self = [super init];
+    if (self)
+    {
+        _tokenRecognisers = [NSMutableArray array];
+        FMDBRetain(_tokenRecognisers);
+        
+        //' quote
+        FMStatementQuotedRecogniser *singleQuoteRecogniser = nil;
+        singleQuoteRecogniser = [FMStatementQuotedRecogniser quotedRecogniserWithStartQuote:@"'"
+                                                                         endQuote:@"'"
+                                                                   escapeSequence:@"\\"
+                                                                             name:@"SingleQuote"];
+        singleQuoteRecogniser.shouldQuoteEscapeSequence = YES;
+        [_tokenRecognisers addObject:singleQuoteRecogniser];
+        
+        //" quote
+        FMStatementQuotedRecogniser *doubleQuoteRecogniser = nil;
+        doubleQuoteRecogniser = [FMStatementQuotedRecogniser quotedRecogniserWithStartQuote:@"\""
+                                                                     endQuote:@"\""
+                                                               escapeSequence:@"\\"
+                                                                         name:@"DoubleQuote"];
+        
+        doubleQuoteRecogniser.shouldQuoteEscapeSequence = NO;
+        [_tokenRecognisers addObject:doubleQuoteRecogniser];
+        
+        //` quote
+        FMStatementQuotedRecogniser *sqlashQuoteRecogniser = nil;
+        sqlashQuoteRecogniser = [FMStatementQuotedRecogniser quotedRecogniserWithStartQuote:@"`"
+                                                                          endQuote:@"`"
+                                                                    escapeSequence:@"\\"
+                                                                              name:@"SqlashQuote"];
+        sqlashQuoteRecogniser.shouldQuoteEscapeSequence = NO;
+        [_tokenRecognisers addObject:sqlashQuoteRecogniser];
+        
+        //; recognizer
+        NSArray *operatorKeywords = @[@";"];
+        [_tokenRecognisers addObject:[FMStatementKeywordRecogniser recogniserForKeywords:operatorKeywords]];
+    }
+    return self;
+}
+
+- (void)dealloc
+{
+    FMDBRelease(_tokenRecognisers);
+#if ! __has_feature(objc_arc)
+    [super dealloc];
+#endif
+}
+
++ (instancetype)sharedInstance
+{
+    static id instance;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        instance = [[self alloc] init];
+    });
+    return instance;
+}
+
+
+- (NSArray *)statementsFromBatchSqlStatement:(NSString *)input;
+{
+    NSUInteger currentTokenOffset = 0;
+    NSUInteger inputLength = [input length];
+    NSArray *recs = _tokenRecognisers;
+    NSScanner *scanner = [NSScanner scannerWithString:input];
+
+    NSMutableArray *resultArray = [NSMutableArray array];
+    NSUInteger lastSplitterLocation = 0;
+
+    while (currentTokenOffset < inputLength)
+    {
+        @autoreleasepool
+        {
+            BOOL recognised = NO;
+            for (NSUInteger i = 0; i < recs.count; i++)
+            {
+                id<FMStatementTokenRecogniser> recogniser = recs[i];
+                NSRange range = [recogniser recogniseRangeWithScanner:scanner currentTokenPosition:&currentTokenOffset];
+                if (NSNotFound != range.location)
+                {
+                    if (i == 3)//Recognised ; keyword
+                    {
+                        FMSplittedStatement *statement = [[FMSplittedStatement alloc] init];
+                        statement.statementString = [input substringWithRange:NSMakeRange(lastSplitterLocation, currentTokenOffset - lastSplitterLocation)];
+                        lastSplitterLocation = currentTokenOffset;
+                        [resultArray addObject:statement];
+                        FMDBRelease(statement);
+                   }
+                    recognised = YES;
+                    break;
+                }
+            }
+            
+            if (!recognised)
+            {
+                currentTokenOffset ++;
+            }
+            
+            if (currentTokenOffset == inputLength && lastSplitterLocation != inputLength)
+                //input comes to end, put all string remaining to the last statement
+            {
+                FMSplittedStatement *statement = [[FMSplittedStatement alloc] init];
+                statement.statementString = [input substringWithRange:NSMakeRange(lastSplitterLocation, currentTokenOffset - lastSplitterLocation)];
+                lastSplitterLocation = currentTokenOffset;
+                [resultArray addObject:statement];
+                FMDBRelease(statement);
+            }
+        }
+    }
+    
+    return [NSArray arrayWithArray:resultArray];
+}
+
++ (void)test
+{
+    NSArray *statementStringArray = @[
+    @"SELECT TABLE IF EXISTS ';' `web_offline_track`;",
+    @"select TABLE IF NOT EXISTS \";\" `web_offline_track` (`id` VARCHAR(40) NOT NULL, `type` INT NULL, `type_extra` BIGINT NULL, `track_id` BIGINT NULL, `detail` TEXT NULL, `size` INT NULL, `dfsid` BIGINT NULL, `bitrate` INT NULL, `state` INT NULL, `download_time` INT NULL, `complete_time` INT NULL, `sou;rce_href` TEXT NULL, `source_text` TEXT NULL, `source_extra` TEXT NULL, `album_id` VARCHAR(40) NULL, `relative_path` TEXT NULL, `track_name` TEXT NULL, `artist_name` TEXT NULL, `album_name` TEXT NULL, PRIMARY KEY (`id`));",
+    @"create TABLE select IF EXISTS `web_playl;ist_order`;",
+    @"select TABLE IF NOT EXISTS `web_playlist_order` (`playlist_id` BIGINT NOT NULL, `field` VARCHAR(40) NULL, `order` VARCHAR(40) NULL, PRIMARY KEY select (`playlist_SELECTid`));",
+    @"'\\\\';",
+    @"'blah blah"];
+    
+    NSMutableString *batchStatement = [NSMutableString string];
+    for (NSString *str in statementStringArray)
+    {
+        [batchStatement appendString:str];
+    }
+    
+    //Result
+    NSArray *statements = [[FMSQLStatementSplitter sharedInstance] statementsFromBatchSqlStatement:batchStatement];
+    NSLog(@"%@ test with parsed result: %@",[super description], statements);
+    
+    //counts
+    NSLog(@"statement count :%lu expected %lu.",
+          (unsigned long)statements.count,
+          (unsigned long)statementStringArray.count);
+    
+    //single statement
+    for (NSUInteger i = 0; i<statementStringArray.count && i<statements.count; i++)
+    {
+        NSString *originalString = statementStringArray[i];
+        FMSplittedStatement *statement = statements[i];
+        NSLog(@"statement check successed : %d", [originalString isEqualToString:statement.statementString]);
+    }
+}
+
+//+ (void)load
+//{
+//    [super load];
+//    [self test];
+//}
+
+@end

+ 97 - 0
src/fmdb/FMStatementSplitter/FMStatementKeywordRecogniser.h

@@ -0,0 +1,97 @@
+//
+//  FMStatementKeywordRecogniser.h
+//  FMDB
+//
+//  Created by openthread on 3/5/14.
+//  Copyright (c) 2014 openthread. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+#import "FMStatementTokenRecogniser.h"
+
+/**
+ * The FMStatementKeywordRecogniser class attempts to recognise a specific keyword in a token stream.
+ * 
+ * A keyword recogniser attempts to recognise a specific word or set of symbols.
+ * Keyword recognisers can also check that the keyword is not followed by specific characters in order to stop it recognising the beginnings of words.
+ */
+@interface FMStatementKeywordRecogniser : NSObject <FMStatementTokenRecogniser>
+
+///---------------------------------------------------------------------------------------
+/// @name Creating and Initialising a Keyword Recogniser
+///---------------------------------------------------------------------------------------
+
+/**
+ * Creates a Keyword Recogniser for a specific keyword.
+ * 
+ * @param keyword The keyword to recognise.
+ *
+ * @return Returns a keyword recogniser for the passed keyword.
+ *
+ * @see initWithKeyword:
+ * @see recogniserForKeyword:invalidFollowingCharacters:
+ */
++ (id)recogniserForKeyword:(NSString *)keyword;
+
++ (id)recogniserForKeywords:(NSArray *)keywords;
+
+/**
+ * Creates a Keyword Recogniser for a specific keyword.
+ * 
+ * @param keyword The keyword to recognise.
+ * @param invalidFollowingCharacters A set of characters that may not follow the keyword in the string being tokenised.
+ *
+ * @return Returns a keyword recogniser for the passed keyword.
+ *
+ * @see recogniserForKeyword:
+ * @see initWithKeyword:invalidFollowingCharacters:
+ */
++ (id)recogniserForKeyword:(NSString *)keyword invalidFollowingCharacters:(NSCharacterSet *)invalidFollowingCharacters;
+
++ (id)recogniserForKeywords:(NSArray *)keywords invalidFollowingCharacters:(NSCharacterSet *)invalidFollowingCharacters;
+
+/**
+ * Initialises a Keyword Recogniser to recognise a specific keyword.
+ * 
+ * @param keyword The keyword to recognise.
+ *
+ * @return Returns the keyword recogniser initialised to recognise the passed keyword.
+ *
+ * @see recogniserForKeyword:
+ * @see initWithKeyword:invalidFollowingCharacters:
+ */
+- (id)initWithKeyword:(NSString *)keyword;
+
+- (id)initWithKeywords:(NSArray *)keywords;
+
+/**
+ * Initialises a Keyword Recogniser to recognise a specific keyword.
+ * 
+ * @param keyword The keyword to recognise.
+ * @param invalidFollowingCharacters A set of characters that may not follow the keyword in the string being tokenised.
+ *
+ * @return Returns the keyword recogniser initialised to recognise the passed keyword.
+ *
+ * @see initWithKeyword:
+ * @see recogniserForKeyword:invalidFollowingCharacters:
+ */
+- (id)initWithKeyword:(NSString *)keyword invalidFollowingCharacters:(NSCharacterSet *)invalidFollowingCharacters;
+
+- (id)initWithKeywords:(NSArray *)keywords invalidFollowingCharacters:(NSCharacterSet *)invalidFollowingCharacters;
+
+///---------------------------------------------------------------------------------------
+/// @name Configuring a Keyword Recogniser
+///---------------------------------------------------------------------------------------
+
+/**
+ * The keyword that the recogniser should attempt to recognise.
+ */
+@property (readwrite,retain,nonatomic) NSArray *keywords;
+
+/**
+ * A set of characters that may not follow the keyword.
+ */
+@property (readwrite,retain,nonatomic) NSCharacterSet *invalidFollowingCharacters;
+
+@end

+ 118 - 0
src/fmdb/FMStatementSplitter/FMStatementKeywordRecogniser.m

@@ -0,0 +1,118 @@
+//
+//  FMStatementKeywordRecogniser.m
+//  FMDB
+//
+//  Created by openthread on 3/5/14.
+//  Copyright (c) 2014 openthread. All rights reserved.
+//
+
+#import "FMStatementKeywordRecogniser.h"
+#import "FMDatabase.h"
+
+@implementation FMStatementKeywordRecogniser
+
+@synthesize keywords = _keywords;
+@synthesize invalidFollowingCharacters = _invalidFollowingCharacters;
+
++ (id)recogniserForKeyword:(NSString *)keyword
+{
+    return [self recogniserForKeywords:@[keyword]];
+}
+
++ (id)recogniserForKeywords:(NSArray *)keywords
+{
+    return FMDBReturnAutoreleased([[self alloc] initWithKeywords:keywords]);
+}
+
+- (id)initWithKeyword:(NSString *)keyword
+{
+    return [self initWithKeywords:@[keyword]];
+}
+
+- (id)initWithKeywords:(NSArray *)keywords
+{
+    return [self initWithKeywords:keywords invalidFollowingCharacters:nil];
+}
+
++ (id)recogniserForKeyword:(NSString *)keyword invalidFollowingCharacters:(NSCharacterSet *)invalidFollowingCharacters
+{
+    return [self recogniserForKeywords:@[keyword]
+            invalidFollowingCharacters:invalidFollowingCharacters];
+}
+
++ (id)recogniserForKeywords:(NSArray *)keywords invalidFollowingCharacters:(NSCharacterSet *)invalidFollowingCharacters
+{
+    return FMDBReturnAutoreleased([[self alloc] initWithKeywords:keywords invalidFollowingCharacters:invalidFollowingCharacters]);
+}
+
+- (id)initWithKeyword:(NSString *)keyword invalidFollowingCharacters:(NSCharacterSet *)invalidFollowingCharacters
+{
+    return [self initWithKeywords:@[keyword] invalidFollowingCharacters:invalidFollowingCharacters];
+}
+
+- (id)initWithKeywords:(NSArray *)keywords invalidFollowingCharacters:(NSCharacterSet *)invalidFollowingCharacters
+{
+    self = [super init];
+    
+    if (nil != self)
+    {
+        self.keywords = keywords;
+        [self setInvalidFollowingCharacters:invalidFollowingCharacters];
+    }
+    
+    return self;
+}
+
+- (id)init
+{
+    return [self initWithKeyword:@" "];
+}
+
+#define CPKeywordRecogniserKeywordKey @"K.k"
+#define CPKeywordRecogniserInvalidFollowingCharactersKey @"K.f"
+
+- (id)initWithCoder:(NSCoder *)aDecoder
+{
+    self = [super init];
+    
+    if (nil != self)
+    {
+        [self setKeywords:[aDecoder decodeObjectForKey:CPKeywordRecogniserKeywordKey]];
+        [self setInvalidFollowingCharacters:[aDecoder decodeObjectForKey:CPKeywordRecogniserInvalidFollowingCharactersKey]];
+    }
+    
+    return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder
+{
+    [aCoder encodeObject:[self keywords] forKey:CPKeywordRecogniserKeywordKey];
+    [aCoder encodeObject:[self invalidFollowingCharacters] forKey:CPKeywordRecogniserInvalidFollowingCharactersKey];
+}
+
+- (NSRange)recogniseRangeWithScanner:(NSScanner *)scanner currentTokenPosition:(NSUInteger *)tokenPosition
+{
+    for (NSString *keyword in self.keywords)
+    {
+        NSUInteger kwLength = [keyword length];
+        NSUInteger remainingChars = [[scanner string] length] - *tokenPosition;
+        if (remainingChars >= kwLength)
+        {
+            if (CFStringFindWithOptions((CFStringRef)[scanner string], (CFStringRef)keyword, CFRangeMake(*tokenPosition, kwLength), kCFCompareAnchored | kCFCompareCaseInsensitive, NULL))
+            {
+                if (remainingChars == kwLength ||
+                    nil == self.invalidFollowingCharacters ||
+                    !CFStringFindCharacterFromSet((CFStringRef)[scanner string], (CFCharacterSetRef)self.invalidFollowingCharacters, CFRangeMake(*tokenPosition + kwLength, 1), kCFCompareAnchored, NULL))
+                {
+                    NSRange result = NSMakeRange(*tokenPosition, kwLength);
+                    *tokenPosition = *tokenPosition + kwLength;
+                    return result;
+                }
+            }
+        }
+    }
+    
+    return NSMakeRange(NSNotFound, 0);
+}
+
+@end

+ 130 - 0
src/fmdb/FMStatementSplitter/FMStatementQuotedRecogniser.h

@@ -0,0 +1,130 @@
+//
+//  FMStatementQuotedRecogniser.h
+//  FMDB
+//
+//  Created by openthread on 3/5/14.
+//  Copyright (c) 2014 openthread. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+#import "FMStatementTokenRecogniser.h"
+
+/**
+ * The FMStatementQuotedRecogniser class is used to recognise quoted literals in the input string.  This can be used for quoted strings, characters, comments and many other things.
+ * 
+ * Quoted tokens are recognised via a start string and end string.  You may optionally add an escape sequence string that stops the end quote being recognised at that point in the input.
+ * You may optionally provide a block used to replace escape sequences with their actual meaning.  If you don't provide an escape replcement block it is assumed that the character
+ * following the escape sequence replaces the whole sequence.
+ *
+ * Finally, you may also provide a maximum length for the quoted sequence to recognise.  If you want to recognise strings of any length, pass NSNotFound.
+ */
+@interface FMStatementQuotedRecogniser : NSObject <FMStatementTokenRecogniser>
+
+///---------------------------------------------------------------------------------------
+/// @name Creating and Initialising a Quoted Recogniser
+///---------------------------------------------------------------------------------------
+
+/**
+ * Creates a quoted recogniser that recognises quoted litterals starting with startQuote and ending with endQuote.
+ *
+ * @param startQuote A string that indicates the beginning of a quoted literal.
+ * @param endQuote   A string that indicates the end of the quoted literal.
+ * @param name       The name to attach to recognised tokens.
+ * @return Returns a FMStatementQuotedRecogniser that recognises C like identifiers.
+ *
+ * @see quotedRecogniserWithStartQuote:endQuote:escapeSequence:name:
+ * @see quotedRecogniserWithStartQuote:endQuote:escapeSequence:maximumLength:name:
+ */
++ (id)quotedRecogniserWithStartQuote:(NSString *)startQuote endQuote:(NSString *)endQuote name:(NSString *)name;
+
+/**
+ * Creates a quoted recogniser that recognises quoted litterals starting with startQuote and ending with endQuote.  Escaped sequences are recognised by the escapeSequence string.
+ *
+ * @param startQuote     A string that indicates the beginning of a quoted literal.
+ * @param endQuote       A string that indicates the end of the quoted literal.
+ * @param escapeSequence A string that indicates an escaped character.
+ * @param name           The name to attach to recognised tokens.
+ * @return Returns a FMStatementQuotedRecogniser that recognises C like identifiers.
+ *
+ * @see quotedRecogniserWithStartQuote:endQuote:name:
+ * @see quotedRecogniserWithStartQuote:endQuote:escapeSequence:maximumLength:name:
+ */
++ (id)quotedRecogniserWithStartQuote:(NSString *)startQuote endQuote:(NSString *)endQuote escapeSequence:(NSString *)escapeSequence name:(NSString *)name;
+
+/**
+ * Creates a quoted recogniser that recognises quoted litterals starting with startQuote and ending with endQuote.  Escaped sequences are recognised by the escapeSequence string.  Quoted strings have a maximum length.
+ *
+ * @param startQuote     A string that indicates the beginning of a quoted literal.
+ * @param endQuote       A string that indicates the end of the quoted literal.
+ * @param escapeSequence A string that indicates an escaped character.
+ * @param maximumLength  The maximum length of the resulting string.
+ * @param name           The name to attach to recognised tokens.
+ * @return Returns a FMStatementQuotedRecogniser that recognises C like identifiers.
+ *
+ * @see quotedRecogniserWithStartQuote:endQuote:name:
+ * @see quotedRecogniserWithStartQuote:endQuote:escapeSequence:name:
+ * @see initWithStartQuote:endQuote:escapeSequence:maximumLength:name:
+ */
++ (id)quotedRecogniserWithStartQuote:(NSString *)startQuote endQuote:(NSString *)endQuote escapeSequence:(NSString *)escapeSequence maximumLength:(NSUInteger)maximumLength name:(NSString *)name;
+
+/**
+ * Initialises a quoted recogniser that recognises quoted litterals starting with startQuote and ending with endQuote.  Escaped sequences are recognised by the escapeSequence string.  Quoted strings have a maximum length.
+ *
+ * @param startQuote     A string that indicates the beginning of a quoted literal.
+ * @param endQuote       A string that indicates the end of the quoted literal.
+ * @param escapeSequence A string that indicates an escaped character.
+ * @param maximumLength  The maximum length of the resulting string.
+ * @param name           The name to attach to recognised tokens.
+ * @return Returns a FMStatementQuotedRecogniser that recognises C like identifiers.
+ *
+ * @see quotedRecogniserWithStartQuote:endQuote:escapeSequence:maximumLength:name:
+ */
+- (id)initWithStartQuote:(NSString *)startQuote endQuote:(NSString *)endQuote escapeSequence:(NSString *)escapeSequence maximumLength:(NSUInteger)maximumLength name:(NSString *)name;
+
+///---------------------------------------------------------------------------------------
+/// @name Configuring a Quoted Recogniser
+///---------------------------------------------------------------------------------------
+
+/**
+ * Determines the string used to indicate the start of the quoted literal.
+ *
+ * @see endQuote
+ */
+@property (readwrite,copy) NSString *startQuote;
+
+/**
+ * Determines the string used to indicate the end of the quoted literal.
+ *
+ * @see startQuote
+ */
+@property (readwrite,copy) NSString *endQuote;
+
+/**
+ * Determines the string used to indicate an escaped character in the quoted literal.
+ */
+@property (readwrite,copy) NSString *escapeSequence;
+
+/**
+ * If `YES`, quoted string will contains `escapeSequence`.
+ * If `NO`, quoted string will not contains `escapeSequence`.
+ * Default is `NO`.
+ */
+@property (nonatomic, assign) BOOL shouldQuoteEscapeSequence;
+
+/**
+ * Determines how much of the input string to consume when an escaped literal is found, and what to replace it with.
+ */
+@property (readwrite,copy) NSString *(^escapeReplacer)(NSString *tokenStream, NSUInteger *quotePosition);
+
+/**
+ * Determines the maximum length of the quoted literal not including quotes.  To indicate the literal can be any length specify NSNotFound.
+ */
+@property (readwrite,assign) NSUInteger maximumLength;
+
+/**
+ * Determines the name of the token produced.
+ */
+@property (readwrite,copy) NSString *name;
+
+@end

+ 180 - 0
src/fmdb/FMStatementSplitter/FMStatementQuotedRecogniser.m

@@ -0,0 +1,180 @@
+//
+//  FMStatementQuotedRecogniser.m
+//  FMDB
+//
+//  Created by openthread on 3/5/14.
+//  Copyright (c) 2014 openthread. All rights reserved.
+//
+
+#import "FMStatementQuotedRecogniser.h"
+#import "FMDatabase.h"
+
+@implementation FMStatementQuotedRecogniser
+
+@synthesize startQuote = _startQuote;
+@synthesize endQuote = _endQuote;
+@synthesize escapeSequence = _escapeSequence;
+@synthesize escapeReplacer = _escapeReplacer;
+@synthesize maximumLength = _maximumLength;
+@synthesize name = _name;
+
++ (NSUInteger)minWithLeftParam:(NSUInteger)left rightParam:(NSUInteger)right
+{
+    return (left < right ? left : right);
+}
+
++ (id)quotedRecogniserWithStartQuote:(NSString *)startQuote endQuote:(NSString *)endQuote name:(NSString *)name
+{
+    return [FMStatementQuotedRecogniser quotedRecogniserWithStartQuote:startQuote endQuote:endQuote escapeSequence:nil name:name];
+}
+
++ (id)quotedRecogniserWithStartQuote:(NSString *)startQuote endQuote:(NSString *)endQuote escapeSequence:(NSString *)escapeSequence name:(NSString *)name
+{
+    return [FMStatementQuotedRecogniser quotedRecogniserWithStartQuote:startQuote endQuote:endQuote escapeSequence:escapeSequence maximumLength:NSNotFound name:name];
+}
+
++ (id)quotedRecogniserWithStartQuote:(NSString *)startQuote endQuote:(NSString *)endQuote escapeSequence:(NSString *)escapeSequence maximumLength:(NSUInteger)maximumLength name:(NSString *)name
+{
+    return FMDBReturnAutoreleased([[FMStatementQuotedRecogniser alloc] initWithStartQuote:startQuote endQuote:endQuote escapeSequence:escapeSequence maximumLength:maximumLength name:name]);
+}
+
+- (id)initWithStartQuote:(NSString *)initStartQuote endQuote:(NSString *)initEndQuote escapeSequence:(NSString *)initEscapeSequence maximumLength:(NSUInteger)initMaximumLength name:(NSString *)initName
+{
+    self = [super init];
+    
+    if (nil != self)
+    {
+        [self setStartQuote:initStartQuote];
+        [self setEndQuote:initEndQuote];
+        [self setEscapeSequence:initEscapeSequence];
+        [self setMaximumLength:initMaximumLength];
+        [self setName:initName];
+    }
+    
+    return self;
+}
+
+#define CPQuotedRecogniserStartQuoteKey     @"Q.s"
+#define CPQuotedRecogniserEndQuoteKey       @"Q.e"
+#define CPQuotedRecogniserEscapeSequenceKey @"Q.es"
+#define CPQuotedRecogniserMaximumLengthKey  @"Q.m"
+#define CPQuotedRecogniserNameKey           @"Q.n"
+
+- (id)initWithCoder:(NSCoder *)aDecoder
+{
+    self = [super init];
+    
+    if (nil != self)
+    {
+        [self setStartQuote:[aDecoder decodeObjectForKey:CPQuotedRecogniserStartQuoteKey]];
+        [self setEndQuote:[aDecoder decodeObjectForKey:CPQuotedRecogniserEndQuoteKey]];
+        [self setEscapeSequence:[aDecoder decodeObjectForKey:CPQuotedRecogniserEscapeSequenceKey]];
+        @try
+        {
+            [self setMaximumLength:[aDecoder decodeIntegerForKey:CPQuotedRecogniserMaximumLengthKey]];
+        }
+        @catch (NSException *exception)
+        {
+            NSLog(@"Warning, value for maximum length too long for this platform, allowing infinite lengths");
+            [self setMaximumLength:NSNotFound];
+        }
+        [self setName:[aDecoder decodeObjectForKey:CPQuotedRecogniserNameKey]];
+    }
+    
+    return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder
+{
+    if (nil != [self escapeReplacer])
+    {
+        NSLog(@"Warning: encoding CPQuoteRecogniser with an escapeReplacer set.  This will not be recreated when decoded.");
+    }
+    [aCoder encodeObject:[self startQuote]     forKey:CPQuotedRecogniserStartQuoteKey];
+    [aCoder encodeObject:[self endQuote]       forKey:CPQuotedRecogniserEndQuoteKey];
+    [aCoder encodeObject:[self escapeSequence] forKey:CPQuotedRecogniserEscapeSequenceKey];
+    [aCoder encodeInteger:[self maximumLength] forKey:CPQuotedRecogniserMaximumLengthKey];
+    [aCoder encodeObject:[self name]           forKey:CPQuotedRecogniserNameKey];
+}
+
+- (NSRange)recogniseRangeWithScanner:(NSScanner *)scanner currentTokenPosition:(NSUInteger *)tokenPosition
+{
+    NSString *(^er)(NSString *tokenStream, NSUInteger *quotePosition) = [self escapeReplacer];
+    NSUInteger startQuoteLength = [self.startQuote length];
+    NSUInteger endQuoteLength = [self.endQuote length];
+    NSString *tokenString = [scanner string];
+
+    long inputLength = [tokenString length];
+    NSUInteger rangeLength = [FMStatementQuotedRecogniser minWithLeftParam:inputLength - *tokenPosition
+                                                                rightParam:startQuoteLength + endQuoteLength + self.maximumLength];
+    CFRange searchRange = CFRangeMake(*tokenPosition, rangeLength);
+    CFRange range;
+    BOOL matched = CFStringFindWithOptions((CFStringRef)tokenString, (CFStringRef)self.startQuote, searchRange, kCFCompareAnchored, &range);
+    
+    CFMutableStringRef outputString = CFStringCreateMutable(kCFAllocatorDefault, 0);
+    
+    if (matched)
+    {
+        searchRange.location = searchRange.location + range.length;
+        searchRange.length   = searchRange.length   - range.length;
+        
+        CFRange endRange;
+        CFRange escapeRange;
+        BOOL matchedEndSequence = CFStringFindWithOptions((CFStringRef)tokenString, (CFStringRef)self.endQuote, searchRange, 0L, &endRange);
+        BOOL matchedEscapeSequence = nil == self.escapeSequence ? NO : CFStringFindWithOptions((CFStringRef)tokenString, (CFStringRef)self.escapeSequence, searchRange, 0L, &escapeRange);
+        
+        while (matchedEndSequence && searchRange.location < inputLength)
+        {
+            if (!matchedEscapeSequence || endRange.location < escapeRange.location)//End quote is not escaped by escape sequence.
+            {
+                NSUInteger resultRangeBegin = *tokenPosition;
+                *tokenPosition = endRange.location + endRange.length;
+                NSUInteger resultRangeLength = *tokenPosition - resultRangeBegin;
+                CFRelease(outputString);
+                return NSMakeRange(resultRangeBegin, resultRangeLength);
+            }
+            else//End quote is escaped by escape sequence
+            {
+                NSUInteger quotedPosition = escapeRange.location + escapeRange.length;
+                CFRange subStrRange = CFRangeMake(searchRange.location,
+                                                  escapeRange.location + (self.shouldQuoteEscapeSequence ? escapeRange.length : 0) - searchRange.location);
+                CFStringRef substr = CFStringCreateWithSubstring(kCFAllocatorDefault, (CFStringRef)tokenString, subStrRange);
+                CFStringAppend(outputString, substr);
+                CFRelease(substr);
+                BOOL appended = NO;
+                if (nil != er)
+                {
+                    NSString *s = er(tokenString, &quotedPosition);
+                    if (nil != s)
+                    {
+                        appended = YES;
+                        CFStringAppend(outputString, (CFStringRef)s);
+                    }
+                }
+                if (!appended)
+                {
+                    substr = CFStringCreateWithSubstring(kCFAllocatorDefault, (CFStringRef)tokenString, CFRangeMake(escapeRange.location + escapeRange.length, 1));
+                    CFStringAppend(outputString, substr);
+                    CFRelease(substr);
+                    quotedPosition += 1;
+                }
+                searchRange.length   = searchRange.location + searchRange.length - quotedPosition;
+                searchRange.location = quotedPosition;
+                
+                if (endRange.location < searchRange.location)
+                {
+                    matchedEndSequence = CFStringFindWithOptions((CFStringRef)tokenString, (CFStringRef)self.endQuote, searchRange, 0L, &endRange);
+                }
+                if (escapeRange.location < searchRange.location)
+                {
+                    matchedEscapeSequence = CFStringFindWithOptions((CFStringRef)tokenString, (CFStringRef)self.escapeSequence, searchRange, 0L, &escapeRange);
+                }
+            }
+        }
+    }
+    
+    CFRelease(outputString);
+    return NSMakeRange(NSNotFound, 0);
+}
+
+@end

+ 16 - 0
src/fmdb/FMStatementSplitter/FMStatementTokenRecogniser.h

@@ -0,0 +1,16 @@
+//
+//  FMStatementTokenRecogniser.h
+//  FMDB
+//
+//  Created by openthread on 3/5/14.
+//  Copyright (c) 2014 openthread. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+@protocol FMStatementTokenRecogniser <NSObject, NSCoding>
+
+@required
+- (NSRange)recogniseRangeWithScanner:(NSScanner *)scanner currentTokenPosition:(NSUInteger *)tokenPosition;
+
+@end