diff --git a/libs/SmartStore/SmartStore.xcodeproj/project.pbxproj b/libs/SmartStore/SmartStore.xcodeproj/project.pbxproj index 0604e80c59..b1454c0b17 100644 --- a/libs/SmartStore/SmartStore.xcodeproj/project.pbxproj +++ b/libs/SmartStore/SmartStore.xcodeproj/project.pbxproj @@ -31,6 +31,7 @@ 4F06AFF11C49D53D00F70798 /* SmartStore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CE4CE2B91C0E4581009F6029 /* SmartStore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 4F883C761C16279F007D4BAE /* SalesforceSDKManagerWithSmartStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 4F883C751C16279F007D4BAE /* SalesforceSDKManagerWithSmartStore.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4F883C7B1C1627BD007D4BAE /* SalesforceSDKManagerWithSmartStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F883C7A1C1627BD007D4BAE /* SalesforceSDKManagerWithSmartStore.m */; }; + 4F99B53C1CEA81A6007BC4D2 /* SFSmartStoreLoadTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F99B53B1CEA81A6007BC4D2 /* SFSmartStoreLoadTests.m */; }; 4FEE44851BFD5CCC00F09C43 /* SFSmartSqlTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F9601541BFD33B30022F021 /* SFSmartSqlTests.m */; }; 4FEE44861BFD5CD200F09C43 /* SFSmartStoreAlterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F9601561BFD33B30022F021 /* SFSmartStoreAlterTests.m */; }; 4FEE44871BFD5CD500F09C43 /* SFSmartStoreFullTextSearchSpeedTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F9601581BFD33B30022F021 /* SFSmartStoreFullTextSearchSpeedTests.m */; }; @@ -257,6 +258,8 @@ 4F96FC4E1BFD31EF0022F021 /* SmartStore-Static-iOS-Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = "SmartStore-Static-iOS-Debug.xcconfig"; path = "Configuration/SmartStore-Static-iOS-Debug.xcconfig"; sourceTree = ""; }; 4F96FC4F1BFD31EF0022F021 /* SmartStore-Static-iOS-Release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = "SmartStore-Static-iOS-Release.xcconfig"; path = "Configuration/SmartStore-Static-iOS-Release.xcconfig"; sourceTree = ""; }; 4F96FC501BFD31EF0022F021 /* SmartStore-Test.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = "SmartStore-Test.xcconfig"; path = "Configuration/SmartStore-Test.xcconfig"; sourceTree = ""; }; + 4F99B53B1CEA81A6007BC4D2 /* SFSmartStoreLoadTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SFSmartStoreLoadTests.m; sourceTree = ""; }; + 4F99B5401CEA81C6007BC4D2 /* SFSmartStoreLoadTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SFSmartStoreLoadTests.h; sourceTree = ""; }; 4FFEE6341BFE98B600B7AA8A /* SmartStoreTests-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "SmartStoreTests-Info.plist"; path = "SmartStoreTests/SmartStoreTests-Info.plist"; sourceTree = SOURCE_ROOT; }; 8289177A1C52B705002F9981 /* FMDatabase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FMDatabase.h; path = ../../../../external/fmdb/src/fmdb/FMDatabase.h; sourceTree = ""; }; 8289177B1C52B705002F9981 /* FMDatabase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FMDatabase.m; path = ../../../../external/fmdb/src/fmdb/FMDatabase.m; sourceTree = ""; }; @@ -480,10 +483,12 @@ CEAAAE5E195911E600CBBFE9 /* SmartStoreTests */ = { isa = PBXGroup; children = ( + 4F99B5401CEA81C6007BC4D2 /* SFSmartStoreLoadTests.h */, 4F9601531BFD33B30022F021 /* SFSmartSqlTests.h */, 4F9601541BFD33B30022F021 /* SFSmartSqlTests.m */, 4F9601551BFD33B30022F021 /* SFSmartStoreAlterTests.h */, 4F9601561BFD33B30022F021 /* SFSmartStoreAlterTests.m */, + 4F99B53B1CEA81A6007BC4D2 /* SFSmartStoreLoadTests.m */, 4F9601571BFD33B30022F021 /* SFSmartStoreFullTextSearchSpeedTests.h */, 4F9601581BFD33B30022F021 /* SFSmartStoreFullTextSearchSpeedTests.m */, 4F9601591BFD33B30022F021 /* SFSmartStoreFullTextSearchTests.h */, @@ -884,6 +889,7 @@ 4FEE44891BFD5CDC00F09C43 /* SFSmartStoreTestCase.m in Sources */, 4FEE44851BFD5CCC00F09C43 /* SFSmartSqlTests.m in Sources */, 4FEE44861BFD5CD200F09C43 /* SFSmartStoreAlterTests.m in Sources */, + 4F99B53C1CEA81A6007BC4D2 /* SFSmartStoreLoadTests.m in Sources */, 4FEE44881BFD5CD900F09C43 /* SFSmartStoreFullTextSearchTests.m in Sources */, 4FEE448A1BFD5CE500F09C43 /* SFSmartStoreTests.m in Sources */, 4FEE44871BFD5CD500F09C43 /* SFSmartStoreFullTextSearchSpeedTests.m in Sources */, diff --git a/libs/SmartStore/SmartStore/Classes/SFAlterSoupLongOperation.m b/libs/SmartStore/SmartStore/Classes/SFAlterSoupLongOperation.m index 6f08196b8d..5fb73dadba 100644 --- a/libs/SmartStore/SmartStore/Classes/SFAlterSoupLongOperation.m +++ b/libs/SmartStore/SmartStore/Classes/SFAlterSoupLongOperation.m @@ -219,6 +219,12 @@ - (void) copyTable for (NSString* keptPath in keptPaths) { SFSoupIndex* oldIndexSpec = mapOldSpecs[keptPath]; SFSoupIndex* newIndexSpec = mapNewSpecs[keptPath]; + + if (newIndexSpec.columnType == nil) { + // we are now using json1, there is no column to populate + continue; + } + if ([oldIndexSpec.columnType isEqualToString:newIndexSpec.columnType]) { [oldColumns addObject:oldIndexSpec.columnName]; [newColumns addObject:newIndexSpec.columnName]; diff --git a/libs/SmartStore/SmartStore/Classes/SFSmartSqlHelper.m b/libs/SmartStore/SmartStore/Classes/SFSmartSqlHelper.m index 0f23ee08a0..393988aac3 100644 --- a/libs/SmartStore/SmartStore/Classes/SFSmartSqlHelper.m +++ b/libs/SmartStore/SmartStore/Classes/SFSmartSqlHelper.m @@ -113,6 +113,13 @@ - (NSString*) convertSmartSql:(NSString*)smartSql withStore:(SFSmartStore*) stor } } + // With json1 support, the column name could be an expression of the form json_extract(soup, '$.x.y.z') + // We can't have TABLE_x.json_extract(soup, ...) in the sql query + // Instead we should have json_extract(TABLE_x.soup, ...) + NSError *error = nil; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"(TABLE_[0-9]+)\\.json_extract\\(soup" options:0 error:&error]; + [regex replaceMatchesInString:sql options:0 range:NSMakeRange(0, [sql length]) withTemplate:@"json_extract($1.soup"]; + return sql; } diff --git a/libs/SmartStore/SmartStore/Classes/SFSmartStore.h b/libs/SmartStore/SmartStore/Classes/SFSmartStore.h index 425600cd75..079840a8db 100755 --- a/libs/SmartStore/SmartStore/Classes/SFSmartStore.h +++ b/libs/SmartStore/SmartStore/Classes/SFSmartStore.h @@ -89,6 +89,12 @@ extern NSString *const STATUS_COL; extern NSString *const SOUP_ENTRY_ID; extern NSString *const SOUP_LAST_MODIFIED_DATE; +/* + Support for explain query plan + */ +extern NSString *const EXPLAIN_SQL; +extern NSString *const EXPLAIN_ARGS; +extern NSString *const EXPLAIN_ROWS; @class FMDatabaseQueue; @class SFQuerySpec; @@ -125,6 +131,16 @@ extern NSString *const SOUP_LAST_MODIFIED_DATE; */ @property (nonatomic, strong) SFUserAccount *user; +/** + Flag to cause explain plan to be captured for every query + */ +@property (nonatomic, assign) BOOL captureExplainQueryPlan; + +/** + Dictionary with results of last explain query plan + */ +@property (nonatomic, strong) NSDictionary *lastExplainQueryPlan; + /** Use this method to obtain a shared store instance with a particular name for the current user. diff --git a/libs/SmartStore/SmartStore/Classes/SFSmartStore.m b/libs/SmartStore/SmartStore/Classes/SFSmartStore.m index 8ac10bb781..715fc3c56b 100755 --- a/libs/SmartStore/SmartStore/Classes/SFSmartStore.m +++ b/libs/SmartStore/SmartStore/Classes/SFSmartStore.m @@ -97,13 +97,12 @@ NSString *const SOUP_ENTRY_ID = @"_soupEntryId"; NSString *const SOUP_LAST_MODIFIED_DATE = @"_soupLastModifiedDate"; -@implementation SFSmartStore +// Explain support +NSString *const EXPLAIN_SQL = @"sql"; +NSString *const EXPLAIN_ARGS = @"args"; +NSString *const EXPLAIN_ROWS = @"rows"; -@synthesize storeQueue = _storeQueue; -@synthesize storeName = _storeName; -@synthesize user = _user; -@synthesize isGlobal = _isGlobal; -@synthesize dbMgr = _dbMgr; +@implementation SFSmartStore + (void)initialize { @@ -533,6 +532,26 @@ - (FMResultSet*) executeQueryThrows:(NSString*)sql withDb:(FMDatabase*)db { } - (FMResultSet*) executeQueryThrows:(NSString*)sql withArgumentsInArray:(NSArray*)arguments withDb:(FMDatabase*)db { + if (self.captureExplainQueryPlan) { + NSString* explainSql = [NSString stringWithFormat:@"EXPLAIN QUERY PLAN %@", sql]; + NSMutableDictionary* lastPlan = [NSMutableDictionary new]; + lastPlan[EXPLAIN_SQL] = explainSql; + if (arguments.count > 0) lastPlan[EXPLAIN_ARGS] = arguments; + NSMutableArray* explainRows = [NSMutableArray new]; + + FMResultSet* frs = [db executeQuery:explainSql withArgumentsInArray:arguments]; + while ([frs next]) { + NSMutableDictionary* explainRow = [NSMutableDictionary new]; + for (NSUInteger i=0; i 0) { [self updateTable:soupTableName values:values entryId:entryId idCol:ID_COL withDb:db]; } // fts if (hasFts) { NSMutableDictionary *ftsValues = [NSMutableDictionary dictionary]; - [self projectIndexedPaths:entry values:ftsValues indices:indices typeFilter:kSoupIndexTypeFullText]; + [self projectIndexedPaths:entry values:ftsValues indices:indices typeFilter:kValueExtractedToFtsColumn]; if ([ftsValues count] > 0) { [self updateTable:[NSString stringWithFormat:@"%@_fts", soupTableName] values:ftsValues entryId:entryId idCol:DOCID_COL withDb:db]; } @@ -1645,10 +1673,13 @@ - (BOOL) hasFts:(NSString*)soupName withDb:(FMDatabase *)db #pragma mark - Misc -- (void) projectIndexedPaths:(NSDictionary*)entry values:(NSMutableDictionary*)values indices:(NSArray*)indices typeFilter:(NSString*)typeFilter +- (void) projectIndexedPaths:(NSDictionary*)entry values:(NSMutableDictionary*)values indices:(NSArray*)indices typeFilter:(SFIndexSpecTypeFilterBlock)typeFilter { // build up the set of index column values for this row for (SFSoupIndex *idx in indices) { + if (!typeFilter(idx)) + continue; + id indexColVal = [SFJsonUtils projectIntoJson:entry path:[idx path]];; // values for non-leaf nodes are json-ized if ([indexColVal isKindOfClass:[NSDictionary class]] || [indexColVal isKindOfClass:[NSArray class]]) { @@ -1656,9 +1687,7 @@ - (void) projectIndexedPaths:(NSDictionary*)entry values:(NSMutableDictionary*)v } NSString *colName = [idx columnName]; - if (typeFilter == nil || [typeFilter isEqualToString:idx.indexType]) { - values[colName] = indexColVal != nil ? indexColVal : [NSNull null]; - } + values[colName] = indexColVal != nil ? indexColVal : [NSNull null]; } } diff --git a/libs/SmartStore/SmartStore/Classes/SFSoupIndex.h b/libs/SmartStore/SmartStore/Classes/SFSoupIndex.h index bd43a191f8..049b1042a7 100644 --- a/libs/SmartStore/SmartStore/Classes/SFSoupIndex.h +++ b/libs/SmartStore/SmartStore/Classes/SFSoupIndex.h @@ -30,6 +30,17 @@ extern NSString * const kSoupIndexTypeString; extern NSString * const kSoupIndexTypeInteger; extern NSString * const kSoupIndexTypeFloating; extern NSString * const kSoupIndexTypeFullText; +extern NSString * const kSoupIndexTypeJSON1; + + +/** + * Index types filter + */ +@class SFSoupIndex; +typedef BOOL (^SFIndexSpecTypeFilterBlock)(SFSoupIndex*); +extern SFIndexSpecTypeFilterBlock const kValueExtractedToColumn; +extern SFIndexSpecTypeFilterBlock const kValueExtractedToFtsColumn; +extern SFIndexSpecTypeFilterBlock const kValueIndexedWithJSONExtract; /** * Definition of an index on a given soup. @@ -120,3 +131,5 @@ extern NSString * const kSoupIndexTypeFullText; - (NSString*) getPathType; @end + + diff --git a/libs/SmartStore/SmartStore/Classes/SFSoupIndex.m b/libs/SmartStore/SmartStore/Classes/SFSoupIndex.m index fc008bed01..2a2a1e1ace 100644 --- a/libs/SmartStore/SmartStore/Classes/SFSoupIndex.m +++ b/libs/SmartStore/SmartStore/Classes/SFSoupIndex.m @@ -28,10 +28,15 @@ NSString * const kSoupIndexTypeInteger = @"integer"; NSString * const kSoupIndexTypeFloating = @"floating"; NSString * const kSoupIndexTypeFullText = @"full_text"; +NSString * const kSoupIndexTypeJSON1 = @"json1"; NSString * const kSoupIndexPath = @"path"; NSString * const kSoupIndexType = @"type"; NSString * const kSoupIndexColumnName = @"columnName"; +SFIndexSpecTypeFilterBlock const kValueExtractedToColumn = ^BOOL (SFSoupIndex* idx) { return ![idx.indexType isEqualToString:kSoupIndexTypeJSON1]; }; +SFIndexSpecTypeFilterBlock const kValueExtractedToFtsColumn = ^BOOL (SFSoupIndex* idx) { return [idx.indexType isEqualToString:kSoupIndexTypeFullText]; };; +SFIndexSpecTypeFilterBlock const kValueIndexedWithJSONExtract = ^BOOL (SFSoupIndex* idx) { return [idx.indexType isEqualToString:kSoupIndexTypeJSON1]; };; + @implementation SFSoupIndex @@ -76,6 +81,8 @@ - (NSString*)columnType { result = @"INTEGER"; } else if ([self.indexType isEqualToString:kSoupIndexTypeFloating]) { result = @"REAL"; + } else if ([self.indexType isEqualToString:kSoupIndexTypeJSON1]) { + result = nil; } return result; } diff --git a/libs/SmartStore/SmartStoreTests/SFSmartStoreAlterTests.m b/libs/SmartStore/SmartStoreTests/SFSmartStoreAlterTests.m index efde9b6e68..e9915fbfcc 100644 --- a/libs/SmartStore/SmartStoreTests/SFSmartStoreAlterTests.m +++ b/libs/SmartStore/SmartStoreTests/SFSmartStoreAlterTests.m @@ -156,163 +156,47 @@ -(void) testAlterSoupTypeChangeStringToInteger */ -(void) testAlterSoupTypeChangeStringToFullText { - NSArray* indexSpecs = [SFSoupIndex asArraySoupIndexes:@[@{@"path": kCity, @"type": @"string"}, @{@"path": kCountry, @"type": @"string"}]]; - XCTAssertFalse([self.store soupExists:kTestSoupName], "Test soup should not exists"); - [self.store registerSoup:kTestSoupName withIndexSpecs:indexSpecs error:nil]; - XCTAssertTrue([self.store soupExists:kTestSoupName], "Register soup call failed"); - - NSArray* savedEntries = [self.store upsertEntries:@[@{kCity:@"San Francisco", kCountry:@"United States"}, @{kName:@"Paris", kCountry:@"France"}] - toSoup:kTestSoupName]; - - // Check indices - NSArray* actualIndexSpecs = [self.store indicesForSoup:kTestSoupName]; - [self checkIndexSpecs:actualIndexSpecs withExpectedIndexSpecs:indexSpecs checkColumnName:NO]; - - // Check soup table - [self.store.storeQueue inDatabase:^(FMDatabase *db) { - FMResultSet* frs = [self.store queryTable:kTestSoupTableName forColumns:nil orderBy:@"id ASC" limit:nil whereClause:nil whereArgs:nil withDb:db]; - [self checkSoupRow:frs withExpectedEntry:savedEntries[0] withSoupIndexes:actualIndexSpecs]; - [self checkSoupRow:frs withExpectedEntry:savedEntries[1] withSoupIndexes:actualIndexSpecs]; - XCTAssertFalse([frs next], @"Only two rows should have been returned"); - [frs close]; - }]; - - // Check fts table - XCTAssertFalse([self hasTable:kTestSoupFtsTableName store:self.store], "No fts table expected"); - - // Alter soup - country now full_text - NSArray* indexSpecsNew = [SFSoupIndex asArraySoupIndexes:@[@{@"path": kCity, @"type": @"string"}, @{@"path": kCountry, @"type": @"full_text"}]]; - [self.store alterSoup:kTestSoupName withIndexSpecs:indexSpecsNew reIndexData:YES]; - - // Check indices - NSArray* actualIndexSpecsNew = [self.store indicesForSoup:kTestSoupName]; - [self checkIndexSpecs:actualIndexSpecsNew withExpectedIndexSpecs:indexSpecsNew checkColumnName:NO]; - - // Check soup table - [self.store.storeQueue inDatabase:^(FMDatabase *db) { - FMResultSet* frs = [self.store queryTable:kTestSoupTableName forColumns:nil orderBy:@"id ASC" limit:nil whereClause:nil whereArgs:nil withDb:db]; - [self checkSoupRow:frs withExpectedEntry:savedEntries[0] withSoupIndexes:actualIndexSpecsNew]; - [self checkSoupRow:frs withExpectedEntry:savedEntries[1] withSoupIndexes:actualIndexSpecsNew]; - XCTAssertFalse([frs next], @"Only two rows should have been returned"); - [frs close]; - }]; - - // Check fts table - [self.store.storeQueue inDatabase:^(FMDatabase *db) { - FMResultSet* frs = [self.store queryTable:kTestSoupFtsTableName forColumns:@[DOCID_COL, kCountryCol] orderBy:@"docid ASC" limit:nil whereClause:nil whereArgs:nil withDb:db]; - [self checkFtsRow:frs withExpectedEntry:savedEntries[0] withSoupIndexes:actualIndexSpecsNew]; - [self checkFtsRow:frs withExpectedEntry:savedEntries[1] withSoupIndexes:actualIndexSpecsNew]; - XCTAssertFalse([frs next], @"Only two rows should have been returned"); - [frs close]; - }]; - - // Alter soup - city now full_text - NSArray* indexSpecsNew2 = [SFSoupIndex asArraySoupIndexes:@[@{@"path": kCity, @"type": @"full_text"}, @{@"path": kCountry, @"type": @"full_text"}]]; - [self.store alterSoup:kTestSoupName withIndexSpecs:indexSpecsNew2 reIndexData:YES]; - - // Check indices - NSArray* actualIndexSpecsNew2 = [self.store indicesForSoup:kTestSoupName]; - [self checkIndexSpecs:actualIndexSpecsNew2 withExpectedIndexSpecs:indexSpecsNew2 checkColumnName:NO]; - - // Check soup table - [self.store.storeQueue inDatabase:^(FMDatabase *db) { - FMResultSet* frs = [self.store queryTable:kTestSoupTableName forColumns:nil orderBy:@"id ASC" limit:nil whereClause:nil whereArgs:nil withDb:db]; - [self checkSoupRow:frs withExpectedEntry:savedEntries[0] withSoupIndexes:actualIndexSpecsNew2]; - [self checkSoupRow:frs withExpectedEntry:savedEntries[1] withSoupIndexes:actualIndexSpecsNew2]; - XCTAssertFalse([frs next], @"Only two rows should have been returned"); - [frs close]; - }]; - - // Check fts table - [self.store.storeQueue inDatabase:^(FMDatabase *db) { - FMResultSet* frs = [self.store queryTable:kTestSoupFtsTableName forColumns:@[DOCID_COL, kCityCol, kCountryCol] orderBy:@"docid ASC" limit:nil whereClause:nil whereArgs:nil withDb:db]; - [self checkFtsRow:frs withExpectedEntry:savedEntries[0] withSoupIndexes:actualIndexSpecsNew2]; - [self checkFtsRow:frs withExpectedEntry:savedEntries[1] withSoupIndexes:actualIndexSpecsNew2]; - XCTAssertFalse([frs next], @"Only two rows should have been returned"); - [frs close]; - }]; + [self tryAlterSoupTypeChange:@"string" toType:@"full_text"]; } /** * Test for alterSoup with column type change from full_text to string */ -- (void) testAlterSoupTypeChangeFullTextToString +-(void) testAlterSoupTypeChangeFullTextToString { - NSArray* indexSpecs = [SFSoupIndex asArraySoupIndexes:@[@{@"path": kCity, @"type": @"full_text"}, @{@"path": kCountry, @"type": @"full_text"}]]; - XCTAssertFalse([self.store soupExists:kTestSoupName], "Test soup should not exists"); - [self.store registerSoup:kTestSoupName withIndexSpecs:indexSpecs error:nil]; - XCTAssertTrue([self.store soupExists:kTestSoupName], "Register soup call failed"); - - NSArray* savedEntries = [self.store upsertEntries:@[@{kCity:@"San Francisco", kCountry:@"United States"}, @{kName:@"Paris", kCountry:@"France"}] - toSoup:kTestSoupName]; - - // Check indices - NSArray* actualIndexSpecs = [self.store indicesForSoup:kTestSoupName]; - [self checkIndexSpecs:actualIndexSpecs withExpectedIndexSpecs:indexSpecs checkColumnName:NO]; - - // Check soup table - [self.store.storeQueue inDatabase:^(FMDatabase *db) { - FMResultSet* frs = [self.store queryTable:kTestSoupTableName forColumns:nil orderBy:@"id ASC" limit:nil whereClause:nil whereArgs:nil withDb:db]; - [self checkSoupRow:frs withExpectedEntry:savedEntries[0] withSoupIndexes:actualIndexSpecs]; - [self checkSoupRow:frs withExpectedEntry:savedEntries[1] withSoupIndexes:actualIndexSpecs]; - XCTAssertFalse([frs next], @"Only two rows should have been returned"); - [frs close]; - }]; - - // Check fts table - [self.store.storeQueue inDatabase:^(FMDatabase *db) { - FMResultSet* frs = [self.store queryTable:kTestSoupFtsTableName forColumns:@[DOCID_COL, kCityCol, kCountryCol] orderBy:@"docid ASC" limit:nil whereClause:nil whereArgs:nil withDb:db]; - [self checkFtsRow:frs withExpectedEntry:savedEntries[0] withSoupIndexes:actualIndexSpecs]; - [self checkFtsRow:frs withExpectedEntry:savedEntries[1] withSoupIndexes:actualIndexSpecs]; - XCTAssertFalse([frs next], @"Only two rows should have been returned"); - [frs close]; - }]; - - // Alter soup - country now string - NSArray* indexSpecsNew = [SFSoupIndex asArraySoupIndexes:@[@{@"path": kCity, @"type": @"full_text"}, @{@"path": kCountry, @"type": @"string"}]]; - [self.store alterSoup:kTestSoupName withIndexSpecs:indexSpecsNew reIndexData:YES]; - - // Check indices - NSArray* actualIndexSpecsNew = [self.store indicesForSoup:kTestSoupName]; - [self checkIndexSpecs:actualIndexSpecsNew withExpectedIndexSpecs:indexSpecsNew checkColumnName:NO]; - - // Check soup table - [self.store.storeQueue inDatabase:^(FMDatabase *db) { - FMResultSet* frs = [self.store queryTable:kTestSoupTableName forColumns:nil orderBy:@"id ASC" limit:nil whereClause:nil whereArgs:nil withDb:db]; - [self checkSoupRow:frs withExpectedEntry:savedEntries[0] withSoupIndexes:actualIndexSpecsNew]; - [self checkSoupRow:frs withExpectedEntry:savedEntries[1] withSoupIndexes:actualIndexSpecsNew]; - XCTAssertFalse([frs next], @"Only two rows should have been returned"); - [frs close]; - }]; - - // Check fts table - [self.store.storeQueue inDatabase:^(FMDatabase *db) { - FMResultSet* frs = [self.store queryTable:kTestSoupFtsTableName forColumns:@[DOCID_COL, kCityCol] orderBy:@"docid ASC" limit:nil whereClause:nil whereArgs:nil withDb:db]; - [self checkFtsRow:frs withExpectedEntry:savedEntries[0] withSoupIndexes:actualIndexSpecsNew]; - [self checkFtsRow:frs withExpectedEntry:savedEntries[1] withSoupIndexes:actualIndexSpecsNew]; - XCTAssertFalse([frs next], @"Only two rows should have been returned"); - [frs close]; - }]; - - // Alter soup - city now string - NSArray* indexSpecsNew2 = [SFSoupIndex asArraySoupIndexes:@[@{@"path": kCity, @"type": @"string"}, @{@"path": kCountry, @"type": @"string"}]]; - [self.store alterSoup:kTestSoupName withIndexSpecs:indexSpecsNew2 reIndexData:YES]; - - // Check indices - NSArray* actualIndexSpecsNew2 = [self.store indicesForSoup:kTestSoupName]; - [self checkIndexSpecs:actualIndexSpecsNew2 withExpectedIndexSpecs:indexSpecsNew2 checkColumnName:NO]; - - // Check soup table - [self.store.storeQueue inDatabase:^(FMDatabase *db) { - FMResultSet* frs = [self.store queryTable:kTestSoupTableName forColumns:nil orderBy:@"id ASC" limit:nil whereClause:nil whereArgs:nil withDb:db]; - [self checkSoupRow:frs withExpectedEntry:savedEntries[0] withSoupIndexes:actualIndexSpecsNew2]; - [self checkSoupRow:frs withExpectedEntry:savedEntries[1] withSoupIndexes:actualIndexSpecsNew2]; - XCTAssertFalse([frs next], @"Only two rows should have been returned"); - [frs close]; - }]; - - // Check fts table - XCTAssertFalse([self hasTable:kTestSoupFtsTableName store:self.store], "No fts table expected"); + [self tryAlterSoupTypeChange:@"full_text" toType:@"string"]; +} + +/** + * Test for alterSoup with column type change from string to json1 + */ +-(void) testAlterSoupTypeChangeStringToJSON1 +{ + [self tryAlterSoupTypeChange:@"string" toType:@"json1"]; +} + +/** + * Test for alterSoup with column type change from json1 to string + */ +-(void) testAlterSoupTypeChangeJSON1ToString +{ + [self tryAlterSoupTypeChange:@"json1" toType:@"string"]; +} + +/** + * Test for alterSoup with column type change from full_text to json1 + */ +-(void) testAlterSoupTypeChangeFullTextToJSON1 +{ + [self tryAlterSoupTypeChange:@"full_text" toType:@"json1"]; +} + +/** + * Test for alterSoup with column type change from json1 to full_text + */ +-(void) testAlterSoupTypeChangeJSON1ToFullText +{ + [self tryAlterSoupTypeChange:@"json1" toType:@"full_text"]; } -(void) testAlterSoupResumeAfterRenameOldSoupTable @@ -347,6 +231,84 @@ -(void) testAlterSoupResumeAfterDropOldTable #pragma mark - helper methods +-(void) tryAlterSoupTypeChange:(NSString*)fromType toType:(NSString*)toType +{ + NSArray* indexSpecs = [SFSoupIndex asArraySoupIndexes:@[@{@"path":kCity, @"type":fromType}, @{@"path": kCountry, @"type":fromType}]]; + XCTAssertFalse([self.store soupExists:kTestSoupName], "Test soup should not exists"); + [self.store registerSoup:kTestSoupName withIndexSpecs:indexSpecs error:nil]; + XCTAssertTrue([self.store soupExists:kTestSoupName], "Register soup call failed"); + + NSArray* savedEntries = [self.store upsertEntries:@[@{kCity:@"San Francisco", kCountry:@"United States"}, @{kName:@"Paris", kCountry:@"France"}] + toSoup:kTestSoupName]; + + // Check db + [self checkDb:savedEntries cityColType:fromType countryColType:fromType]; + + // Alter soup - country now full_text + NSArray* indexSpecsNew = [SFSoupIndex asArraySoupIndexes:@[@{@"path":kCity, @"type":fromType}, @{@"path":kCountry, @"type":toType}]]; + [self.store alterSoup:kTestSoupName withIndexSpecs:indexSpecsNew reIndexData:YES]; + + // Check db + [self checkDb:savedEntries cityColType:fromType countryColType:toType]; + + // Alter soup - city now full_text + NSArray* indexSpecsNew2 = [SFSoupIndex asArraySoupIndexes:@[@{@"path":kCity, @"type":toType}, @{@"path":kCountry, @"type":toType}]]; + [self.store alterSoup:kTestSoupName withIndexSpecs:indexSpecsNew2 reIndexData:YES]; + + // Check db + [self checkDb:savedEntries cityColType:toType countryColType:toType]; +} + +-(void) checkDb:(NSArray*)expectedEntries cityColType:(NSString*)cityColType countryColType:(NSString*)countryColType +{ + // Expected column names + NSString* expectedCityCol = ([cityColType isEqualToString:kSoupIndexTypeJSON1] ? [NSString stringWithFormat:@"json_extract(soup, '$.%@')", kCity] : kCityCol); + NSString* expectedCountryCol = ([countryColType isEqualToString:kSoupIndexTypeJSON1] ? [NSString stringWithFormat:@"json_extract(soup, '$.%@')", kCountry] : kCountryCol); + + // Check indices + NSArray* expectedIndexSpecs = [SFSoupIndex asArraySoupIndexes:@[@{@"path": kCity, @"type": cityColType, @"columnName":expectedCityCol}, @{@"path": kCountry, @"type": countryColType, @"columnName":expectedCountryCol}]]; + NSArray* actualIndexSpecs = [self.store indicesForSoup:kTestSoupName]; + [self checkIndexSpecs:actualIndexSpecs withExpectedIndexSpecs:expectedIndexSpecs checkColumnName:YES]; + + // Check soup table columns + NSMutableArray* expectedColumns = [NSMutableArray new]; + [expectedColumns addObject:@"id"]; + [expectedColumns addObject:@"soup"]; + [expectedColumns addObject:@"created"]; + [expectedColumns addObject:@"lastModified"]; + if (![cityColType isEqualToString:kSoupIndexTypeJSON1]) [expectedColumns addObject:kCityCol]; + if (![countryColType isEqualToString:kSoupIndexTypeJSON1]) [expectedColumns addObject:kCountryCol]; + [self checkColumns:kTestSoupTableName expectedColumns:expectedColumns store:self.store]; + + // Check soup table rows + [self.store.storeQueue inDatabase:^(FMDatabase *db) { + FMResultSet* frs = [self.store queryTable:kTestSoupTableName forColumns:nil orderBy:@"id ASC" limit:nil whereClause:nil whereArgs:nil withDb:db]; + [self checkSoupRow:frs withExpectedEntry:expectedEntries[0] withSoupIndexes:actualIndexSpecs]; + [self checkSoupRow:frs withExpectedEntry:expectedEntries[1] withSoupIndexes:actualIndexSpecs]; + XCTAssertFalse([frs next], @"Only two rows should have been returned"); + [frs close]; + }]; + + + if ([kCityCol isEqualToString:kSoupIndexTypeFullText] || [kCountryCol isEqualToString:kSoupIndexTypeFullText]) { + // Check fts table columns + expectedColumns = [NSMutableArray new]; + [expectedColumns addObject:@"docid"]; + if ([cityColType isEqualToString:kSoupIndexTypeFullText]) [expectedColumns addObject:kCityCol]; + if ([countryColType isEqualToString:kSoupIndexTypeFullText]) [expectedColumns addObject:kCountryCol]; + [self checkColumns:kTestSoupFtsTableName expectedColumns:expectedColumns store:self.store]; + + // Check fts table rows + [self.store.storeQueue inDatabase:^(FMDatabase *db) { + FMResultSet* frs = [self.store queryTable:kTestSoupFtsTableName forColumns:expectedColumns orderBy:@"docid ASC" limit:nil whereClause:nil whereArgs:nil withDb:db]; + [self checkFtsRow:frs withExpectedEntry:expectedEntries[0] withSoupIndexes:actualIndexSpecs]; + [self checkFtsRow:frs withExpectedEntry:expectedEntries[1] withSoupIndexes:actualIndexSpecs]; + XCTAssertFalse([frs next], @"Only two rows should have been returned"); + [frs close]; + }]; + } +} + - (void) alterSoupHelper:(BOOL)reIndexData { NSArray* indexSpecs = [SFSoupIndex asArraySoupIndexes:@[@{@"path": kLastName, @"type": @"string"}, @{@"path": kAddressCity, @"type": @"string"}]]; diff --git a/libs/SmartStore/SmartStoreTests/SFSmartStoreLoadTests.h b/libs/SmartStore/SmartStoreTests/SFSmartStoreLoadTests.h new file mode 100644 index 0000000000..7d2ed408dd --- /dev/null +++ b/libs/SmartStore/SmartStoreTests/SFSmartStoreLoadTests.h @@ -0,0 +1,31 @@ +/* + Copyright (c) 2016, salesforce.com, inc. All rights reserved. + + Redistribution and use of this software in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this list of conditions + and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior written + permission of salesforce.com, inc. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +// Logic unit tests contain unit test code that is designed to be linked into an independent test executable. +// See Also: http://developer.apple.com/iphone/library/documentation/Xcode/Conceptual/iphone_development/135-Unit_Testing_Applications/unit_testing_applications.html + +#import "SFSmartStoreTestCase.h" + +@interface SFSmartStoreLoadTests : SFSmartStoreTestCase +@end diff --git a/libs/SmartStore/SmartStoreTests/SFSmartStoreLoadTests.m b/libs/SmartStore/SmartStoreTests/SFSmartStoreLoadTests.m new file mode 100644 index 0000000000..08ffc5643c --- /dev/null +++ b/libs/SmartStore/SmartStoreTests/SFSmartStoreLoadTests.m @@ -0,0 +1,261 @@ +/* + Copyright (c) 2016, salesforce.com, inc. All rights reserved. + + Redistribution and use of this software in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this list of conditions + and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior written + permission of salesforce.com, inc. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#import "SFSmartStoreLoadTests.h" +#import "SFSmartStore+Internal.h" +#import "SFSoupIndex.h" +#import "SFQuerySpec.h" +#import +#import "FMDatabaseQueue.h" +#import "FMDatabase.h" + +@interface SFSmartStoreLoadTests () + +@property (nonatomic, strong) SFUserAccount *smartStoreUser; +@property (nonatomic, strong) SFSmartStore *store; + +@end + +@implementation SFSmartStoreLoadTests + +#define NUMBER_ENTRIES 1000//0 +#define NUMBER_ENTRIES_PER_BATCH 100 +#define MS_IN_S 1000 +#define TEST_SMARTSTORE @"testSmartStore" +#define TEST_SOUP @"testSoup" + +#pragma mark - setup and teardown + + +- (void) setUp +{ + [super setUp]; + [SFLogger setLogLevel:SFLogLevelDebug]; + self.smartStoreUser = [self setUpSmartStoreUser]; + self.store = [SFSmartStore sharedStoreWithName:TEST_SMARTSTORE]; +} + +- (void) tearDown +{ + [SFSmartStore removeSharedStoreWithName:TEST_SMARTSTORE]; + [self tearDownSmartStoreUser:self.smartStoreUser]; + [super tearDown]; + self.smartStoreUser = nil; + self.store = nil; +} + +#pragma mark - tests + +-(void) testUpsertQuery1StringIndex1field20characters +{ + [self tryUpsertQuery:kSoupIndexTypeString numberEntries:NUMBER_ENTRIES numberFieldsPerEntry:1 numberCharactersPerField:20 numberIndexes:1]; +} + +-(void) testUpsertQuery1StringIndex1field1000characters +{ + [self tryUpsertQuery:kSoupIndexTypeString numberEntries:NUMBER_ENTRIES numberFieldsPerEntry:1 numberCharactersPerField:1000 numberIndexes:1]; +} + +-(void) testUpsertQuery1StringIndex10fields20characters +{ + [self tryUpsertQuery:kSoupIndexTypeString numberEntries:NUMBER_ENTRIES numberFieldsPerEntry:10 numberCharactersPerField:20 numberIndexes:1]; +} + +-(void) testUpsertQuery10StringIndexes10fields20characters +{ + [self tryUpsertQuery:kSoupIndexTypeString numberEntries:NUMBER_ENTRIES numberFieldsPerEntry:10 numberCharactersPerField:20 numberIndexes:10]; +} + +-(void) testUpsertQuery1JSON1Index1field20characters +{ + [self tryUpsertQuery:kSoupIndexTypeJSON1 numberEntries:NUMBER_ENTRIES numberFieldsPerEntry:1 numberCharactersPerField:20 numberIndexes:1]; +} + +-(void) testUpsertQuery1JSON1Index1field1000characters +{ + [self tryUpsertQuery:kSoupIndexTypeJSON1 numberEntries:NUMBER_ENTRIES numberFieldsPerEntry:1 numberCharactersPerField:1000 numberIndexes:1]; +} + +-(void) testUpsertQuery1JSON1Index10fields20characters +{ + [self tryUpsertQuery:kSoupIndexTypeJSON1 numberEntries:NUMBER_ENTRIES numberFieldsPerEntry:10 numberCharactersPerField:20 numberIndexes:1]; +} + +-(void)testUpsertQuery10JSON1Indexes10fields20characters +{ + [self tryUpsertQuery:kSoupIndexTypeJSON1 numberEntries:NUMBER_ENTRIES numberFieldsPerEntry:10 numberCharactersPerField:20 numberIndexes:10]; +} + +-(void) testAlterSoupClassicIndexing +{ + [self tryAlterSoup:kSoupIndexTypeString]; +} + +-(void) testAlterSoupJSON1Indexing +{ + [self tryAlterSoup:kSoupIndexTypeJSON1]; +} + + +#pragma mark - helper methods + + +-(void) tryUpsertQuery:(NSString*)indexType + numberEntries:(NSUInteger)numberEntries + numberFieldsPerEntry:(NSUInteger)numberFieldsPerEntry +numberCharactersPerField:(NSUInteger)numberCharactersPerField + numberIndexes:(NSUInteger)numberIndexes +{ + [self setupSoup:TEST_SOUP numberIndexes:numberIndexes indexType:indexType]; + [self upsertEntries:numberEntries / NUMBER_ENTRIES_PER_BATCH numberEntriesPerBatch:NUMBER_ENTRIES_PER_BATCH numberFieldsPerEntry:numberFieldsPerEntry numberCharactersPerField:numberCharactersPerField]; + [self queryEntries]; +} + +-(void) setupSoup:(NSString*)soupName numberIndexes:(NSUInteger)numberIndexes indexType:(NSString*)indexType +{ + NSMutableArray* indexSpecs = [NSMutableArray new]; + for (NSUInteger indexNumber=0; indexNumber %.3f ms", + numberBatches * numberEntriesPerBatch, numberEntriesPerBatch, numberFieldsPerEntry, numberCharactersPerField, avgMilliseconds]; +} + +-(void) queryEntries +{ + // Should find all +// [self queryEntries:[SFQuerySpec newAllQuerySpec:TEST_SOUP withOrderPath:nil withOrder:kSFSoupQuerySortOrderAscending withPageSize:1]]; + [self queryEntries:[SFQuerySpec newAllQuerySpec:TEST_SOUP withOrderPath:nil withOrder:kSFSoupQuerySortOrderAscending withPageSize:10]]; + [self queryEntries:[SFQuerySpec newAllQuerySpec:TEST_SOUP withOrderPath:nil withOrder:kSFSoupQuerySortOrderAscending withPageSize:100]]; + + // Should find 100 + [self queryEntries:[SFQuerySpec newLikeQuerySpec:TEST_SOUP withPath:@"k_0" withLikeKey:@"v_0_%" withOrderPath:nil withOrder:kSFSoupQuerySortOrderAscending withPageSize:1]]; + [self queryEntries:[SFQuerySpec newLikeQuerySpec:TEST_SOUP withPath:@"k_0" withLikeKey:@"v_0_%" withOrderPath:nil withOrder:kSFSoupQuerySortOrderAscending withPageSize:10]]; + [self queryEntries:[SFQuerySpec newLikeQuerySpec:TEST_SOUP withPath:@"k_0" withLikeKey:@"v_0_%" withOrderPath:nil withOrder:kSFSoupQuerySortOrderAscending withPageSize:100]]; + + // Should find 10 + [self queryEntries:[SFQuerySpec newLikeQuerySpec:TEST_SOUP withPath:@"k_0" withLikeKey:@"v_0_0_%" withOrderPath:nil withOrder:kSFSoupQuerySortOrderAscending withPageSize:1]]; + [self queryEntries:[SFQuerySpec newLikeQuerySpec:TEST_SOUP withPath:@"k_0" withLikeKey:@"v_0_0_%" withOrderPath:nil withOrder:kSFSoupQuerySortOrderAscending withPageSize:10]]; + + // Should find none + [self queryEntries:[SFQuerySpec newExactQuerySpec:TEST_SOUP withPath:@"k_0" withMatchKey:@"missing" withOrderPath:nil withOrder:kSFSoupQuerySortOrderAscending withPageSize:1]]; +} + + +-(void) queryEntries:(SFQuerySpec*) querySpec +{ + NSMutableArray* times = [NSMutableArray new]; + NSUInteger countMatches = 0; + BOOL hasMore = YES; + for (NSUInteger pageIndex = 0; hasMore; pageIndex++) { + NSDate* start = [NSDate date]; + + NSError* error = nil; + NSArray* results = [self.store queryWithQuerySpec:querySpec pageIndex:pageIndex error:&error]; + XCTAssertNil(error, @"There should be no errors."); + + NSDate* end = [NSDate date]; + [times addObject:[NSNumber numberWithDouble:[end timeIntervalSinceDate:start]*MS_IN_S]]; + hasMore = (results.count == querySpec.pageSize); + countMatches += results.count; + } + double avgMilliseconds = [self average:times]; + [SFLogger log:SFLogLevelDebug format:@"Querying with %@ query matching %u entries and %u page size: average time per page --> %.3f ms", + [querySpec asDictionary][kQuerySpecParamQueryType], countMatches, querySpec.pageSize, avgMilliseconds]; +} + +-(NSString*) pad:(NSString*)s numberCharacters:(NSUInteger)numberCharacters +{ + NSMutableString* result = [NSMutableString stringWithCapacity:numberCharacters]; + [result appendString:s]; + for (NSUInteger i=s.length; i