Skip to content

Commit 9e320ec

Browse files
committed
fix(datastore): full sync when sync predicate changes
1 parent b9b5918 commit 9e320ec

File tree

13 files changed

+612
-47
lines changed

13 files changed

+612
-47
lines changed

Amplify/Categories/DataStore/Model/Internal/Persistable.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import Foundation
2020
/// - `Temporal.Time`
2121
/// - Warning: Although this has `public` access, it is intended for internal use and should not be used directly
2222
/// by host applications. The behavior of this may change without warning.
23-
public protocol Persistable {}
23+
public protocol Persistable: Encodable {}
2424

2525
extension Bool: Persistable {}
2626
extension Double: Persistable {}

Amplify/Categories/DataStore/Query/QueryOperator.swift

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import Foundation
99

10-
public enum QueryOperator {
10+
public enum QueryOperator: Encodable {
1111
case notEqual(_ value: Persistable?)
1212
case equals(_ value: Persistable?)
1313
case lessOrEqual(_ value: Persistable)
@@ -18,7 +18,7 @@ public enum QueryOperator {
1818
case notContains(_ value: String)
1919
case between(start: Persistable, end: Persistable)
2020
case beginsWith(_ value: String)
21-
21+
2222
public func evaluate(target: Any) -> Bool {
2323
switch self {
2424
case .notEqual(let predicateValue):
@@ -51,4 +51,60 @@ public enum QueryOperator {
5151
}
5252
return false
5353
}
54+
55+
private enum CodingKeys: String, CodingKey {
56+
case type
57+
case value
58+
case start
59+
case end
60+
}
61+
62+
public func encode(to encoder: Encoder) throws {
63+
var container = encoder.container(keyedBy: CodingKeys.self)
64+
65+
switch self {
66+
case .notEqual(let value):
67+
try container.encode("notEqual", forKey: .type)
68+
if let value = value {
69+
try container.encode(value, forKey: .value)
70+
}
71+
case .equals(let value):
72+
try container.encode("equals", forKey: .type)
73+
if let value = value {
74+
try container.encode(value, forKey: .value)
75+
}
76+
case .lessOrEqual(let value):
77+
try container.encode("lessOrEqual", forKey: .type)
78+
try container.encode(value, forKey: .value)
79+
80+
case .lessThan(let value):
81+
try container.encode("lessThan", forKey: .type)
82+
try container.encode(value, forKey: .value)
83+
84+
case .greaterOrEqual(let value):
85+
try container.encode("greaterOrEqual", forKey: .type)
86+
try container.encode(value, forKey: .value)
87+
88+
case .greaterThan(let value):
89+
try container.encode("greaterThan", forKey: .type)
90+
try container.encode(value, forKey: .value)
91+
92+
case .contains(let value):
93+
try container.encode("contains", forKey: .type)
94+
try container.encode(value, forKey: .value)
95+
96+
case .notContains(let value):
97+
try container.encode("notContains", forKey: .type)
98+
try container.encode(value, forKey: .value)
99+
100+
case .between(let start, let end):
101+
try container.encode("between", forKey: .type)
102+
try container.encode(start, forKey: .start)
103+
try container.encode(end, forKey: .end)
104+
105+
case .beginsWith(let value):
106+
try container.encode("beginsWith", forKey: .type)
107+
try container.encode(value, forKey: .value)
108+
}
109+
}
54110
}

Amplify/Categories/DataStore/Query/QueryPredicate.swift

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
import Foundation
99

1010
/// Protocol that indicates concrete types conforming to it can be used a predicate member.
11-
public protocol QueryPredicate: Evaluable {}
11+
public protocol QueryPredicate: Evaluable, Encodable {}
1212

13-
public enum QueryPredicateGroupType: String {
13+
public enum QueryPredicateGroupType: String, Encodable {
1414
case and
1515
case or
1616
case not
@@ -26,14 +26,14 @@ public func not<Predicate: QueryPredicate>(_ predicate: Predicate) -> QueryPredi
2626
/// The case `.all` is a predicate used as an argument to select all of a single modeltype. We
2727
/// chose `.all` instead of `nil` because we didn't want to use the implicit nature of `nil` to
2828
/// specify an action applies to an entire data set.
29-
public enum QueryPredicateConstant: QueryPredicate {
29+
public enum QueryPredicateConstant: QueryPredicate, Encodable {
3030
case all
3131
public func evaluate(target: Model) -> Bool {
3232
return true
3333
}
3434
}
3535

36-
public class QueryPredicateGroup: QueryPredicate {
36+
public class QueryPredicateGroup: QueryPredicate, Encodable {
3737
public internal(set) var type: QueryPredicateGroupType
3838
public internal(set) var predicates: [QueryPredicate]
3939

@@ -92,9 +92,37 @@ public class QueryPredicateGroup: QueryPredicate {
9292
return !predicate.evaluate(target: target)
9393
}
9494
}
95+
96+
// MARK: - Encodable conformance
97+
98+
private enum CodingKeys: String, CodingKey {
99+
case type
100+
case predicates
101+
}
102+
103+
struct AnyQueryPredicate: Encodable {
104+
private let _encode: (Encoder) throws -> Void
105+
106+
init(_ base: QueryPredicate) {
107+
_encode = base.encode
108+
}
109+
110+
func encode(to encoder: Encoder) throws {
111+
try _encode(encoder)
112+
}
113+
}
114+
115+
public func encode(to encoder: Encoder) throws {
116+
var container = encoder.container(keyedBy: CodingKeys.self)
117+
try container.encode(type.rawValue, forKey: .type)
118+
119+
let anyPredicates = predicates.map(AnyQueryPredicate.init)
120+
try container.encode(anyPredicates, forKey: .predicates)
121+
}
122+
95123
}
96124

97-
public class QueryPredicateOperation: QueryPredicate {
125+
public class QueryPredicateOperation: QueryPredicate, Encodable {
98126

99127
public let field: String
100128
public let `operator`: QueryOperator

AmplifyPlugins/Core/AWSPluginsCore/Sync/ModelSync/ModelSyncMetadata+Schema.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ extension ModelSyncMetadata {
1515
public enum CodingKeys: String, ModelKey {
1616
case id
1717
case lastSync
18+
case syncPredicate
1819
}
1920

2021
public static let keys = CodingKeys.self
@@ -27,7 +28,8 @@ extension ModelSyncMetadata {
2728

2829
definition.fields(
2930
.id(),
30-
.field(keys.lastSync, is: .optional, ofType: .int)
31+
.field(keys.lastSync, is: .optional, ofType: .int),
32+
.field(keys.syncPredicate, is: .optional, ofType: .string)
3133
)
3234
}
3335
}

AmplifyPlugins/Core/AWSPluginsCore/Sync/ModelSync/ModelSyncMetadata.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,15 @@ public struct ModelSyncMetadata: Model {
1313

1414
/// The timestamp (in Unix seconds) at which the last sync was started, as reported by the service
1515
public var lastSync: Int?
16+
17+
/// The sync predicate for this model, extracted out from the sync expression.
18+
public var syncPredicate: String?
1619

1720
public init(id: String,
18-
lastSync: Int?) {
21+
lastSync: Int?,
22+
syncPredicate: String? = nil) {
1923
self.id = id
2024
self.lastSync = lastSync
25+
self.syncPredicate = syncPredicate
2126
}
2227
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Amplify
9+
import Foundation
10+
import SQLite
11+
import AWSPluginsCore
12+
13+
class ModelSyncMetadataMigration: ModelMigration {
14+
15+
weak var storageAdapter: SQLiteStorageEngineAdapter?
16+
17+
func apply() throws {
18+
try performModelMetadataSyncPredicateUpgrade()
19+
}
20+
21+
init(storageAdapter: SQLiteStorageEngineAdapter? = nil) {
22+
self.storageAdapter = storageAdapter
23+
}
24+
25+
/// Add the new syncPredicate column for the ModelSyncMetadata system table.
26+
///
27+
/// ModelSyncMetadata's syncPredicate column was added in Amplify version 2.22.0 to
28+
/// support a bug fix related to persisting the sync predicate of the sync expression.
29+
/// Apps before upgrading to this version of the plugin will have created the table already.
30+
/// Upgraded apps will not re-create the table with the CreateTableStatement, neither will throw an error
31+
/// (CreateTableStatement is run with 'create table if not exists' doing a no-op). This function
32+
/// checks if the column exists on the table, and if it doesn't, alter the table to add the new column.
33+
///
34+
/// For more details, see https://github.com/aws-amplify/amplify-swift/pull/2757.
35+
/// - Returns: `true` if upgrade occured, `false` otherwise.
36+
@discardableResult
37+
func performModelMetadataSyncPredicateUpgrade() throws -> Bool {
38+
do {
39+
guard let field = ModelSyncMetadata.schema.field(
40+
withName: ModelSyncMetadata.keys.syncPredicate.stringValue) else {
41+
log.error("Could not find corresponding ModelField from ModelSyncMetadata for syncPredicate")
42+
return false
43+
}
44+
let exists = try columnExists(modelSchema: ModelSyncMetadata.schema,
45+
field: field)
46+
guard !exists else {
47+
log.debug("Detected ModelSyncMetadata table has syncPredicate column. No migration needed")
48+
return false
49+
}
50+
51+
log.debug("Detected ModelSyncMetadata table exists without syncPredicate column.")
52+
guard let storageAdapter = storageAdapter else {
53+
log.debug("Missing SQLiteStorageEngineAdapter for model migration")
54+
throw DataStoreError.nilStorageAdapter()
55+
}
56+
guard let connection = storageAdapter.connection else {
57+
throw DataStoreError.nilSQLiteConnection()
58+
}
59+
let addColumnStatement = AlterTableAddColumnStatement(
60+
modelSchema: ModelSyncMetadata.schema,
61+
field: field).stringValue
62+
try connection.execute(addColumnStatement)
63+
log.debug("ModelSyncMetadata table altered to add syncPredicate column.")
64+
return true
65+
} catch {
66+
throw DataStoreError.invalidOperation(causedBy: error)
67+
}
68+
}
69+
70+
func columnExists(modelSchema: ModelSchema, field: ModelField) throws -> Bool {
71+
guard let storageAdapter = storageAdapter else {
72+
log.debug("Missing SQLiteStorageEngineAdapter for model migration")
73+
throw DataStoreError.nilStorageAdapter()
74+
}
75+
guard let connection = storageAdapter.connection else {
76+
throw DataStoreError.nilSQLiteConnection()
77+
}
78+
79+
let tableInfoStatement = TableInfoStatement(modelSchema: modelSchema)
80+
do {
81+
let existingColumns = try connection.prepare(tableInfoStatement.stringValue).run()
82+
let columnToFind = field.name
83+
var columnExists = false
84+
for column in existingColumns {
85+
// The second element is the column name
86+
if column.count >= 2,
87+
let columnName = column[1],
88+
let columNameString = columnName as? String,
89+
columnToFind == columNameString {
90+
columnExists = true
91+
break
92+
}
93+
}
94+
return columnExists
95+
} catch {
96+
throw DataStoreError.invalidOperation(causedBy: error)
97+
}
98+
}
99+
}
100+
101+
extension ModelSyncMetadataMigration: DefaultLogger {
102+
public static var log: Logger {
103+
Amplify.Logging.logger(forCategory: CategoryType.dataStore.displayName, forNamespace: String(describing: self))
104+
}
105+
public var log: Logger {
106+
Self.log
107+
}
108+
}

AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/SQLStatement+AlterTable.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,13 @@ struct AlterTableStatement: SQLStatement {
2222
self.modelSchema = toModelSchema
2323
}
2424
}
25+
26+
struct AlterTableAddColumnStatement: SQLStatement {
27+
var modelSchema: ModelSchema
28+
var field: ModelField
29+
30+
var stringValue: String {
31+
"ALTER TABLE \"\(modelSchema.name)\" ADD COLUMN \"\(field.sqlName)\" \"\(field.sqlType)\";"
32+
}
33+
}
34+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Amplify
9+
import Foundation
10+
import SQLite
11+
12+
struct TableInfoStatement: SQLStatement {
13+
let modelSchema: ModelSchema
14+
15+
var stringValue: String {
16+
return "PRAGMA table_info(\"\(modelSchema.name)\");"
17+
}
18+
}

AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/StorageEngineAdapter+SQLite.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,12 @@ final class SQLiteStorageEngineAdapter: StorageEngineAdapter {
117117
let delegate = SQLiteMutationSyncMetadataMigrationDelegate(
118118
storageAdapter: self,
119119
modelSchemas: modelSchemas)
120-
let modelMigration = MutationSyncMetadataMigration(delegate: delegate)
121-
let modelMigrations = ModelMigrations(modelMigrations: [modelMigration])
120+
let mutationSyncMetadataMigration = MutationSyncMetadataMigration(delegate: delegate)
121+
122+
let modelSyncMetadataMigration = ModelSyncMetadataMigration(storageAdapter: self)
123+
124+
let modelMigrations = ModelMigrations(modelMigrations: [mutationSyncMetadataMigration,
125+
modelSyncMetadataMigration])
122126
do {
123127
try modelMigrations.apply()
124128
} catch {

0 commit comments

Comments
 (0)