141 lines
6.2 KiB
Swift
141 lines
6.2 KiB
Swift
//
|
|
// Copyright Amazon.com Inc. or its affiliates.
|
|
// All Rights Reserved.
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
|
|
import Amplify
|
|
import Foundation
|
|
import SQLite
|
|
import AWSPluginsCore
|
|
|
|
extension SQLiteMutationSyncMetadataMigrationDelegate {
|
|
|
|
// MARK: - Precondition
|
|
|
|
/// Migration requires some hardcoded SQL statements that may break if the schema changes
|
|
/// Ensure that the schemas used in this migration are as expected
|
|
func preconditionCheck() throws {
|
|
let fieldCount = 4
|
|
let idField = "id"
|
|
let deletedField = "deleted"
|
|
let lastChangedAtField = "lastChangedAt"
|
|
let versionField = "version"
|
|
|
|
guard MutationSyncMetadata.schema.fields.count == fieldCount else {
|
|
throw DataStoreError.internalOperation("MutationSyncMetadata schema has changed from 4 fields", "", nil)
|
|
}
|
|
|
|
guard MutationSyncMetadata.schema.fields[idField] != nil,
|
|
MutationSyncMetadata.schema.fields[deletedField] != nil,
|
|
MutationSyncMetadata.schema.fields[lastChangedAtField] != nil,
|
|
MutationSyncMetadata.schema.fields[versionField] != nil else {
|
|
throw DataStoreError.internalOperation("MutationSyncMetadata schema missing expected fields", "", nil)
|
|
}
|
|
|
|
guard MutationSyncMetadataMigration.MutationSyncMetadataCopy.schema.fields.count == fieldCount else {
|
|
throw DataStoreError.internalOperation("MutationSyncMetadataCopy schema has changed from 4 fields", "", nil)
|
|
}
|
|
|
|
guard MutationSyncMetadataMigration.MutationSyncMetadataCopy.schema.fields[idField] != nil,
|
|
MutationSyncMetadataMigration.MutationSyncMetadataCopy.schema.fields[deletedField] != nil,
|
|
MutationSyncMetadataMigration.MutationSyncMetadataCopy.schema.fields[lastChangedAtField] != nil,
|
|
MutationSyncMetadataMigration.MutationSyncMetadataCopy.schema.fields[versionField] != nil else {
|
|
throw DataStoreError.internalOperation("MutationSyncMetadataCopy schema missing expected fields", "", nil)
|
|
}
|
|
}
|
|
|
|
// MARK: - Needs Migration
|
|
|
|
/// If there are no MutationSyncMetadata records, then it is not necessary to apply the migration since there is no
|
|
/// data to migrate. If there is data, and the id's have already been migrated ( > 0 keys), then no migration needed
|
|
func mutationSyncMetadataStoreEmptyOrMigrated() throws -> Bool {
|
|
let records = try selectMutationSyncMetadataRecords()
|
|
if records.metadataCount == 0 || records.metadataIdMatchNewKeyCount > 0 {
|
|
log.debug("No MutationSyncMetadata migration needed.")
|
|
return true
|
|
}
|
|
log.debug("Migration is needed. MutationSyncMetadata IDs need to be migrated to new key format.")
|
|
return false
|
|
}
|
|
|
|
/// Retrieve the record count from the MutationSyncMetadata table for
|
|
/// 1. the total number of records
|
|
/// 2. the total number of records that have the `id` match `<modelName>|<modelId>`
|
|
func selectMutationSyncMetadataRecords() throws -> (metadataCount: Int64, metadataIdMatchNewKeyCount: Int64) {
|
|
guard let storageAdapter = storageAdapter else {
|
|
log.debug("Missing SQLiteStorageEngineAdapter for model migration")
|
|
throw DataStoreError.nilStorageAdapter()
|
|
}
|
|
guard let connection = storageAdapter.connection else {
|
|
throw DataStoreError.nilSQLiteConnection()
|
|
}
|
|
let sql = """
|
|
select (select count(1) as count from MutationSyncMetadata) as allRecords,
|
|
(select count(1) as count from MutationSyncMetadata where id like '%|%') as newKeys
|
|
"""
|
|
log.debug("Checking MutationSyncMetadata records, SQL: \(sql)")
|
|
let rows = try connection.run(sql)
|
|
let iter = rows.makeIterator()
|
|
while let row = try iter.failableNext() {
|
|
if let metadataCount = row[0] as? Int64, let metadataIdMatchNewKeyCount = row[1] as? Int64 {
|
|
return (metadataCount, metadataIdMatchNewKeyCount)
|
|
} else {
|
|
log.verbose("Failed to iterate over records")
|
|
throw DataStoreError.unknown("Failed to iterate over records", "", nil)
|
|
}
|
|
}
|
|
|
|
throw DataStoreError.unknown("Failed to select MutationSyncMetadata records", "", nil)
|
|
}
|
|
|
|
// MARK: - Cannot Migrate
|
|
|
|
func containsDuplicateIdsAcrossModels() throws -> Bool {
|
|
guard let storageAdapter = storageAdapter else {
|
|
log.debug("Missing SQLiteStorageEngineAdapter for model migration")
|
|
throw DataStoreError.nilStorageAdapter()
|
|
}
|
|
guard let connection = storageAdapter.connection else {
|
|
throw DataStoreError.nilSQLiteConnection()
|
|
}
|
|
let sql = selectDuplicateIdAcrossModels()
|
|
log.debug("Checking for duplicate IDs, SQL: \(sql)")
|
|
let rows = try connection.run(sql)
|
|
let iter = rows.makeIterator()
|
|
while let row = try iter.failableNext() {
|
|
return !row.isEmpty
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/// Retrieve results where `id` is the same across multiple tables.
|
|
/// For three models, the SQL statement looks like this:
|
|
/// ```
|
|
/// SELECT id, tableName, count(id) as count FROM (
|
|
/// SELECT id, 'Restaurant' as tableName FROM Restaurant UNION ALL
|
|
/// SELECT id, 'Menu' as tableName FROM Menu UNION ALL
|
|
/// SELECT id, 'Dish' as tableName FROM Dish) GROUP BY id HAVING count > 1
|
|
/// ```
|
|
/// If there are three models in different model tables with the same id "1"
|
|
/// the result of this query will have a row like:
|
|
/// ```
|
|
/// // [id, tableName, count(id)
|
|
/// [Optional("1"), Optional("Restaurant"), Optional(3)]
|
|
/// ```
|
|
/// As long as there is one resulting duplicate id, the entire function will return true
|
|
func selectDuplicateIdAcrossModels() -> String {
|
|
var sql = ""
|
|
for modelSchema in modelSchemas {
|
|
let modelName = modelSchema.name
|
|
if sql != "" {
|
|
sql += " UNION ALL "
|
|
}
|
|
sql += "SELECT id, \'\(modelName)\' as tableName FROM \(modelName)"
|
|
}
|
|
return "SELECT id, tableName, count(id) as count FROM (" + sql + ") GROUP BY id HAVING count > 1"
|
|
}
|
|
}
|