Skip to content

Commit

Permalink
Closes #225 Delete Data using a WHERE clause
Browse files Browse the repository at this point in the history
  • Loading branch information
vestrel00 committed Jun 16, 2022
1 parent 32ad28a commit b4e56e7
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 43 deletions.
9 changes: 3 additions & 6 deletions core/src/main/java/contacts/core/Delete.kt
Original file line number Diff line number Diff line change
Expand Up @@ -229,22 +229,19 @@ interface Delete : CrudApi {
*
* ## [commit] vs [commitInOneTransaction]
*
* If you specified RawContacts in any of;
* If you used several of the following in one call,
*
* - [rawContacts]
* - [rawContactsWithId]
* - [rawContactsWhere]
* - [rawContactsWhereData]
*
* that belong to a Contact that you also specified in any of;
*
* - [contacts]
* - [contactsWithId]
* - [contactsWhere]
* - [contactsWhereData]
*
* then this may return false even if the Contact and RawContacts were actually deleted IF
* you used [commit]. Using [commitInOneTransaction] does not have this limitation.
* then this value may be false even if the Contact and RawContacts were actually deleted if
* you used [commit]. Using [commitInOneTransaction] does not have this "issue".
*/
val isSuccessful: Boolean

Expand Down
140 changes: 108 additions & 32 deletions core/src/main/java/contacts/core/data/DataDelete.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package contacts.core.data

import android.content.ContentProviderOperation
import android.content.ContentProviderOperation.newDelete
import android.content.ContentResolver
import contacts.core.*
import contacts.core.entities.ExistingDataEntity
import contacts.core.entities.operation.withSelection
import contacts.core.entities.table.ProfileUris
import contacts.core.entities.table.Table
import contacts.core.util.*
import contacts.core.util.applyBatch
import contacts.core.util.deleteSuccess
import contacts.core.util.isProfileId
Expand Down Expand Up @@ -77,6 +79,17 @@ interface DataDelete : CrudApi {
*/
fun dataWithId(dataIds: Sequence<Long>): DataDelete

/**
* Deletes all of the data that match the given [where].
*/
fun dataWhere(where: Where<AbstractDataField>?): DataDelete

/**
* Same as [DataDelete.dataWhere] except you have direct access to all properties of [Fields]
* in the function parameter. Use this to shorten your code.
*/
fun dataWhere(where: Fields.() -> Where<AbstractDataField>?): DataDelete

/**
* Deletes the [ExistingDataEntity]s in the queue (added via [data]) and returns the [Result].
*
Expand Down Expand Up @@ -124,19 +137,41 @@ interface DataDelete : CrudApi {
/**
* True if all specified or matching data have successfully been deleted. False if even one
* delete failed.
*
* ## [commit] vs [commitInOneTransaction]
*
* If you used several of the following in one call,
*
* - [data]
* - [dataWithId]
* - [dataWhere]
*
* then this value may be false even if the data were actually deleted if you used [commit].
* Using [commitInOneTransaction] does not have this "issue".
*/
val isSuccessful: Boolean

/**
* True if the [data] has been successfully deleted. False otherwise.
*
* This is used in conjunction with [DataDelete.data].
*/
fun isSuccessful(data: ExistingDataEntity): Boolean

/**
* True if the data with the given [dataId] has been successfully deleted. False otherwise.
*
* This is used in conjunction with [DataDelete.dataWithId].
*/
fun isSuccessful(dataId: Long): Boolean

/**
* True if the delete operation using the given [where] was successful.
*
* This is used in conjunction with [DataDelete.dataWhere].
*/
fun isSuccessful(where: Where<AbstractDataField>): Boolean

// We have to cast the return type because we are not using recursive generic types.
override fun redactedCopy(): Result
}
Expand All @@ -152,23 +187,30 @@ private class DataDeleteImpl(
private val isProfile: Boolean,

private val dataIds: MutableSet<Long> = mutableSetOf(),
private var dataWhere: Where<AbstractDataField>? = null,

override val isRedacted: Boolean = false
) : DataDelete {

private val hasNothingToCommit: Boolean
get() = dataIds.isEmpty() && dataWhere == null

override fun toString(): String =
"""
DataDelete {
isProfile: $isProfile
dataIds: $dataIds
dataWhere: $dataWhere
hasPermission: ${permissions.canUpdateDelete()}
isRedacted: $isRedacted
}
""".trimIndent()

override fun redactedCopy(): DataDelete = DataDeleteImpl(
contactsApi, isProfile,
dataIds,
contactsApi,
isProfile = isProfile,
dataIds = dataIds,
dataWhere = dataWhere?.redactedCopy(),
isRedacted = true
)

Expand All @@ -186,26 +228,37 @@ private class DataDeleteImpl(
this.dataIds.addAll(dataIds)
}

override fun dataWhere(where: Where<AbstractDataField>?): DataDelete = apply {
dataWhere = where?.redactedCopyOrThis(isRedacted)
}

override fun dataWhere(where: Fields.() -> Where<AbstractDataField>?) = dataWhere(where(Fields))

override fun commit(): DataDelete.Result {
onPreExecute()

return if (dataIds.isEmpty() || !permissions.canUpdateDelete()) {
DataDeleteResult(emptyMap())
return if (!permissions.canUpdateDelete() || hasNothingToCommit) {
DataDeleteAllResult(isSuccessful = false)
} else {
val dataIdsResultMap = mutableMapOf<Long, Boolean>()
for (dataId in dataIds) {
dataIdsResultMap[dataId] =
if (dataId.isProfileId != isProfile) {
// Intentionally fail the operation to ensure that this is only used for profile
// or non-profile deletes. Otherwise, operation can succeed. This is only done
// to enforce API design.
// Intentionally fail the operation to ensure that this is only used for
// profile or non-profile deletes. Otherwise, operation can succeed. This
// is only done to enforce API design.
false
} else {
contentResolver.deleteDataWithId(dataId)
contentResolver.deleteDataWhere(Fields.DataId equalTo dataId, isProfile)
}
}

DataDeleteResult(dataIdsResultMap)
val whereResultMap = mutableMapOf<String, Boolean>()
dataWhere?.let {
whereResultMap[it.toString()] = contentResolver.deleteDataWhere(it, isProfile)
}

DataDeleteResult(dataIdsResultMap, whereResultMap)
}
.redactedCopyOrThis(isRedacted)
.also { onPostExecute(contactsApi, it) }
Expand All @@ -214,70 +267,91 @@ private class DataDeleteImpl(
override fun commitInOneTransaction(): DataDelete.Result {
onPreExecute()

// I know this if-else can be folded. But this is way more readable IMO =)
val isSuccessful = if (dataIds.isEmpty() || !permissions.canUpdateDelete()) {
false
return if (!permissions.canUpdateDelete() || hasNothingToCommit) {
DataDeleteAllResult(isSuccessful = false)
} else {
val validDataIds = dataIds.filter { it.isProfileId == isProfile }

if (dataIds.size != validDataIds.size) {
// There are some invalid ids or profile or non-profile data ids, fail without
// performing operation.
false
DataDeleteAllResult(isSuccessful = false)
} else {
contentResolver.deleteDataRowsWithIds(dataIds, isProfile)
val operations = arrayListOf<ContentProviderOperation>()

if (validDataIds.isNotEmpty()) {
deleteOperationFor(Fields.DataId `in` validDataIds, isProfile)
.let(operations::add)
}

dataWhere?.let {
deleteOperationFor(it, isProfile).let(operations::add)
}

DataDeleteAllResult(isSuccessful = contentResolver.applyBatch(operations).deleteSuccess)
}
}

return DataDeleteAllResult(isSuccessful)
.redactedCopyOrThis(isRedacted)
.also { onPostExecute(contactsApi, it) }
}
}

private fun ContentResolver.deleteDataWithId(dataId: Long): Boolean = applyBatch(
newDelete(if (dataId.isProfileId) ProfileUris.DATA.uri else Table.Data.uri)
.withSelection(Fields.DataId equalTo dataId)
.build()
).deleteSuccess
private fun ContentResolver.deleteDataWhere(
where: Where<AbstractDataField>, isProfile: Boolean
): Boolean = applyBatch(deleteOperationFor(where, isProfile)).deleteSuccess

private fun ContentResolver.deleteDataRowsWithIds(
dataIds: Collection<Long>, isProfile: Boolean
): Boolean = applyBatch(
newDelete(if (isProfile) ProfileUris.DATA.uri else Table.Data.uri)
.withSelection(Fields.DataId `in` dataIds)
.build()
).deleteSuccess
private fun deleteOperationFor(
where: Where<AbstractDataField>, isProfile: Boolean
): ContentProviderOperation = newDelete(if (isProfile) ProfileUris.DATA.uri else Table.Data.uri)
.withSelection(where)
.build()

private class DataDeleteResult private constructor(
private val dataIdsResultMap: Map<Long, Boolean>,
private var whereResultMap: Map<String, Boolean>,
override val isRedacted: Boolean
) : DataDelete.Result {

constructor(dataIdsResultMap: Map<Long, Boolean>) : this(dataIdsResultMap, false)
constructor(
dataIdsResultMap: Map<Long, Boolean>,
whereResultMap: Map<String, Boolean>
) : this(dataIdsResultMap, whereResultMap, false)

override fun toString(): String =
"""
DataDelete.Result {
isSuccessful: $isSuccessful
dataIdsResultMap: $dataIdsResultMap
whereResultMap: $whereResultMap
isRedacted: $isRedacted
}
""".trimIndent()

override fun redactedCopy(): DataDelete.Result = DataDeleteResult(
dataIdsResultMap,
dataIdsResultMap = dataIdsResultMap,
whereResultMap = whereResultMap.redactedStringKeys(),
isRedacted = true
)

override val isSuccessful: Boolean by unsafeLazy {
// By default, all returns true when the collection is empty. So, we override that.
dataIdsResultMap.run { isNotEmpty() && all { it.value } }
if (dataIdsResultMap.isEmpty() && whereResultMap.isEmpty()
) {
// Deleting nothing is NOT successful.
false
} else {
// A set has failure if it is NOT empty and one of its entries is false.
val hasDataFailure = dataIdsResultMap.any { !it.value }
val hasWhereFailure = whereResultMap.any { !it.value }
!hasDataFailure && !hasWhereFailure
}
}

override fun isSuccessful(data: ExistingDataEntity): Boolean = isSuccessful(data.id)

override fun isSuccessful(dataId: Long): Boolean = dataIdsResultMap.getOrElse(dataId) { false }

override fun isSuccessful(where: Where<AbstractDataField>): Boolean =
whereResultMap.getOrElse(where.toString()) { false }
}

private class DataDeleteAllResult private constructor(
Expand Down Expand Up @@ -306,4 +380,6 @@ private class DataDeleteAllResult private constructor(
override fun isSuccessful(data: ExistingDataEntity): Boolean = isSuccessful

override fun isSuccessful(dataId: Long): Boolean = isSuccessful

override fun isSuccessful(where: Where<AbstractDataField>): Boolean = isSuccessful
}
29 changes: 24 additions & 5 deletions docs/data/delete-data-sets.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ val delete = Contacts(context).data().delete()
To delete a set of data,

```kotlin
val deleteResult = Contacts(context)
val deleteResult = delete
.data()
.delete()
.data(data)
Expand All @@ -27,7 +27,7 @@ val deleteResult = Contacts(context)
If you want to delete a list of emails and phones,

```kotlin
val deleteResult = Contacts(context)
val deleteResult = delete
.data()
.delete()
.data(emails + phones)
Expand All @@ -37,13 +37,23 @@ val deleteResult = Contacts(context)
If you want to delete a set of data using data IDs,

```kotlin
val deleteResult = Contacts(context)
val deleteResult = delete
.data()
.delete()
.dataWithId(1, 2, 3)
.commit()
```

## An advanced delete

You may specify a matching criteria, like in queries, that will delete all matching data,

```kotlin
val deleteResult = delete
.dataWhere { Email.Address endsWith "@yahoo.com" }
.commit()
```

## Executing the delete

To execute the delete,
Expand All @@ -52,7 +62,7 @@ To execute the delete,
.commit()
```

If you want to delete all given data in a single atomic transaction,
If you want to delete all specified data in a single atomic transaction,

```kotlin
.commitInOneTransaction()
Expand All @@ -75,7 +85,16 @@ val allDeletesSuccessful = deleteResult.isSuccessful
To check if a particular delete succeeded,

```kotlin
val firstDeleteSuccessful = deleteResult.isSuccessful(data1)
val dataDeleteSuccessful = deleteResult.isSuccessful(data)
val dataDeleteSuccessful = deleteResult.isSuccessful(data.id)
```

To check if a particular advanced delete managed to delete at least one matching data,

```kotlin
val where = Fields.Email.Address endsWith "@yahoo.com"
val deleteResult = delete.dataWhere(where).commit()
val advancedDeleteSuccessful = deleteResult.isSuccessful(where)
```

## Performing the delete and result processing asynchronously
Expand Down

0 comments on commit b4e56e7

Please sign in to comment.