Skip to content

fix(datastore): base sync when sync expression changes #2937

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 36 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
9720a40
added comments for migration place in SQLiteStorageAdapter
Oct 8, 2024
a28ad3e
changed log before migration
Oct 8, 2024
24a24c2
added AddSyncExpressionToLastSyncMetadata
Oct 8, 2024
6a3f061
DB migration (MVP)
Oct 8, 2024
455f879
add new field to LastSyncMetadata model (MVP: pre-deserialized Query …
Oct 8, 2024
1fc5f09
updated signature of saveLastDelta/BaseSyncTime and added TODO in Syn…
Oct 8, 2024
f409ad3
use & save syncExpression (MVP)
Oct 8, 2024
0daf143
changed syncExpression in LastSyncMetadata from String to QueryPredic…
Oct 14, 2024
552cd9e
Added NONE PredicateType for MatchNoneQueryPredicate in GsonPredicate…
Oct 14, 2024
7975082
enabled objects assigned to concrete QueryPredicate classes to be ser…
Oct 14, 2024
7b03322
fix broken test cases
Oct 14, 2024
ab1c7f2
Updated lookupLastSyncTime logic with syncExpression comparison
Oct 14, 2024
5e694e0
fixed existing unit tests in SyncProcessorTest
Oct 15, 2024
9568719
added test cases when sync expression change in SyncProcessorTest
Oct 15, 2024
1e68d62
[minor format fix]
Oct 16, 2024
4c85614
minor fixed to pass checkstyle
Oct 16, 2024
076a042
[minor] more fixes for checkstyles
Oct 16, 2024
fc8d8a0
one last fix for checkstyle (hopefully)
Oct 16, 2024
66e0447
modified access modifier of ModelMigration and its implementations
Oct 17, 2024
f76bb96
reverted access modifier in ModelMigrations and added InternalApiWarning
Oct 18, 2024
ae8a66f
changed visibility modifier in LastMetadata and added InternalApiWarning
Oct 18, 2024
b282dab
got rid of new syncExpressions API in DataStoreConfiguration and its …
Oct 18, 2024
329b1d8
Update aws-datastore.api
Oct 21, 2024
34ffcfc
release resources if SyncProcessor reinitialization is required
Oct 24, 2024
c3eee86
[test the unit test failing cause in CI]
Oct 24, 2024
66f34df
removed initSyncProcessor from @Before to avoid double initialization
Oct 24, 2024
3046a41
checkstyle fix
Oct 24, 2024
b852be6
Merge branch 'main' into edisooon/use-last-syncexpression-in-lookupLa…
edisooon Oct 30, 2024
acac3ee
Merge branch 'main' into edisooon/use-last-syncexpression-in-lookupLa…
edisooon Oct 30, 2024
ba02506
Merge branch 'main' into edisooon/use-last-syncexpression-in-lookupLa…
tylerjroach Dec 17, 2024
39695e6
deleted internalAPIWarning for ModelMigration classes
Dec 30, 2024
3ae7025
kept and deprecated old methods and created overrides in LastSyncMeta…
Dec 30, 2024
85b4999
used Objects.equals in SyncTimeRegistry
Dec 30, 2024
921fc67
fixed checkstyle
Dec 30, 2024
9f783a0
Merge branch 'main' into edisooon/use-last-syncexpression-in-lookupLa…
tylerjroach Dec 31, 2024
5af954d
apiDump
Dec 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

package com.amplifyframework.core.model.query.predicate;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
Expand All @@ -33,13 +34,15 @@ public final class GsonPredicateAdapters {
private GsonPredicateAdapters() {}

/**
* Registers the adapters into an {@link GsonBuilder}.
* Registers the QueryPredicate adapter into an {@link GsonBuilder}.
* registerTypeHierarchyAdapter enables objects assigned to concrete QueryPredicate classes
* (e.g., QueryPredicateOperation) to use this adapter.
*
* @param builder A GsonBuilder.
*/
public static void register(GsonBuilder builder) {
builder
.registerTypeAdapter(QueryOperator.class, new QueryOperatorAdapter())
.registerTypeAdapter(QueryPredicate.class, new QueryPredicateAdapter());
.registerTypeHierarchyAdapter(QueryPredicate.class, new QueryPredicateAdapter());
}

/**
Expand Down Expand Up @@ -127,10 +130,17 @@ public static final class QueryPredicateAdapter implements
JsonDeserializer<QueryPredicate>, JsonSerializer<QueryPredicate> {
private static final String TYPE = "_type";

// internal Gson instance for avoiding infinite loop
private final Gson gson = new GsonBuilder()
.registerTypeAdapter(QueryOperator.class, new QueryOperatorAdapter())
.serializeNulls()
.create();

private enum PredicateType {
OPERATION,
GROUP,
ALL
ALL,
NONE
}

/**
Expand All @@ -147,11 +157,13 @@ public QueryPredicate deserialize(JsonElement json, Type type, JsonDeserializati
String predicateType = jsonObject.get(TYPE).getAsString();
switch (PredicateType.valueOf(predicateType)) {
case OPERATION:
return context.deserialize(json, QueryPredicateOperation.class);
return gson.fromJson(json, QueryPredicateOperation.class);
case GROUP:
return context.deserialize(json, QueryPredicateGroup.class);
return gson.fromJson(json, QueryPredicateGroup.class);
case ALL:
return context.deserialize(json, MatchAllQueryPredicate.class);
return gson.fromJson(json, MatchAllQueryPredicate.class);
case NONE:
return gson.fromJson(json, MatchNoneQueryPredicate.class);
default:
throw new JsonParseException("Unable to deserialize " +
json.toString() + " to QueryPredicate instance.");
Expand All @@ -164,16 +176,15 @@ public QueryPredicate deserialize(JsonElement json, Type type, JsonDeserializati
@Override
public JsonElement serialize(QueryPredicate predicate, Type type, JsonSerializationContext context)
throws JsonParseException {
JsonElement json;
JsonElement json = gson.toJsonTree(predicate);
PredicateType predicateType;
if (predicate instanceof MatchAllQueryPredicate) {
predicateType = PredicateType.ALL;
json = context.serialize(predicate, MatchAllQueryPredicate.class);
} else if (predicate instanceof MatchNoneQueryPredicate) {
predicateType = PredicateType.NONE;
} else if (predicate instanceof QueryPredicateOperation) {
json = context.serialize(predicate, QueryPredicateOperation.class);
predicateType = PredicateType.OPERATION;
} else if (predicate instanceof QueryPredicateGroup) {
json = context.serialize(predicate, QueryPredicateGroup.class);
predicateType = PredicateType.GROUP;
} else {
throw new JsonParseException("Unable to identify the predicate type.");
Expand Down
1 change: 0 additions & 1 deletion aws-datastore/api/aws-datastore.api
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,6 @@ public abstract interface class com/amplifyframework/datastore/syncengine/Connec
}

public final class com/amplifyframework/datastore/syncengine/LastSyncMetadata : com/amplifyframework/core/model/Model {
public static fun baseSyncedAt (Ljava/lang/String;J)Lcom/amplifyframework/datastore/syncengine/LastSyncMetadata;
public fun equals (Ljava/lang/Object;)Z
public fun getId ()Ljava/lang/String;
public fun getLastSyncTime ()Ljava/lang/Long;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -934,7 +934,10 @@ private Completable updateModels() {
Objects.requireNonNull(databaseConnectionHandle);
sqliteStorageHelper.update(databaseConnectionHandle, oldVersion, newVersion);
} else {
LOG.debug("Database up to date. Checking ModelMetadata.");
// We only need to do the model migration here because the current implementation of
// sqliteStorageHelper.update will drop all existing tables and recreate tables with new schemas,
// However, this might be changed in the future
LOG.debug("Database up to date. Checking System Models.");
new ModelMigrations(databaseConnectionHandle, modelsProvider).apply();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package com.amplifyframework.datastore.storage.sqlite.migrations;

import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

import com.amplifyframework.core.Amplify;
import com.amplifyframework.core.category.CategoryType;
import com.amplifyframework.logging.Logger;
import com.amplifyframework.util.Wrap;

/**
* Add SyncExpression (TEXT) column to LastSyncMetadata table.
*/
final class AddSyncExpressionToLastSyncMetadata implements ModelMigration {
private static final Logger LOG = Amplify.Logging.logger(CategoryType.DATASTORE, "amplify:aws-datastore");
private final SQLiteDatabase database;
private final String newSyncExpColumnName = "syncExpression";

/**
* Constructor for the migration class.
* @param database Connection to the SQLite database.
*/
AddSyncExpressionToLastSyncMetadata(SQLiteDatabase database) {
this.database = database;
}

@Override
public void apply() {
if (!needsMigration()) {
LOG.debug("No LastSyncMetadata migration needed.");
return;
}
addNewSyncExpColumnName();
}

/**
* Alter LastSyncMetadata table with new column.
* Existing rows in LasySyncMetadata will have 'null' for ${newSyncExpColumnName} value,
* until the next sync/hydrate operation.
*/
private void addNewSyncExpColumnName() {
try {
database.beginTransaction();
final String addColumnSql = "ALTER TABLE LastSyncMetadata ADD COLUMN " +
newSyncExpColumnName + " TEXT";
database.execSQL(addColumnSql);
database.setTransactionSuccessful();
LOG.debug("Successfully upgraded LastSyncMetadata table with new field: " + newSyncExpColumnName);
} finally {
if (database.inTransaction()) {
database.endTransaction();
}
}
}

private boolean needsMigration() {
final String checkColumnSql = "SELECT COUNT(*) FROM pragma_table_info('LastSyncMetadata') " +
"WHERE name=" + Wrap.inSingleQuotes(newSyncExpColumnName);
try (Cursor queryResults = database.rawQuery(checkColumnSql, new String[]{})) {
if (queryResults.moveToNext()) {
int recordNum = queryResults.getInt(0);
return recordNum == 0; // needs to be upgraded if there's no column named ${newSyncExpColumnName}
}
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public class ModelMigrations {
public ModelMigrations(SQLiteDatabase databaseConnectionHandle, ModelProvider modelsProvider) {
List<ModelMigration> migrationClasses = new ArrayList<>();
migrationClasses.add(new AddModelNameToModelMetadataKey(databaseConnectionHandle, modelsProvider));
migrationClasses.add(new AddSyncExpressionToLastSyncMetadata(databaseConnectionHandle));
this.modelMigrations = Immutable.of(migrationClasses);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@
import androidx.annotation.Nullable;
import androidx.core.util.ObjectsCompat;

import com.amplifyframework.annotations.InternalApiWarning;
import com.amplifyframework.core.model.Model;
import com.amplifyframework.core.model.annotations.ModelConfig;
import com.amplifyframework.core.model.annotations.ModelField;
import com.amplifyframework.core.model.query.predicate.QueryPredicate;

import java.util.Objects;
import java.util.UUID;

/**
* Metadata about the last time that a model class was sync'd with AppSync backend.
* Metadata about the last time that a model class was sync'd with AppSync backend using a certain syncExpression.
* This metadata is persisted locally as a system model. This metadata is inspected
* whenever the Sync Engine starts up. The system consider the value of
* {@link LastSyncMetadata#getLastSyncTime()} to decide whether or not it should
Expand All @@ -39,13 +41,16 @@ public final class LastSyncMetadata implements Model {
private final @ModelField(targetType = "String", isRequired = true) String modelClassName;
private final @ModelField(targetType = "AWSTimestamp", isRequired = true) Long lastSyncTime;
private final @ModelField(targetType = "String", isRequired = true) String lastSyncType;
private final @ModelField(targetType = "String") QueryPredicate syncExpression;

@SuppressWarnings("checkstyle:ParameterName") // The field is named "id" in the model; keep it consistent
private LastSyncMetadata(String id, String modelClassName, Long lastSyncTime, SyncType syncType) {
private LastSyncMetadata(String id, String modelClassName, Long lastSyncTime,
SyncType syncType, @Nullable QueryPredicate syncExpression) {
this.id = id;
this.modelClassName = modelClassName;
this.lastSyncTime = lastSyncTime;
this.lastSyncType = syncType.name();
this.syncExpression = syncExpression;
}

/**
Expand All @@ -55,26 +60,65 @@ private LastSyncMetadata(String id, String modelClassName, Long lastSyncTime, Sy
* @param lastSyncTime Last time it was synced
* @param <T> t type of Model.
* @return {@link LastSyncMetadata} for the model class
* @deprecated use the override with SyncExpression parameter instead
*/
@InternalApiWarning
@Deprecated
public static <T extends Model> LastSyncMetadata baseSyncedAt(@NonNull String modelClassName,
@Nullable long lastSyncTime) {
@Nullable long lastSyncTime) {
Objects.requireNonNull(modelClassName);
return create(modelClassName, lastSyncTime, SyncType.BASE);
}

/**
* Creates an instance of an {@link LastSyncMetadata}, indicating that the provided
* model has been base sync'd, and that the last sync occurred at the given time.
* @param modelClassName Name of model
* @param lastSyncTime Last time it was synced
* @param syncExpression the corresponding sync expression being used during last sync
* @param <T> t type of Model.
* @return {@link LastSyncMetadata} for the model class
*/
@InternalApiWarning
public static <T extends Model> LastSyncMetadata baseSyncedAt(@NonNull String modelClassName,
@Nullable long lastSyncTime,
@Nullable QueryPredicate syncExpression) {
Objects.requireNonNull(modelClassName);
return create(modelClassName, lastSyncTime, SyncType.BASE, syncExpression);
}

/**
* Creates an instance of an {@link LastSyncMetadata}, indicating that the provided
* model has been base delta sync'd, and that the last sync occurred at the given time.
* @param modelClassName Name of model
* @param lastSyncTime Last time it was synced
* @param <T> t type of Model.
* @return {@link LastSyncMetadata} for the model class
* @deprecated use the override with SyncExpression parameter instead
*/
@Deprecated
static <T extends Model> LastSyncMetadata deltaSyncedAt(@NonNull String modelClassName,
@Nullable long lastSyncTime) {
@Nullable long lastSyncTime) {
Objects.requireNonNull(modelClassName);
return create(modelClassName, lastSyncTime, SyncType.DELTA);
}

/**
* Creates an instance of an {@link LastSyncMetadata}, indicating that the provided
* model has been base delta sync'd, and that the last sync occurred at the given time.
* @param modelClassName Name of model
* @param lastSyncTime Last time it was synced
* @param syncExpression the corresponding sync expression being used during last sync
* @param <T> t type of Model.
* @return {@link LastSyncMetadata} for the model class
*/
static <T extends Model> LastSyncMetadata deltaSyncedAt(@NonNull String modelClassName,
@Nullable long lastSyncTime,
@Nullable QueryPredicate syncExpression) {
Objects.requireNonNull(modelClassName);
return create(modelClassName, lastSyncTime, SyncType.DELTA, syncExpression);
}

/**
* Creates an {@link LastSyncMetadata} indicating that the provided model class
* has never been synced.
Expand All @@ -84,7 +128,7 @@ static <T extends Model> LastSyncMetadata deltaSyncedAt(@NonNull String modelCla
*/
public static <T extends Model> LastSyncMetadata neverSynced(@NonNull String modelClassName) {
Objects.requireNonNull(modelClassName);
return create(modelClassName, null, SyncType.BASE);
return create(modelClassName, null, SyncType.BASE, null);
}

/**
Expand All @@ -94,12 +138,33 @@ public static <T extends Model> LastSyncMetadata neverSynced(@NonNull String mod
* @param syncType The type of sync (FULL or DELTA).
* @param <T> Type of model
* @return {@link LastSyncMetadata}
* @deprecated use the override with SyncExpression parameter instead
*/
@SuppressWarnings("WeakerAccess")
static <T extends Model> LastSyncMetadata create(
@NonNull String modelClassName, @Nullable Long lastSyncTime, @NonNull SyncType syncType) {
@Deprecated
static <T extends Model> LastSyncMetadata create(@NonNull String modelClassName,
@Nullable Long lastSyncTime,
@NonNull SyncType syncType) {
Objects.requireNonNull(modelClassName);
return new LastSyncMetadata(hash(modelClassName), modelClassName, lastSyncTime, syncType);
return new LastSyncMetadata(hash(modelClassName), modelClassName, lastSyncTime, syncType, null);
}

/**
* Creates an {@link LastSyncMetadata} for the provided model class.
* @param modelClassName Name of model class for which metadata pertains
* @param lastSyncTime Time of last sync; null, if never.
* @param syncType The type of sync (FULL or DELTA).
* @param syncExpression the corresponding sync expression being used during last sync
* @param <T> Type of model
* @return {@link LastSyncMetadata}
*/
@SuppressWarnings("WeakerAccess")
static <T extends Model> LastSyncMetadata create(@NonNull String modelClassName,
@Nullable Long lastSyncTime,
@NonNull SyncType syncType,
@Nullable QueryPredicate syncExpression) {
Objects.requireNonNull(modelClassName);
return new LastSyncMetadata(hash(modelClassName), modelClassName, lastSyncTime, syncType, syncExpression);
}

@NonNull
Expand Down Expand Up @@ -144,6 +209,14 @@ public String getLastSyncType() {
return lastSyncType;
}

/**
* Returns the sync expression being used in the last sync.
* @return A serialized sync expression
*/
QueryPredicate getSyncExpression() {
return this.syncExpression;
}

/**
* Computes a stable hash for a model class, by its name.
* Since {@link Model}s have to have unique IDs, we need an ID for this class.
Expand Down Expand Up @@ -175,6 +248,9 @@ public boolean equals(Object thatObject) {
if (!ObjectsCompat.equals(lastSyncType, that.lastSyncType)) {
return false;
}
if (!ObjectsCompat.equals(syncExpression, that.syncExpression)) {
return false;
}
return ObjectsCompat.equals(lastSyncTime, that.lastSyncTime);
}

Expand All @@ -184,6 +260,7 @@ public int hashCode() {
result = 31 * result + modelClassName.hashCode();
result = 31 * result + lastSyncTime.hashCode();
result = 31 * result + lastSyncType.hashCode();
result = 31 * result + syncExpression.hashCode();
return result;
}

Expand All @@ -194,6 +271,7 @@ public String toString() {
", modelClassName='" + modelClassName + '\'' +
", lastSyncTime=" + lastSyncTime +
", lastSyncType=" + lastSyncType +
", syncExpression=" + syncExpression +
'}';
}
}
Loading
Loading