[db] improve loading of versions

faster permission loading and more tests
This commit is contained in:
Torsten Grote
2022-06-08 10:52:31 -03:00
parent a47ab38533
commit dfd8eb178f
6 changed files with 226 additions and 108 deletions

View File

@@ -30,6 +30,7 @@ internal abstract class DbTest {
internal lateinit var repoDao: RepositoryDaoInt
internal lateinit var appDao: AppDaoInt
internal lateinit var appPrefsDao: AppPrefsDaoInt
internal lateinit var versionDao: VersionDaoInt
internal lateinit var db: FDroidDatabaseInt
private val testCoroutineDispatcher = Dispatchers.Unconfined
@@ -45,6 +46,7 @@ internal abstract class DbTest {
.build()
repoDao = db.getRepositoryDao()
appDao = db.getAppDao()
appPrefsDao = db.getAppPrefsDao()
versionDao = db.getVersionDao()
mockkObject(FDroidDatabaseHolder)

View File

@@ -11,6 +11,7 @@ import org.junit.Assert
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.test.assertEquals
import kotlin.test.fail
internal object TestUtils {
@@ -106,4 +107,7 @@ internal object TestUtils {
return data[0] as T?
}
fun <T> LiveData<T>.getOrFail(): T {
return getOrAwaitValue() ?: fail()
}
}

View File

@@ -2,7 +2,8 @@ package org.fdroid.database
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.fdroid.database.TestUtils.getOrAwaitValue
import org.fdroid.database.TestUtils.getOrFail
import org.fdroid.index.v2.PackageVersionV2
import org.fdroid.test.TestAppUtils.getRandomMetadataV2
import org.fdroid.test.TestRepoUtils.getRandomRepo
import org.fdroid.test.TestUtils.getRandomString
@@ -20,24 +21,46 @@ internal class VersionTest : DbTest() {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
private val packageId = getRandomString()
private val versionId = getRandomString()
private val packageName = getRandomString()
private val packageVersion1 = getRandomPackageVersionV2()
private val packageVersion2 = getRandomPackageVersionV2()
private val packageVersion3 = getRandomPackageVersionV2()
private val versionId1 = packageVersion1.file.sha256
private val versionId2 = packageVersion2.file.sha256
private val versionId3 = packageVersion3.file.sha256
private val isCompatible1 = Random.nextBoolean()
private val isCompatible2 = Random.nextBoolean()
private val packageVersions = mapOf(
versionId1 to packageVersion1,
versionId2 to packageVersion2,
)
private fun getVersion1(repoId: Long) =
packageVersion1.toVersion(repoId, packageName, versionId1, isCompatible1)
private fun getVersion2(repoId: Long) =
packageVersion2.toVersion(repoId, packageName, versionId2, isCompatible2)
private val compatChecker: (PackageVersionV2) -> Boolean = {
when (it.file.sha256) {
versionId1 -> isCompatible1
versionId2 -> isCompatible2
else -> fail()
}
}
@Test
fun insertGetDeleteSingleVersion() {
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageId, getRandomMetadataV2())
val packageVersion = getRandomPackageVersionV2()
val isCompatible = Random.nextBoolean()
versionDao.insert(repoId, packageId, versionId, packageVersion, isCompatible)
appDao.insert(repoId, packageName, getRandomMetadataV2())
versionDao.insert(repoId, packageName, versionId1, packageVersion1, isCompatible1)
val appVersions = versionDao.getAppVersions(repoId, packageId)
val appVersions = versionDao.getAppVersions(repoId, packageName)
assertEquals(1, appVersions.size)
val appVersion = appVersions[0]
assertEquals(versionId, appVersion.version.versionId)
val version = packageVersion.toVersion(repoId, packageId, versionId, isCompatible)
assertEquals(version, appVersion.version)
val manifest = packageVersion.manifest
assertEquals(versionId1, appVersion.version.versionId)
assertEquals(getVersion1(repoId), appVersion.version)
val manifest = packageVersion1.manifest
assertEquals(manifest.usesPermission.toSet(), appVersion.usesPermission?.toSet())
assertEquals(manifest.usesPermissionSdk23.toSet(), appVersion.usesPermissionSdk23?.toSet())
assertEquals(
@@ -45,42 +68,35 @@ internal class VersionTest : DbTest() {
appVersion.version.manifest.features?.toSet()
)
val versionedStrings = versionDao.getVersionedStrings(repoId, packageId)
val versionedStrings = versionDao.getVersionedStrings(repoId, packageName)
val expectedSize = manifest.usesPermission.size + manifest.usesPermissionSdk23.size
assertEquals(expectedSize, versionedStrings.size)
versionDao.deleteAppVersion(repoId, packageId, versionId)
assertEquals(0, versionDao.getAppVersions(repoId, packageId).size)
assertEquals(0, versionDao.getVersionedStrings(repoId, packageId).size)
versionDao.deleteAppVersion(repoId, packageName, versionId1)
assertEquals(0, versionDao.getAppVersions(repoId, packageName).size)
assertEquals(0, versionDao.getVersionedStrings(repoId, packageName).size)
}
@Test
fun insertGetDeleteTwoVersions() {
// insert two versions along with required objects
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageId, getRandomMetadataV2())
val packageVersion1 = getRandomPackageVersionV2()
val packageVersion2 = getRandomPackageVersionV2()
val version1 = getRandomString()
val version2 = getRandomString()
val isCompatible1 = Random.nextBoolean()
val isCompatible2 = Random.nextBoolean()
versionDao.insert(repoId, packageId, version1, packageVersion1, isCompatible1)
versionDao.insert(repoId, packageId, version2, packageVersion2, isCompatible2)
appDao.insert(repoId, packageName, getRandomMetadataV2())
versionDao.insert(repoId, packageName, versionId1, packageVersion1, isCompatible1)
versionDao.insert(repoId, packageName, versionId2, packageVersion2, isCompatible2)
// get app versions from DB and assign them correctly
val appVersions = versionDao.getAppVersions(packageId).getOrAwaitValue() ?: fail()
val appVersions = versionDao.getAppVersions(packageName).getOrFail()
assertEquals(2, appVersions.size)
val appVersion = if (version1 == appVersions[0].version.versionId) {
val appVersion = if (versionId1 == appVersions[0].version.versionId) {
appVersions[0]
} else appVersions[1]
val appVersion2 = if (version2 == appVersions[0].version.versionId) {
val appVersion2 = if (versionId2 == appVersions[0].version.versionId) {
appVersions[0]
} else appVersions[1]
// check first version matches
val exVersion1 = packageVersion1.toVersion(repoId, packageId, version1, isCompatible1)
assertEquals(exVersion1, appVersion.version)
assertEquals(getVersion1(repoId), appVersion.version)
val manifest = packageVersion1.manifest
assertEquals(manifest.usesPermission.toSet(), appVersion.usesPermission?.toSet())
assertEquals(manifest.usesPermissionSdk23.toSet(), appVersion.usesPermissionSdk23?.toSet())
@@ -90,8 +106,7 @@ internal class VersionTest : DbTest() {
)
// check second version matches
val exVersion2 = packageVersion2.toVersion(repoId, packageId, version2, isCompatible2)
assertEquals(exVersion2, appVersion2.version)
assertEquals(getVersion2(repoId), appVersion2.version)
val manifest2 = packageVersion2.manifest
assertEquals(manifest2.usesPermission.toSet(), appVersion2.usesPermission?.toSet())
assertEquals(manifest2.usesPermissionSdk23.toSet(),
@@ -102,9 +117,118 @@ internal class VersionTest : DbTest() {
)
// delete app and check that all associated data also gets deleted
appDao.deleteAppMetadata(repoId, packageId)
assertEquals(0, versionDao.getAppVersions(repoId, packageId).size)
assertEquals(0, versionDao.getVersionedStrings(repoId, packageId).size)
appDao.deleteAppMetadata(repoId, packageName)
assertEquals(0, versionDao.getAppVersions(repoId, packageName).size)
assertEquals(0, versionDao.getVersionedStrings(repoId, packageName).size)
}
@Test
fun versionsOnlyFromEnabledRepo() {
// insert two versions into the same repo
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName, getRandomMetadataV2())
versionDao.insert(repoId, packageName, packageVersions, compatChecker)
assertEquals(2, versionDao.getAppVersions(packageName).getOrFail().size)
assertEquals(2, versionDao.getVersions(listOf(packageName)).size)
// add another version into another repo
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId2, packageName, getRandomMetadataV2())
versionDao.insert(repoId2, packageName, versionId3, packageVersion3, true)
assertEquals(3, versionDao.getAppVersions(packageName).getOrFail().size)
assertEquals(3, versionDao.getVersions(listOf(packageName)).size)
// disable second repo
repoDao.setRepositoryEnabled(repoId2, false)
// now only two versions get returned
assertEquals(2, versionDao.getAppVersions(packageName).getOrFail().size)
assertEquals(2, versionDao.getVersions(listOf(packageName)).size)
}
@Test
fun versionsSortedByVersionCode() {
// insert three versions into the same repo
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName, getRandomMetadataV2())
versionDao.insert(repoId, packageName, packageVersions, compatChecker)
versionDao.insert(repoId, packageName, versionId3, packageVersion3, true)
val versions1 = versionDao.getAppVersions(packageName).getOrFail()
val versions2 = versionDao.getVersions(listOf(packageName))
assertEquals(3, versions1.size)
assertEquals(3, versions2.size)
// check that they are sorted as expected
listOf(
packageVersion1.manifest.versionCode,
packageVersion2.manifest.versionCode,
packageVersion3.manifest.versionCode,
).sortedDescending().forEachIndexed { i, versionCode ->
assertEquals(versionCode, versions1[i].version.manifest.versionCode)
assertEquals(versionCode, versions2[i].versionCode)
}
}
@Test
fun getVersionsRespectsAppPrefsIgnore() {
// insert one version into the repo
val repoId = repoDao.insertOrReplace(getRandomRepo())
val versionCode = Random.nextLong(1, Long.MAX_VALUE)
val packageVersion = getRandomPackageVersionV2(versionCode)
val versionId = packageVersion.file.sha256
appDao.insert(repoId, packageName, getRandomMetadataV2())
versionDao.insert(repoId, packageName, versionId, packageVersion, true)
assertEquals(1, versionDao.getVersions(listOf(packageName)).size)
// default app prefs don't change result
var appPrefs = AppPrefs(packageName)
appPrefsDao.update(appPrefs)
assertEquals(1, versionDao.getVersions(listOf(packageName)).size)
// ignore lower version code doesn't change result
appPrefs = appPrefs.toggleIgnoreVersionCodeUpdate(versionCode - 1)
appPrefsDao.update(appPrefs)
assertEquals(1, versionDao.getVersions(listOf(packageName)).size)
// ignoring exact version code does change result
appPrefs = appPrefs.toggleIgnoreVersionCodeUpdate(versionCode)
appPrefsDao.update(appPrefs)
assertEquals(0, versionDao.getVersions(listOf(packageName)).size)
// ignoring higher version code does change result
appPrefs = appPrefs.toggleIgnoreVersionCodeUpdate(versionCode + 1)
appPrefsDao.update(appPrefs)
assertEquals(0, versionDao.getVersions(listOf(packageName)).size)
// ignoring all updates does change result
appPrefs = appPrefs.toggleIgnoreAllUpdates()
appPrefsDao.update(appPrefs)
assertEquals(0, versionDao.getVersions(listOf(packageName)).size)
// not ignoring all updates brings back version
appPrefs = appPrefs.toggleIgnoreAllUpdates()
appPrefsDao.update(appPrefs)
assertEquals(1, versionDao.getVersions(listOf(packageName)).size)
}
@Test
fun getVersionsConsidersOnlyGivenPackages() {
// insert two versions
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName, getRandomMetadataV2())
versionDao.insert(repoId, packageName, packageVersions, compatChecker)
assertEquals(2, versionDao.getVersions(listOf(packageName)).size)
// insert versions for a different package
val packageName2 = getRandomString()
appDao.insert(repoId, packageName2, getRandomMetadataV2())
versionDao.insert(repoId, packageName2, packageVersions, compatChecker)
// still only returns above versions
assertEquals(2, versionDao.getVersions(listOf(packageName)).size)
// all versions are returned only if all packages are asked for
assertEquals(4, versionDao.getVersions(listOf(packageName, packageName2)).size)
}
}

