Skip to content

Commit 3156d03

Browse files
authored
fix(DataStore): Allow different model types with the same ID (#1490)
* fix: use modelName in MutationSyncMetadata.id * fix: Add migration script * address PR comments * fix broken unit tests using MutationSyncMetadata.id * fix MutationSync custom decoding to create MutationSyncMetadata * address PR comments * fix rebase * some refactoring * method rename
1 parent 16640e1 commit 3156d03

File tree

46 files changed

+1600
-111
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1600
-111
lines changed

AmplifyPlugins/Core/AWSPluginsCore/Sync/MutationSync/MutationSync.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ public struct MutationSync<ModelType: Model>: Decodable {
3030
let modelType = ModelType.self
3131
let json = try JSONValue(from: decoder)
3232

33+
var resolvedModelName = modelType.modelName
34+
3335
// in case of `AnyModel`, decode the underlying type and erase to `AnyModel`
3436
if modelType == AnyModel.self {
3537
guard case let .string(modelName) = json["__typename"] else {
@@ -53,6 +55,7 @@ public struct MutationSync<ModelType: Model>: Decodable {
5355
""")
5456
}
5557
self.model = anyModel
58+
resolvedModelName = modelName
5659
} else {
5760
self.model = try modelType.init(from: decoder)
5861
}
@@ -74,7 +77,8 @@ public struct MutationSync<ModelType: Model>: Decodable {
7477
)
7578
}
7679

77-
self.syncMetadata = MutationSyncMetadata(id: model.id,
80+
self.syncMetadata = MutationSyncMetadata(modelId: model.id,
81+
modelName: resolvedModelName,
7882
deleted: deleted,
7983
lastChangedAt: Int(lastChangedAt),
8084
version: Int(version))

AmplifyPlugins/Core/AWSPluginsCore/Sync/MutationSync/MutationSyncMetadata.swift

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,42 @@
88
import Amplify
99

1010
public struct MutationSyncMetadata: Model {
11+
/// Alias of MutationSyncMetadata's identifier, which has the format of `{modelName}|{modelId}`
12+
public typealias Identifier = String
1113

12-
public let id: Model.Identifier
14+
public let id: MutationSyncMetadata.Identifier
1315
public var deleted: Bool
1416
public var lastChangedAt: Int
1517
public var version: Int
18+
19+
static let deliminator = "|"
20+
21+
public var modelId: String {
22+
id.components(separatedBy: MutationSyncMetadata.deliminator).last ?? ""
23+
}
24+
public var modelName: String {
25+
id.components(separatedBy: MutationSyncMetadata.deliminator).first ?? ""
26+
}
27+
28+
@available(*, deprecated, message: """
29+
The format of the `id` has changed to support unique ids across mutiple model types.
30+
Use init(modelId:modelName:deleted:lastChangedAt) to pass in the `modelName`.
31+
""")
32+
public init(id: Model.Identifier, deleted: Bool, lastChangedAt: Int, version: Int) {
33+
self.id = id
34+
self.deleted = deleted
35+
self.lastChangedAt = lastChangedAt
36+
self.version = version
37+
}
38+
39+
public init(modelId: Model.Identifier, modelName: String, deleted: Bool, lastChangedAt: Int, version: Int) {
40+
self.id = Self.identifier(modelName: modelName, modelId: modelId)
41+
self.deleted = deleted
42+
self.lastChangedAt = lastChangedAt
43+
self.version = version
44+
}
45+
46+
public static func identifier(modelName: String, modelId: Model.Identifier) -> MutationSyncMetadata.Identifier {
47+
"\(modelName)\(deliminator)\(modelId)"
48+
}
1649
}

AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/AWSDataStorePlugin+DataStoreBaseBehavior.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,8 +247,9 @@ extension AWSDataStorePlugin: DataStoreBaseBehavior {
247247
return
248248
}
249249
let metadata = MutationSyncMetadata.keys
250+
let metadataId = MutationSyncMetadata.identifier(modelName: modelSchema.name, modelId: model.id)
250251
storageEngine.query(MutationSyncMetadata.self,
251-
predicate: metadata.id == model.id,
252+
predicate: metadata.id == metadataId,
252253
sort: nil,
253254
paginationInput: .firstResult) {
254255
do {

AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/AWSDataStorePlugin.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ final public class AWSDataStorePlugin: DataStoreCategoryPlugin {
124124
}
125125
try resolveStorageEngine(dataStoreConfiguration: dataStoreConfiguration)
126126
try storageEngine.setUp(modelSchemas: ModelRegistry.modelSchemas)
127+
try storageEngine.applyModelMigrations(modelSchemas: ModelRegistry.modelSchemas)
127128
storageEngineInitSemaphore.signal()
128129
storageEngine.startSync { result in
129130

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
11+
protocol ModelMigration {
12+
func apply() throws
13+
}
14+
15+
class ModelMigrations {
16+
var modelMigrations: [ModelMigration]
17+
18+
init(modelMigrations: [ModelMigration]) {
19+
self.modelMigrations = modelMigrations
20+
}
21+
22+
func apply() throws {
23+
for modelMigrations in modelMigrations {
24+
try modelMigrations.apply()
25+
}
26+
}
27+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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 Foundation
9+
import Amplify
10+
11+
extension MutationSyncMetadataMigration {
12+
public struct MutationSyncMetadataCopy: Model {
13+
public let id: String
14+
public var deleted: Bool
15+
public var lastChangedAt: Int
16+
public var version: Int
17+
18+
// MARK: - CodingKeys
19+
20+
public enum CodingKeys: String, ModelKey {
21+
case id
22+
case deleted
23+
case lastChangedAt
24+
case version
25+
}
26+
27+
public static let keys = CodingKeys.self
28+
29+
// MARK: - ModelSchema
30+
31+
public static let schema = defineSchema { definition in
32+
let sync = MutationSyncMetadataCopy.keys
33+
34+
definition.attributes(.isSystem)
35+
36+
definition.fields(
37+
.id(),
38+
.field(sync.deleted, is: .required, ofType: .bool),
39+
.field(sync.lastChangedAt, is: .required, ofType: .int),
40+
.field(sync.version, is: .required, ofType: .int)
41+
)
42+
}
43+
}
44+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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 AWSPluginsCore
11+
12+
class MutationSyncMetadataMigration: ModelMigration {
13+
14+
weak var delegate: MutationSyncMetadataMigrationDelegate?
15+
16+
init(delegate: MutationSyncMetadataMigrationDelegate) {
17+
self.delegate = delegate
18+
}
19+
20+
func apply() throws {
21+
guard let delegate = delegate else {
22+
log.debug("Missing MutationSyncMetadataMigrationDelegate delegate")
23+
throw DataStoreError.unknown("Missing MutationSyncMetadataMigrationDelegate delegate", "", nil)
24+
}
25+
try delegate.preconditionCheck()
26+
try delegate.transaction {
27+
if try delegate.mutationSyncMetadataStoreEmptyOrMigrated() {
28+
return
29+
}
30+
31+
if try delegate.containsDuplicateIdsAcrossModels() {
32+
log.debug("Duplicate IDs found across different model types.")
33+
log.debug("Clearing MutationSyncMetadata and ModelSyncMetadata to force full sync.")
34+
try delegate.applyMigrationStep(.emptyMutationSyncMetadataStore)
35+
try delegate.applyMigrationStep(.emptyModelSyncMetadataStore)
36+
} else {
37+
log.debug("No duplicate IDs found.")
38+
log.debug("Modifying and backfilling MutationSyncMetadata")
39+
try delegate.applyMigrationStep(.removeMutationSyncMetadataCopyStore)
40+
try delegate.applyMigrationStep(.createMutationSyncMetadataCopyStore)
41+
try delegate.applyMigrationStep(.backfillMutationSyncMetadata)
42+
try delegate.applyMigrationStep(.removeMutationSyncMetadataStore)
43+
try delegate.applyMigrationStep(.renameMutationSyncMetadataCopy)
44+
}
45+
}
46+
}
47+
}
48+
49+
extension MutationSyncMetadataMigration: DefaultLogger { }
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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+
final class SQLiteMutationSyncMetadataMigrationDelegate: MutationSyncMetadataMigrationDelegate {
14+
15+
let modelSchemas: [ModelSchema]
16+
weak var storageAdapter: SQLiteStorageEngineAdapter?
17+
18+
init(storageAdapter: SQLiteStorageEngineAdapter, modelSchemas: [ModelSchema]) {
19+
self.storageAdapter = storageAdapter
20+
self.modelSchemas = modelSchemas
21+
}
22+
23+
func transaction(_ basicClosure: BasicThrowableClosure) throws {
24+
try storageAdapter?.transaction(basicClosure)
25+
}
26+
27+
func applyMigrationStep(_ step: MutationSyncMetadataMigrationStep) throws {
28+
switch step {
29+
case .emptyMutationSyncMetadataStore:
30+
try emptyMutationSyncMetadataStore()
31+
case .emptyModelSyncMetadataStore:
32+
try emptyMutationSyncMetadataStore()
33+
case .removeMutationSyncMetadataCopyStore:
34+
try removeMutationSyncMetadataCopyStore()
35+
case .createMutationSyncMetadataCopyStore:
36+
try createMutationSyncMetadataCopyStore()
37+
case .backfillMutationSyncMetadata:
38+
try backfillMutationSyncMetadata()
39+
case .removeMutationSyncMetadataStore:
40+
try removeMutationSyncMetadataStore()
41+
case .renameMutationSyncMetadataCopy:
42+
try renameMutationSyncMetadataCopy()
43+
}
44+
}
45+
46+
// MARK: - Clear
47+
48+
@discardableResult func emptyMutationSyncMetadataStore() throws -> String {
49+
guard let storageAdapter = storageAdapter else {
50+
log.debug("Missing SQLiteStorageEngineAdapter")
51+
throw DataStoreError.unknown("Missing storage adapter for model migration", "", nil)
52+
}
53+
54+
return try storageAdapter.emptyStore(for: MutationSyncMetadata.schema)
55+
}
56+
57+
@discardableResult func emptyModelSyncMetadataStore() throws -> String {
58+
guard let storageAdapter = storageAdapter else {
59+
log.debug("Missing SQLiteStorageEngineAdapter")
60+
throw DataStoreError.unknown("Missing storage adapter for model migration", "", nil)
61+
}
62+
63+
return try storageAdapter.emptyStore(for: ModelSyncMetadata.schema)
64+
}
65+
66+
// MARK: - Migration
67+
68+
@discardableResult func removeMutationSyncMetadataCopyStore() throws -> String {
69+
guard let storageAdapter = storageAdapter else {
70+
log.debug("Missing SQLiteStorageEngineAdapter")
71+
throw DataStoreError.unknown("Missing storage adapter for model migration", "", nil)
72+
}
73+
74+
return try storageAdapter.removeStore(for: MutationSyncMetadataMigration.MutationSyncMetadataCopy.schema)
75+
}
76+
77+
@discardableResult func createMutationSyncMetadataCopyStore() throws -> String {
78+
guard let storageAdapter = storageAdapter else {
79+
log.debug("Missing SQLiteStorageEngineAdapter")
80+
throw DataStoreError.unknown("Missing storage adapter for model migration", "", nil)
81+
}
82+
83+
return try storageAdapter.createStore(for: MutationSyncMetadataMigration.MutationSyncMetadataCopy.schema)
84+
}
85+
86+
@discardableResult func backfillMutationSyncMetadata() throws -> String {
87+
var sql = ""
88+
for modelSchema in modelSchemas {
89+
let modelName = modelSchema.name
90+
91+
if sql != "" {
92+
sql += " UNION ALL "
93+
}
94+
sql += "SELECT id, \'\(modelName)\' as tableName FROM \(modelName)"
95+
}
96+
sql = "INSERT INTO \(MutationSyncMetadataMigration.MutationSyncMetadataCopy.modelName) (id,deleted,lastChangedAt,version) " +
97+
"select models.tableName || '|' || mm.id, mm.deleted, mm.lastChangedAt, mm.version " +
98+
"from MutationSyncMetadata mm INNER JOIN (" + sql + ") as models on mm.id=models.id"
99+
try storageAdapter?.connection.execute(sql)
100+
return sql
101+
}
102+
103+
@discardableResult func removeMutationSyncMetadataStore() throws -> String {
104+
guard let storageAdapter = storageAdapter else {
105+
log.debug("Missing SQLiteStorageEngineAdapter")
106+
throw DataStoreError.unknown("Missing storage adapter for model migration", "", nil)
107+
}
108+
109+
return try storageAdapter.removeStore(for: MutationSyncMetadata.schema)
110+
}
111+
112+
@discardableResult func renameMutationSyncMetadataCopy() throws -> String {
113+
guard let storageAdapter = storageAdapter else {
114+
log.debug("Missing SQLiteStorageEngineAdapter")
115+
throw DataStoreError.unknown("Missing storage adapter for model migration", "", nil)
116+
}
117+
118+
return try storageAdapter.renameStore(from: MutationSyncMetadataMigration.MutationSyncMetadataCopy.schema,
119+
toModelSchema: MutationSyncMetadata.schema)
120+
}
121+
}
122+
123+
extension SQLiteMutationSyncMetadataMigrationDelegate: DefaultLogger { }

0 commit comments

Comments
 (0)