Skip to content

Commit

Permalink
fix(datastore): full sync when sync predicate changes (#2757)
Browse files Browse the repository at this point in the history
  • Loading branch information
lawmicha authored Dec 8, 2023
1 parent a5a981a commit 2530737
Show file tree
Hide file tree
Showing 13 changed files with 611 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import Foundation
/// - `Temporal.Time`
/// - Warning: Although this has `public` access, it is intended for internal use and should not be used directly
/// by host applications. The behavior of this may change without warning.
public protocol Persistable {}
public protocol Persistable: Encodable {}

extension Bool: Persistable {}
extension Double: Persistable {}
Expand Down
60 changes: 58 additions & 2 deletions Amplify/Categories/DataStore/Query/QueryOperator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

public enum QueryOperator {
public enum QueryOperator: Encodable {
case notEqual(_ value: Persistable?)
case equals(_ value: Persistable?)
case lessOrEqual(_ value: Persistable)
Expand All @@ -18,7 +18,7 @@ public enum QueryOperator {
case notContains(_ value: String)
case between(start: Persistable, end: Persistable)
case beginsWith(_ value: String)

public func evaluate(target: Any) -> Bool {
switch self {
case .notEqual(let predicateValue):
Expand Down Expand Up @@ -51,4 +51,60 @@ public enum QueryOperator {
}
return false
}

private enum CodingKeys: String, CodingKey {
case type
case value
case start
case end
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

switch self {
case .notEqual(let value):
try container.encode("notEqual", forKey: .type)
if let value = value {
try container.encode(value, forKey: .value)
}
case .equals(let value):
try container.encode("equals", forKey: .type)
if let value = value {
try container.encode(value, forKey: .value)
}
case .lessOrEqual(let value):
try container.encode("lessOrEqual", forKey: .type)
try container.encode(value, forKey: .value)

case .lessThan(let value):
try container.encode("lessThan", forKey: .type)
try container.encode(value, forKey: .value)

case .greaterOrEqual(let value):
try container.encode("greaterOrEqual", forKey: .type)
try container.encode(value, forKey: .value)

case .greaterThan(let value):
try container.encode("greaterThan", forKey: .type)
try container.encode(value, forKey: .value)

case .contains(let value):
try container.encode("contains", forKey: .type)
try container.encode(value, forKey: .value)

case .notContains(let value):
try container.encode("notContains", forKey: .type)
try container.encode(value, forKey: .value)

case .between(let start, let end):
try container.encode("between", forKey: .type)
try container.encode(start, forKey: .start)
try container.encode(end, forKey: .end)

case .beginsWith(let value):
try container.encode("beginsWith", forKey: .type)
try container.encode(value, forKey: .value)
}
}
}
38 changes: 33 additions & 5 deletions Amplify/Categories/DataStore/Query/QueryPredicate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
import Foundation

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

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

public class QueryPredicateGroup: QueryPredicate {
public class QueryPredicateGroup: QueryPredicate, Encodable {
public internal(set) var type: QueryPredicateGroupType
public internal(set) var predicates: [QueryPredicate]

Expand Down Expand Up @@ -92,9 +92,37 @@ public class QueryPredicateGroup: QueryPredicate {
return !predicate.evaluate(target: target)
}
}

// MARK: - Encodable conformance

private enum CodingKeys: String, CodingKey {
case type
case predicates
}

struct AnyQueryPredicate: Encodable {
private let _encode: (Encoder) throws -> Void

init(_ base: QueryPredicate) {
_encode = base.encode
}

func encode(to encoder: Encoder) throws {
try _encode(encoder)
}
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(type.rawValue, forKey: .type)

let anyPredicates = predicates.map(AnyQueryPredicate.init)
try container.encode(anyPredicates, forKey: .predicates)
}

}

public class QueryPredicateOperation: QueryPredicate {
public class QueryPredicateOperation: QueryPredicate, Encodable {

public let field: String
public let `operator`: QueryOperator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ extension ModelSyncMetadata {
public enum CodingKeys: String, ModelKey {
case id
case lastSync
case syncPredicate
}

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

definition.fields(
.id(),
.field(keys.lastSync, is: .optional, ofType: .int)
.field(keys.lastSync, is: .optional, ofType: .int),
.field(keys.syncPredicate, is: .optional, ofType: .string)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,15 @@ public struct ModelSyncMetadata: Model {

/// The timestamp (in Unix seconds) at which the last sync was started, as reported by the service
public var lastSync: Int64?

/// The sync predicate for this model, extracted out from the sync expression.
public var syncPredicate: String?

public init(id: String,
lastSync: Int64?) {
lastSync: Int64? = nil,
syncPredicate: String? = nil) {
self.id = id
self.lastSync = lastSync
self.syncPredicate = syncPredicate
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Amplify
import Foundation
import SQLite
import AWSPluginsCore

class ModelSyncMetadataMigration: ModelMigration {

weak var storageAdapter: SQLiteStorageEngineAdapter?

func apply() throws {
try performModelMetadataSyncPredicateUpgrade()
}

init(storageAdapter: SQLiteStorageEngineAdapter? = nil) {
self.storageAdapter = storageAdapter
}

/// Add the new syncPredicate column for the ModelSyncMetadata system table.
///
/// ModelSyncMetadata's syncPredicate column was added in Amplify version 2.22.0 to
/// support a bug fix related to persisting the sync predicate of the sync expression.
/// Apps before upgrading to this version of the plugin will have created the table already.
/// Upgraded apps will not re-create the table with the CreateTableStatement, neither will throw an error
/// (CreateTableStatement is run with 'create table if not exists' doing a no-op). This function
/// checks if the column exists on the table, and if it doesn't, alter the table to add the new column.
///
/// For more details, see https://github.com/aws-amplify/amplify-swift/pull/2757.
/// - Returns: `true` if upgrade occured, `false` otherwise.
@discardableResult
func performModelMetadataSyncPredicateUpgrade() throws -> Bool {
do {
guard let field = ModelSyncMetadata.schema.field(
withName: ModelSyncMetadata.keys.syncPredicate.stringValue) else {
log.error("Could not find corresponding ModelField from ModelSyncMetadata for syncPredicate")
return false
}
let exists = try columnExists(modelSchema: ModelSyncMetadata.schema,
field: field)
guard !exists else {
log.debug("Detected ModelSyncMetadata table has syncPredicate column. No migration needed")
return false
}

log.debug("Detected ModelSyncMetadata table exists without syncPredicate column.")
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 addColumnStatement = AlterTableAddColumnStatement(
modelSchema: ModelSyncMetadata.schema,
field: field).stringValue
try connection.execute(addColumnStatement)
log.debug("ModelSyncMetadata table altered to add syncPredicate column.")
return true
} catch {
throw DataStoreError.invalidOperation(causedBy: error)
}
}

func columnExists(modelSchema: ModelSchema, field: ModelField) 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 tableInfoStatement = TableInfoStatement(modelSchema: modelSchema)
do {
let existingColumns = try connection.prepare(tableInfoStatement.stringValue).run()
let columnToFind = field.name
var columnExists = false
for column in existingColumns {
// The second element is the column name
if column.count >= 2,
let columnName = column[1],
let columNameString = columnName as? String,
columnToFind == columNameString {
columnExists = true
break
}
}
return columnExists
} catch {
throw DataStoreError.invalidOperation(causedBy: error)
}
}
}

extension ModelSyncMetadataMigration: DefaultLogger {
public static var log: Logger {
Amplify.Logging.logger(forCategory: CategoryType.dataStore.displayName, forNamespace: String(describing: self))
}
public var log: Logger {
Self.log
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,13 @@ struct AlterTableStatement: SQLStatement {
self.modelSchema = toModelSchema
}
}

struct AlterTableAddColumnStatement: SQLStatement {
var modelSchema: ModelSchema
var field: ModelField

var stringValue: String {
"ALTER TABLE \"\(modelSchema.name)\" ADD COLUMN \"\(field.sqlName)\" \"\(field.sqlType)\";"
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Amplify
import Foundation
import SQLite

struct TableInfoStatement: SQLStatement {
let modelSchema: ModelSchema

var stringValue: String {
return "PRAGMA table_info(\"\(modelSchema.name)\");"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,12 @@ final class SQLiteStorageEngineAdapter: StorageEngineAdapter {
let delegate = SQLiteMutationSyncMetadataMigrationDelegate(
storageAdapter: self,
modelSchemas: modelSchemas)
let modelMigration = MutationSyncMetadataMigration(delegate: delegate)
let modelMigrations = ModelMigrations(modelMigrations: [modelMigration])
let mutationSyncMetadataMigration = MutationSyncMetadataMigration(delegate: delegate)

let modelSyncMetadataMigration = ModelSyncMetadataMigration(storageAdapter: self)

let modelMigrations = ModelMigrations(modelMigrations: [mutationSyncMetadataMigration,
modelSyncMetadataMigration])
do {
try modelMigrations.apply()
} catch {
Expand Down
Loading

0 comments on commit 2530737

Please sign in to comment.