View File

@@ -73,7 +73,7 @@ public class DbUpdateChecker(
releaseChannels) ?: return null
val versionedStrings = versionDao.getVersionedStrings(
repoId = version.repoId,
packageId = version.packageId,
packageName = version.packageId,
versionId = version.versionId,
)
return version.toAppVersion(versionedStrings)
@@ -109,7 +109,7 @@ public class DbUpdateChecker(
private fun getUpdatableApp(version: Version, installedVersionCode: Long): UpdatableApp? {
val versionedStrings = versionDao.getVersionedStrings(
repoId = version.repoId,
packageId = version.packageId,
packageName = version.packageId,
versionId = version.versionId,
)
val appOverviewItem =

View File

@@ -4,6 +4,7 @@ import androidx.core.os.LocaleListCompat
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Relation
import org.fdroid.database.VersionedStringType.PERMISSION
import org.fdroid.database.VersionedStringType.PERMISSION_SDK_23
import org.fdroid.index.v2.ANTI_FEATURE_KNOWN_VULNERABILITY
@@ -48,8 +49,7 @@ public data class Version(
internal fun toAppVersion(versionedStrings: List<VersionedString>): AppVersion = AppVersion(
version = this,
usesPermission = versionedStrings.getPermissions(this),
usesPermissionSdk23 = versionedStrings.getPermissionsSdk23(this),
versionedStrings = versionedStrings,
)
}
@@ -72,10 +72,13 @@ internal fun PackageVersionV2.toVersion(
isCompatible = isCompatible,
)
public data class AppVersion(
internal val version: Version,
val usesPermission: List<PermissionV2>? = null,
val usesPermissionSdk23: List<PermissionV2>? = null,
public data class AppVersion internal constructor(
@Embedded internal val version: Version,
@Relation(
parentColumn = "versionId",
entityColumn = "versionId",
)
internal val versionedStrings: List<VersionedString>?,
) {
public val repoId: Long get() = version.repoId
public val packageId: String get() = version.packageId
@@ -84,9 +87,12 @@ public data class AppVersion(
public val manifest: AppManifest get() = version.manifest
public val file: FileV1 get() = version.file
public val src: FileV2? get() = version.src
public val usesPermission: List<PermissionV2>? get() = versionedStrings?.getPermissions(version)
public val usesPermissionSdk23: List<PermissionV2>?
get() = versionedStrings?.getPermissionsSdk23(version)
public val featureNames: List<String> get() = version.manifest.features ?: emptyList()
public val nativeCode: List<String> get() = version.manifest.nativecode ?: emptyList()
public val releaseChannels: List<String> = version.releaseChannels ?: emptyList()
public val releaseChannels: List<String> get() = version.releaseChannels ?: emptyList()
val antiFeatureNames: List<String>
get() {
return version.antiFeatures?.map { it.key } ?: emptyList()

View File

@@ -1,9 +1,6 @@
package org.fdroid.database
import androidx.lifecycle.LiveData
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.liveData
import androidx.lifecycle.map
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy.REPLACE
@@ -15,7 +12,6 @@ import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.decodeFromJsonElement
import org.fdroid.database.FDroidDatabaseHolder.dispatcher
import org.fdroid.database.VersionedStringType.PERMISSION
import org.fdroid.database.VersionedStringType.PERMISSION_SDK_23
import org.fdroid.index.IndexParser.json
@@ -25,15 +21,20 @@ import org.fdroid.index.v2.PermissionV2
import org.fdroid.index.v2.ReflectionDiffer
public interface VersionDao {
/**
* Inserts new versions for a given [packageName] from a full index.
*/
public fun insert(
repoId: Long,
packageId: String,
packageName: String,
packageVersions: Map<String, PackageVersionV2>,
checkIfCompatible: (PackageVersionV2) -> Boolean,
)
public fun getAppVersions(packageId: String): LiveData<List<AppVersion>>
public fun getAppVersions(repoId: Long, packageId: String): List<AppVersion>
/**
* Returns a list of versions for the given [packageName] sorting by highest version code first.
*/
public fun getAppVersions(packageName: String): LiveData<List<AppVersion>>
}
/**
@@ -51,26 +52,26 @@ internal interface VersionDaoInt : VersionDao {
@Transaction
override fun insert(
repoId: Long,
packageId: String,
packageName: String,
packageVersions: Map<String, PackageVersionV2>,
checkIfCompatible: (PackageVersionV2) -> Boolean,
) {
// TODO maybe the number of queries here can be reduced
packageVersions.entries.iterator().forEach { (versionId, packageVersion) ->
val isCompatible = checkIfCompatible(packageVersion)
insert(repoId, packageId, versionId, packageVersion, isCompatible)
insert(repoId, packageName, versionId, packageVersion, isCompatible)
}
}
@Transaction
fun insert(
repoId: Long,
packageId: String,
packageName: String,
versionId: String,
packageVersion: PackageVersionV2,
isCompatible: Boolean,
) {
val version = packageVersion.toVersion(repoId, packageId, versionId, isCompatible)
val version = packageVersion.toVersion(repoId, packageName, versionId, isCompatible)
insert(version)
insert(packageVersion.manifest.getVersionedStrings(version))
}
@@ -86,22 +87,22 @@ internal interface VersionDaoInt : VersionDao {
fun update(
repoId: Long,
packageId: String,
packageName: String,
versionsDiffMap: Map<String, JsonObject?>?,
checkIfCompatible: (PackageManifest) -> Boolean,
) {
if (versionsDiffMap == null) { // no more versions, delete all
deleteAppVersion(repoId, packageId)
deleteAppVersion(repoId, packageName)
} else versionsDiffMap.forEach { (versionId, jsonObject) ->
if (jsonObject == null) { // delete individual version
deleteAppVersion(repoId, packageId, versionId)
deleteAppVersion(repoId, packageName, versionId)
} else {
val version = getVersion(repoId, packageId, versionId)
val version = getVersion(repoId, packageName, versionId)
if (version == null) { // new version, parse normally
val packageVersionV2: PackageVersionV2 =
json.decodeFromJsonElement(jsonObject)
val isCompatible = checkIfCompatible(packageVersionV2.packageManifest)
insert(repoId, packageId, versionId, packageVersionV2, isCompatible)
insert(repoId, packageName, versionId, packageVersionV2, isCompatible)
} else { // diff against existing version
diffVersion(version, jsonObject, checkIfCompatible)
}
@@ -153,38 +154,25 @@ internal interface VersionDaoInt : VersionDao {
insertNewList = { versionedStrings -> insert(versionedStrings) },
)
override fun getAppVersions(
packageId: String,
): LiveData<List<AppVersion>> = liveData(dispatcher) {
// TODO we should probably react to changes of versioned strings as well
val versionedStrings = getVersionedStrings(packageId)
val liveData = getVersions(packageId).distinctUntilChanged().map { versions ->
versions.map { version -> version.toAppVersion(versionedStrings) }
}
emitSource(liveData)
}
@Transaction
override fun getAppVersions(repoId: Long, packageId: String): List<AppVersion> {
val versionedStrings = getVersionedStrings(repoId, packageId)
return getVersions(repoId, packageId).map { version ->
version.toAppVersion(versionedStrings)
}
}
@Query("""SELECT * FROM Version
WHERE repoId = :repoId AND packageId = :packageId AND versionId = :versionId""")
fun getVersion(repoId: Long, packageId: String, versionId: String): Version?
@RewriteQueriesToDropUnusedColumns
@Query("""SELECT * FROM Version
JOIN RepositoryPreferences AS pref USING (repoId)
WHERE pref.enabled = 1 AND packageId = :packageId
ORDER BY manifest_versionCode DESC""")
fun getVersions(packageId: String): LiveData<List<Version>>
WHERE pref.enabled = 1 AND packageId = :packageName
ORDER BY manifest_versionCode DESC, pref.weight DESC""")
override fun getAppVersions(packageName: String): LiveData<List<AppVersion>>
@Query("SELECT * FROM Version WHERE repoId = :repoId AND packageId = :packageId")
fun getVersions(repoId: Long, packageId: String): List<Version>
/**
* Only use for testing, not sorted, does take disabled repos into account.
*/
@Transaction
@Query("""SELECT * FROM Version
WHERE repoId = :repoId AND packageId = :packageName""")
fun getAppVersions(repoId: Long, packageName: String): List<AppVersion>
@Query("""SELECT * FROM Version
WHERE repoId = :repoId AND packageId = :packageName AND versionId = :versionId""")
fun getVersion(repoId: Long, packageName: String, versionId: String): Version?
/**
* Used for finding versions that are an update,
@@ -192,47 +180,41 @@ internal interface VersionDaoInt : VersionDao {
*/
@RewriteQueriesToDropUnusedColumns
@Query("""SELECT * FROM Version
JOIN RepositoryPreferences USING (repoId)
JOIN RepositoryPreferences AS pref USING (repoId)
LEFT JOIN AppPrefs USING (packageId)
WHERE RepositoryPreferences.enabled = 1 AND
WHERE pref.enabled = 1 AND
manifest_versionCode > COALESCE(AppPrefs.ignoreVersionCodeUpdate, 0) AND
packageId IN (:packageNames)
ORDER BY manifest_versionCode DESC, RepositoryPreferences.weight DESC""")
ORDER BY manifest_versionCode DESC, pref.weight DESC""")
fun getVersions(packageNames: List<String>): List<Version>
@RewriteQueriesToDropUnusedColumns
@Query("""SELECT * FROM VersionedString
JOIN RepositoryPreferences AS pref USING (repoId)
WHERE pref.enabled = 1 AND packageId = :packageId""")
fun getVersionedStrings(packageId: String): List<VersionedString>
@Query("SELECT * FROM VersionedString WHERE repoId = :repoId AND packageId = :packageId")
fun getVersionedStrings(repoId: Long, packageId: String): List<VersionedString>
@Query("SELECT * FROM VersionedString WHERE repoId = :repoId AND packageId = :packageName")
fun getVersionedStrings(repoId: Long, packageName: String): List<VersionedString>
@Query("""SELECT * FROM VersionedString
WHERE repoId = :repoId AND packageId = :packageId AND versionId = :versionId""")
WHERE repoId = :repoId AND packageId = :packageName AND versionId = :versionId""")
fun getVersionedStrings(
repoId: Long,
packageId: String,
packageName: String,
versionId: String,
): List<VersionedString>
@Query("""DELETE FROM Version WHERE repoId = :repoId AND packageId = :packageId""")
fun deleteAppVersion(repoId: Long, packageId: String)
@Query("""DELETE FROM Version WHERE repoId = :repoId AND packageId = :packageName""")
fun deleteAppVersion(repoId: Long, packageName: String)
@Query("""DELETE FROM Version
WHERE repoId = :repoId AND packageId = :packageId AND versionId = :versionId""")
fun deleteAppVersion(repoId: Long, packageId: String, versionId: String)
WHERE repoId = :repoId AND packageId = :packageName AND versionId = :versionId""")
fun deleteAppVersion(repoId: Long, packageName: String, versionId: String)
@Query("""DELETE FROM VersionedString
WHERE repoId = :repoId AND packageId = :packageId AND versionId = :versionId""")
fun deleteVersionedStrings(repoId: Long, packageId: String, versionId: String)
WHERE repoId = :repoId AND packageId = :packageName AND versionId = :versionId""")
fun deleteVersionedStrings(repoId: Long, packageName: String, versionId: String)
@Query("""DELETE FROM VersionedString WHERE repoId = :repoId
AND packageId = :packageId AND versionId = :versionId AND type = :type""")
AND packageId = :packageName AND versionId = :versionId AND type = :type""")
fun deleteVersionedStrings(
repoId: Long,
packageId: String,
packageName: String,
versionId: String,
type: VersionedStringType,
)