diff --git a/aws-datastore/src/androidTest/java/com/amplifyframework/datastore/storage/sqlite/SQLiteStorageAdapterDeleteWithCpkTest.java b/aws-datastore/src/androidTest/java/com/amplifyframework/datastore/storage/sqlite/SQLiteStorageAdapterDeleteWithCpkTest.java new file mode 100644 index 0000000000..1f06326feb --- /dev/null +++ b/aws-datastore/src/androidTest/java/com/amplifyframework/datastore/storage/sqlite/SQLiteStorageAdapterDeleteWithCpkTest.java @@ -0,0 +1,120 @@ +/* + * Copyright 2020 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; + +import com.amplifyframework.core.model.Model; +import com.amplifyframework.core.model.query.predicate.QueryPredicates; +import com.amplifyframework.datastore.DataStoreException; +import com.amplifyframework.datastore.StrictMode; +import com.amplifyframework.datastore.storage.StorageItemChange; +import com.amplifyframework.datastore.storage.SynchronousStorageAdapter; +import com.amplifyframework.testmodels.customprimarykey.AmplifyModelProvider; +import com.amplifyframework.testmodels.customprimarykey.Comment; + +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.HashSet; +import java.util.Set; + +import io.reactivex.rxjava3.observers.TestObserver; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Test the delete functionality of {@link SQLiteStorageAdapter} operations. + */ +public final class SQLiteStorageAdapterDeleteWithCpkTest { + private SynchronousStorageAdapter adapter; + + /** + * Enables strict mode, for the purpose of catching some common errors while using + * a SQL data-base, such as forgetting to close it when done. + */ + @BeforeClass + public static void enableStrictMode() { + StrictMode.enable(); + } + + /** + * Clear the storage adapter, and then provision a new one that will allow us + * to store the Comments-Blog models. + */ + @Before + public void setup() { + TestStorageAdapter.cleanup(); + this.adapter = TestStorageAdapter.create(AmplifyModelProvider.getInstance()); + } + + /** + * Close the storage adapter, and cleanup any database files it left. + */ + @After + public void teardown() { + TestStorageAdapter.cleanup(adapter); + } + + /** + * Assert that delete model type with predicate deletes items in + * the SQLite database without violating foreign key constraints. + * @throws DataStoreException On unexpected failure manipulating items in/out of DataStore + */ + @Test + public void deleteCustomPrimaryKeyModelTypeWithDeleteAllPredicateCascades() throws DataStoreException { + // Create 1 post, which has 3 comments each + Set expected = new HashSet<>(); + com.amplifyframework.testmodels.customprimarykey.Post + postModel = com.amplifyframework.testmodels.customprimarykey.Post.builder() + .title("test post") + .id("testPostId") + .build(); + adapter.save(postModel); + expected.add(postModel.getPrimaryKeyString()); + for (int comment = 1; comment <= 3; comment++) { + Comment commentModel = Comment.builder() + .title("comment " + comment) + .content("content " + comment) + .likes(2) + .description("description " + comment) + .post(postModel) + .build(); + adapter.save(commentModel); + expected.add(commentModel.getPrimaryKeyString()); + } + // Observe deletions + TestObserver deleteObserver = adapter.observe() + .filter(change -> StorageItemChange.Type.DELETE.equals(change.type())) + .map(StorageItemChange::item) + .map(Model::getPrimaryKeyString) + .test(); + + // Triggers a delete of all blogs. + // All posts will be deleted by cascade. + adapter.delete(com.amplifyframework.testmodels.customprimarykey.Post.class, QueryPredicates.all()); + + // Assert 3 comments. + deleteObserver.assertValueCount(4); + assertEquals(expected, new HashSet<>(deleteObserver.values())); + + // Get the Post and Comments from the database. Should be deleted. + assertTrue(adapter.query(com.amplifyframework.testmodels.customprimarykey.Post.class).isEmpty()); + assertTrue(adapter.query(Comment.class).isEmpty()); + + } +} diff --git a/aws-datastore/src/main/java/com/amplifyframework/datastore/storage/sqlite/SQLiteStorageAdapter.java b/aws-datastore/src/main/java/com/amplifyframework/datastore/storage/sqlite/SQLiteStorageAdapter.java index 87e45fd0a4..851c797146 100644 --- a/aws-datastore/src/main/java/com/amplifyframework/datastore/storage/sqlite/SQLiteStorageAdapter.java +++ b/aws-datastore/src/main/java/com/amplifyframework/datastore/storage/sqlite/SQLiteStorageAdapter.java @@ -563,15 +563,21 @@ public void delete( QueryOptions options = Where.matches(predicate); try (Cursor cursor = sqlCommandProcessor.rawQuery(sqlCommandFactory.queryFor(modelSchema, options))) { final SQLiteTable sqliteTable = SQLiteTable.fromSchema(modelSchema); - final String primaryKeyName = sqliteTable.getPrimaryKey().getAliasedName(); + final List primaryKeyNames = modelSchema.getPrimaryIndexFields(); // identify items that meet the predicate List items = new ArrayList<>(); if (cursor != null && cursor.moveToFirst()) { - int index = cursor.getColumnIndexOrThrow(primaryKeyName); + /** Populate the mapOfModelPrimaryKeys with the values of + * the primary key/ keys for the model**/ do { - String id = cursor.getString(index); - String dummyJson = gson.toJson(Collections.singletonMap("id", id)); + HashMap mapOfModelPrimaryKeys = new HashMap<>(); + for (String field : primaryKeyNames) { + int index = cursor.getColumnIndexOrThrow(sqliteTable.getName() + "_" + field); + String fieldValue = cursor.getString(index); + mapOfModelPrimaryKeys.put(field, fieldValue); + } + String dummyJson = gson.toJson(mapOfModelPrimaryKeys); T dummyItem = gson.fromJson(dummyJson, itemClass); items.add(dummyItem); } while (cursor.moveToNext()); diff --git a/testmodels/src/main/java/com/amplifyframework/testmodels/customprimarykey/AmplifyModelProvider.java b/testmodels/src/main/java/com/amplifyframework/testmodels/customprimarykey/AmplifyModelProvider.java new file mode 100644 index 0000000000..4d071e2476 --- /dev/null +++ b/testmodels/src/main/java/com/amplifyframework/testmodels/customprimarykey/AmplifyModelProvider.java @@ -0,0 +1,54 @@ +package com.amplifyframework.testmodels.customprimarykey; + +import com.amplifyframework.core.model.Model; +import com.amplifyframework.core.model.ModelProvider; +import com.amplifyframework.util.Immutable; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * Contains the set of model classes that implement {@link Model} + * interface. + */ + +public final class AmplifyModelProvider implements ModelProvider { + private static final String AMPLIFY_MODEL_VERSION = "676dbf8de03bdea7d825974b4ba506c0"; + private static AmplifyModelProvider amplifyGeneratedModelInstance; + private AmplifyModelProvider() { + + } + + public static AmplifyModelProvider getInstance() { + if (amplifyGeneratedModelInstance == null) { + amplifyGeneratedModelInstance = new AmplifyModelProvider(); + } + return amplifyGeneratedModelInstance; + } + + /** + * Get a set of the model classes. + * + * @return a set of the model classes. + */ + @Override + public Set> models() { + final Set> modifiableSet = new HashSet<>( + Arrays.>asList(Blog.class, Post.class, Comment.class, ModelCompositeMultiplePk.class, BlogWithDefaultHasOne.class, User.class, BlogWithCustomHasOne.class) + ); + + return Immutable.of(modifiableSet); + + } + + /** + * Get the version of the models. + * + * @return the version string of the models. + */ + @Override + public String version() { + return AMPLIFY_MODEL_VERSION; + } +}