mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 23:54:39 +00:00
Migrates EventStore from Android's SQLLite to KMP
Fixes testing of libsodium between java and android
This commit is contained in:
@@ -609,7 +609,7 @@ SQLite-based storage with full NIP support (NIP-09, NIP-40, NIP-45, NIP-50, NIP-
|
||||
import com.vitorpamplona.quartz.nip01Core.store.EventStore
|
||||
import android.content.Context
|
||||
|
||||
val store = EventStore(context)
|
||||
val store = EventStore()
|
||||
|
||||
// Insert
|
||||
store.insert(event)
|
||||
|
||||
+3
-2
@@ -51,7 +51,7 @@ class LargeDBInsertBenchmark : BaseLargeCacheBenchmark() {
|
||||
benchmarkRule.measureRepeated {
|
||||
val db =
|
||||
runWithMeasurementDisabled {
|
||||
EventStore(context, null)
|
||||
EventStore(null)
|
||||
}
|
||||
firstThousandEvents.forEach { event ->
|
||||
try {
|
||||
@@ -81,7 +81,8 @@ class LargeDBInsertBenchmark : BaseLargeCacheBenchmark() {
|
||||
benchmarkRule.measureRepeated {
|
||||
val db =
|
||||
runWithMeasurementDisabled {
|
||||
val db = EventStore(context, null)
|
||||
val db =
|
||||
EventStore(null)
|
||||
toBeDeletedEvents.forEach { event ->
|
||||
try {
|
||||
db.insert(event)
|
||||
|
||||
+2
-2
@@ -49,14 +49,14 @@ class LargeDBQueryingBenchmark : BaseLargeCacheBenchmark() {
|
||||
val allEvents = getEventDB().distinctBy { it.id }.sortedBy { it.createdAt }
|
||||
}
|
||||
|
||||
lateinit var db: EventStore
|
||||
lateinit var db: com.vitorpamplona.quartz.nip01Core.store.sqlite.EventStore
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
|
||||
context.deleteDatabase("allEvents.db")
|
||||
db = EventStore(context, "allEvents.db")
|
||||
db = EventStore("allEvents.db")
|
||||
allEvents.forEach { event ->
|
||||
try {
|
||||
db.insert(event)
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8
|
||||
org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
|
||||
@@ -73,6 +73,7 @@ core = "1.7.0"
|
||||
mavenPublish = "0.36.0"
|
||||
spmForKmpVersion = "1.4.10"
|
||||
stabilityAnalyser = "0.7.0"
|
||||
sqlite = "2.6.2"
|
||||
|
||||
[libraries]
|
||||
abedElazizShe-video-compressor-fork = { group = "com.github.davotoula", name = "LightCompressor-enhanced", version.ref = "lightcompressor-enhanced" }
|
||||
@@ -181,6 +182,9 @@ androidx-window-core-android = { group = "androidx.window", name = "window-core-
|
||||
kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" }
|
||||
kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlinTest" }
|
||||
androidx-core = { group = "androidx.test", name = "core", version.ref = "core" }
|
||||
androidx-sqlite = { group = "androidx.sqlite", name = "sqlite", version.ref = "sqlite" }
|
||||
androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" }
|
||||
androidx-sqlite-bundled-jvm = { module = "androidx.sqlite:sqlite-bundled-jvm", version.ref = "sqlite" }
|
||||
|
||||
[plugins]
|
||||
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
+16
-1
@@ -159,9 +159,10 @@ kotlin {
|
||||
}
|
||||
|
||||
// This makes sure that the resource file directory is visible for iOS tests.
|
||||
val rootDir = "${rootProject.rootDir.path}/quartz/src/iosTest/resources"
|
||||
val rootDir = "${rootProject.rootDir.path}/quartz/src/commonTest/resources"
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
maxHeapSize = "4g"
|
||||
environment("TEST_RESOURCES_ROOT", rootDir)
|
||||
}
|
||||
|
||||
@@ -196,6 +197,10 @@ kotlin {
|
||||
|
||||
// immutable collections to avoid recomposition
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
|
||||
// SQLite KMP driver for event store
|
||||
api(libs.androidx.sqlite)
|
||||
implementation(libs.androidx.sqlite.bundled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +208,10 @@ kotlin {
|
||||
dependencies {
|
||||
implementation(libs.kotlin.test)
|
||||
implementation(libs.kotlinx.coroutines.test)
|
||||
|
||||
// SQLite bundled driver for tests
|
||||
api(libs.androidx.sqlite)
|
||||
implementation(libs.androidx.sqlite.bundled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,6 +265,9 @@ kotlin {
|
||||
dependencies {
|
||||
// Bitcoin secp256k1 bindings
|
||||
implementation(libs.secp256k1.kmp.jni.jvm)
|
||||
|
||||
// SQLite bundled driver for JVM tests
|
||||
implementation(libs.androidx.sqlite.bundled.jvm)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,6 +296,9 @@ kotlin {
|
||||
// LibSodium for ChaCha encryption (NIP-44) - Needed for host tests
|
||||
implementation(libs.lazysodium.java)
|
||||
implementation(libs.jna)
|
||||
|
||||
// SQLite bundled driver for Host tests
|
||||
implementation(libs.androidx.sqlite.bundled.jvm)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
@@ -20,7 +20,17 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz
|
||||
|
||||
import java.util.zip.GZIPInputStream
|
||||
|
||||
actual class TestResourceLoader {
|
||||
actual fun loadDecompressString(file: String): String =
|
||||
this@TestResourceLoader
|
||||
.javaClass.classLoader
|
||||
?.getResourceAsStream(file)
|
||||
?.let { GZIPInputStream(it) }
|
||||
?.bufferedReader()
|
||||
?.use { it.readText() } ?: throw IllegalArgumentException("Resource not found: $file")
|
||||
|
||||
actual fun loadString(file: String): String =
|
||||
this@TestResourceLoader
|
||||
.javaClass.classLoader
|
||||
|
||||
+28
-2
@@ -20,12 +20,38 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.utils
|
||||
|
||||
import com.goterl.lazysodium.LazySodium
|
||||
import com.goterl.lazysodium.LazySodiumAndroid
|
||||
import com.goterl.lazysodium.Sodium
|
||||
import com.goterl.lazysodium.SodiumAndroid
|
||||
|
||||
actual object LibSodiumInstance {
|
||||
private val libSodium = SodiumAndroid()
|
||||
private val lazySodium = LazySodiumAndroid(libSodium)
|
||||
private val libSodium: Sodium =
|
||||
try {
|
||||
// If we are running in a host test, SodiumJava might be available.
|
||||
// SodiumJava uses a ResourceLoader to find the dylib/so/dll in the jar.
|
||||
Class
|
||||
.forName("com.goterl.lazysodium.SodiumJava")
|
||||
.getConstructor()
|
||||
.newInstance() as Sodium
|
||||
} catch (_: Exception) {
|
||||
SodiumAndroid()
|
||||
}
|
||||
|
||||
private val lazySodium: LazySodium =
|
||||
if (libSodium is SodiumAndroid) {
|
||||
LazySodiumAndroid(libSodium)
|
||||
} else {
|
||||
// this should only happen on test cases
|
||||
val sodiumJava =
|
||||
Class
|
||||
.forName("com.goterl.lazysodium.SodiumJava")
|
||||
|
||||
Class
|
||||
.forName("com.goterl.lazysodium.LazySodiumJava")
|
||||
.getConstructor(sodiumJava)
|
||||
.newInstance(libSodium) as LazySodium
|
||||
}
|
||||
|
||||
actual fun cryptoAeadXChaCha20Poly1305IetfDecrypt(
|
||||
message: ByteArray,
|
||||
|
||||
+2
-2
@@ -24,10 +24,10 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
|
||||
interface IEventStore {
|
||||
fun insert(event: Event): Boolean
|
||||
fun insert(event: Event)
|
||||
|
||||
interface ITransaction {
|
||||
fun insert(event: Event): Boolean
|
||||
fun insert(event: Event)
|
||||
}
|
||||
|
||||
fun transaction(body: ITransaction.() -> Unit)
|
||||
+4
-4
@@ -20,10 +20,10 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import androidx.sqlite.SQLiteConnection
|
||||
|
||||
class AddressableModule : IModule {
|
||||
override fun create(db: SQLiteDatabase) {
|
||||
override fun create(db: SQLiteConnection) {
|
||||
db.execSQL(
|
||||
"""
|
||||
CREATE UNIQUE INDEX addressable_idx
|
||||
@@ -57,7 +57,7 @@ class AddressableModule : IModule {
|
||||
)
|
||||
}
|
||||
|
||||
override fun drop(db: SQLiteDatabase) {}
|
||||
override fun drop(db: SQLiteConnection) {}
|
||||
|
||||
override fun deleteAll(db: SQLiteDatabase) {}
|
||||
override fun deleteAll(db: SQLiteConnection) {}
|
||||
}
|
||||
+7
-7
@@ -20,7 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import androidx.sqlite.SQLiteConnection
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
@@ -29,7 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.core.isReplaceable
|
||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||
|
||||
class DeletionRequestModule(
|
||||
val hasher: (db: SQLiteDatabase) -> TagNameValueHasher,
|
||||
val hasher: (db: SQLiteConnection) -> TagNameValueHasher,
|
||||
val indexStrategy: IndexingStrategy = DefaultIndexingStrategy(),
|
||||
) : IModule {
|
||||
fun rejectDeletedEventsSQLTemplate(): String =
|
||||
@@ -58,7 +58,7 @@ class DeletionRequestModule(
|
||||
* deleted by ID or ATag including GiftWraps that
|
||||
* must be checked against the p-tag (pubkey_owner_hash)
|
||||
*/
|
||||
override fun create(db: SQLiteDatabase) {
|
||||
override fun create(db: SQLiteConnection) {
|
||||
val sql = rejectDeletedEventsSQLTemplate().replace("\n", "\n ")
|
||||
db.execSQL(
|
||||
"""
|
||||
@@ -75,13 +75,13 @@ class DeletionRequestModule(
|
||||
)
|
||||
}
|
||||
|
||||
override fun drop(db: SQLiteDatabase) {}
|
||||
override fun drop(db: SQLiteConnection) {}
|
||||
|
||||
override fun deleteAll(db: SQLiteDatabase) {}
|
||||
override fun deleteAll(db: SQLiteConnection) {}
|
||||
|
||||
fun insert(
|
||||
event: Event,
|
||||
db: SQLiteDatabase,
|
||||
db: SQLiteConnection,
|
||||
) {
|
||||
if (event is DeletionEvent) {
|
||||
val idValues = event.deleteEventIds()
|
||||
@@ -103,7 +103,7 @@ class DeletionRequestModule(
|
||||
pubkey: HexKey,
|
||||
idValues: List<String>,
|
||||
addresses: List<Address>,
|
||||
hasher: TagNameValueHasher,
|
||||
hasher: com.vitorpamplona.quartz.nip01Core.store.sqlite.TagNameValueHasher,
|
||||
): List<SqlArgs> {
|
||||
val owner = hasher.hash(pubkey)
|
||||
val idParams = idValues.joinToString(",") { "?" }
|
||||
+4
-4
@@ -20,10 +20,10 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import androidx.sqlite.SQLiteConnection
|
||||
|
||||
class EphemeralModule : IModule {
|
||||
override fun create(db: SQLiteDatabase) {
|
||||
override fun create(db: SQLiteConnection) {
|
||||
// Rejects all ephemeral events.
|
||||
db.execSQL(
|
||||
"""
|
||||
@@ -38,7 +38,7 @@ class EphemeralModule : IModule {
|
||||
)
|
||||
}
|
||||
|
||||
override fun drop(db: SQLiteDatabase) {}
|
||||
override fun drop(db: SQLiteConnection) {}
|
||||
|
||||
override fun deleteAll(db: SQLiteDatabase) {}
|
||||
override fun deleteAll(db: SQLiteConnection) {}
|
||||
}
|
||||
+48
-45
@@ -20,7 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import androidx.sqlite.SQLiteConnection
|
||||
import com.vitorpamplona.quartz.nip01Core.core.AddressSerializer
|
||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
@@ -28,10 +28,10 @@ import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||
|
||||
class EventIndexesModule(
|
||||
val hasher: (db: SQLiteDatabase) -> TagNameValueHasher,
|
||||
val hasher: (db: SQLiteConnection) -> TagNameValueHasher,
|
||||
val indexStrategy: IndexingStrategy = DefaultIndexingStrategy(),
|
||||
) : IModule {
|
||||
override fun create(db: SQLiteDatabase) {
|
||||
override fun create(db: SQLiteConnection) {
|
||||
db.execSQL(
|
||||
"""
|
||||
CREATE TABLE event_headers (
|
||||
@@ -128,7 +128,7 @@ class EventIndexesModule(
|
||||
)
|
||||
}
|
||||
|
||||
override fun drop(db: SQLiteDatabase) {
|
||||
override fun drop(db: SQLiteConnection) {
|
||||
db.execSQL("DROP TABLE IF EXISTS event_tags")
|
||||
db.execSQL("DROP TABLE IF EXISTS event_headers")
|
||||
}
|
||||
@@ -151,10 +151,9 @@ class EventIndexesModule(
|
||||
|
||||
fun insert(
|
||||
event: Event,
|
||||
db: SQLiteDatabase,
|
||||
db: SQLiteConnection,
|
||||
): Long {
|
||||
val hasher = hasher(db)
|
||||
val stmt = db.compileStatement(sqlInsertHeader)
|
||||
|
||||
val kindLong = event.kind.toLong()
|
||||
val pubkeyHash = hasher.hash(event.pubKey)
|
||||
@@ -168,52 +167,56 @@ class EventIndexesModule(
|
||||
|
||||
val eTagHash = hasher.hashETag(event.id)
|
||||
|
||||
stmt.bindString(1, event.id)
|
||||
stmt.bindString(2, event.pubKey)
|
||||
stmt.bindLong(3, event.createdAt)
|
||||
stmt.bindLong(4, kindLong)
|
||||
stmt.bindString(5, OptimizedJsonMapper.toJson(event.tags))
|
||||
stmt.bindString(6, event.content)
|
||||
stmt.bindString(7, event.sig)
|
||||
if (event is AddressableEvent) {
|
||||
val dTag = event.dTag()
|
||||
stmt.bindString(8, dTag)
|
||||
stmt.bindLong(9, eventOwnerHash)
|
||||
stmt.bindLong(10, eTagHash)
|
||||
stmt.bindLong(11, hasher.hashATag(AddressSerializer.assemble(event.kind, event.pubKey, dTag)))
|
||||
} else {
|
||||
stmt.bindNull(8)
|
||||
stmt.bindLong(9, eventOwnerHash)
|
||||
stmt.bindLong(10, eTagHash)
|
||||
stmt.bindNull(11)
|
||||
}
|
||||
|
||||
val headerId = stmt.executeInsert()
|
||||
|
||||
val stmtTags = db.compileStatement(sqlInsertTags)
|
||||
|
||||
// sorting helps SQLLite by avoiding
|
||||
// rebalancing the tree every new insert
|
||||
val indexableTags = ArrayList<Long>()
|
||||
for (idx in event.tags.indices) {
|
||||
if (indexStrategy.shouldIndex(event.kind, event.tags[idx])) {
|
||||
indexableTags.add(hasher.hash(event.tags[idx][0], event.tags[idx][1]))
|
||||
db.prepare(sqlInsertHeader).use { stmt ->
|
||||
stmt.bindText(1, event.id)
|
||||
stmt.bindText(2, event.pubKey)
|
||||
stmt.bindLong(3, event.createdAt)
|
||||
stmt.bindLong(4, kindLong)
|
||||
stmt.bindText(5, OptimizedJsonMapper.toJson(event.tags))
|
||||
stmt.bindText(6, event.content)
|
||||
stmt.bindText(7, event.sig)
|
||||
if (event is AddressableEvent) {
|
||||
val dTag = event.dTag()
|
||||
stmt.bindText(8, dTag)
|
||||
stmt.bindLong(9, eventOwnerHash)
|
||||
stmt.bindLong(10, eTagHash)
|
||||
stmt.bindLong(11, hasher.hashATag(AddressSerializer.assemble(event.kind, event.pubKey, dTag)))
|
||||
} else {
|
||||
stmt.bindNull(8)
|
||||
stmt.bindLong(9, eventOwnerHash)
|
||||
stmt.bindLong(10, eTagHash)
|
||||
stmt.bindNull(11)
|
||||
}
|
||||
stmt.step()
|
||||
}
|
||||
indexableTags.sort()
|
||||
indexableTags.forEach {
|
||||
stmtTags.bindLong(1, headerId)
|
||||
stmtTags.bindLong(2, it)
|
||||
stmtTags.bindLong(3, event.createdAt)
|
||||
stmtTags.bindLong(4, kindLong)
|
||||
stmtTags.bindLong(5, pubkeyHash)
|
||||
stmtTags.executeInsert()
|
||||
|
||||
val headerId = db.lastInsertRowId()
|
||||
|
||||
db.prepare(sqlInsertTags).use { stmtTags ->
|
||||
// sorting helps SQLLite by avoiding
|
||||
// rebalancing the tree every new insert
|
||||
val indexableTags = ArrayList<Long>()
|
||||
for (idx in event.tags.indices) {
|
||||
if (indexStrategy.shouldIndex(event.kind, event.tags[idx])) {
|
||||
indexableTags.add(hasher.hash(event.tags[idx][0], event.tags[idx][1]))
|
||||
}
|
||||
}
|
||||
indexableTags.sort()
|
||||
indexableTags.forEach {
|
||||
stmtTags.bindLong(1, headerId)
|
||||
stmtTags.bindLong(2, it)
|
||||
stmtTags.bindLong(3, event.createdAt)
|
||||
stmtTags.bindLong(4, kindLong)
|
||||
stmtTags.bindLong(5, pubkeyHash)
|
||||
stmtTags.step()
|
||||
stmtTags.reset()
|
||||
}
|
||||
}
|
||||
|
||||
return headerId
|
||||
}
|
||||
|
||||
override fun deleteAll(db: SQLiteDatabase) {
|
||||
override fun deleteAll(db: SQLiteConnection) {
|
||||
db.execSQL("DELETE FROM event_tags")
|
||||
db.execSQL("DELETE FROM event_headers")
|
||||
}
|
||||
+4
-5
@@ -20,18 +20,17 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.content.Context
|
||||
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.store.IEventStore
|
||||
|
||||
class EventStore(
|
||||
context: Context,
|
||||
dbName: String? = "events.db",
|
||||
val relayUrl: String? = "wss://quartz.local",
|
||||
relayUrl: String? = "wss://quartz.local",
|
||||
val indexStrategy: IndexingStrategy = DefaultIndexingStrategy(),
|
||||
) : IEventStore {
|
||||
val store = SQLiteEventStore(context, dbName, relayUrl, indexStrategy)
|
||||
val store = SQLiteEventStore(BundledSQLiteDriver(), dbName, relayUrl, indexStrategy)
|
||||
|
||||
override fun insert(event: Event) = store.insertEvent(event)
|
||||
|
||||
@@ -61,5 +60,5 @@ class EventStore(
|
||||
|
||||
override fun deleteExpiredEvents() = store.deleteExpiredEvents()
|
||||
|
||||
override fun close() = store.close()
|
||||
override fun close() = store.connection.close()
|
||||
}
|
||||
+13
-12
@@ -20,12 +20,12 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import androidx.sqlite.SQLiteConnection
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
||||
|
||||
class ExpirationModule : IModule {
|
||||
override fun create(db: SQLiteDatabase) {
|
||||
override fun create(db: SQLiteConnection) {
|
||||
db.execSQL(
|
||||
"""
|
||||
CREATE TABLE event_expirations (
|
||||
@@ -51,7 +51,7 @@ class ExpirationModule : IModule {
|
||||
)
|
||||
}
|
||||
|
||||
override fun drop(db: SQLiteDatabase) {
|
||||
override fun drop(db: SQLiteConnection) {
|
||||
db.execSQL("DROP TABLE IF EXISTS event_expirations")
|
||||
}
|
||||
|
||||
@@ -64,18 +64,19 @@ class ExpirationModule : IModule {
|
||||
fun insert(
|
||||
event: Event,
|
||||
headerId: Long,
|
||||
db: SQLiteDatabase,
|
||||
db: SQLiteConnection,
|
||||
) {
|
||||
val exp = event.expiration()
|
||||
if (exp != null && exp > 0) {
|
||||
val stmt = db.compileStatement(insertExpiration)
|
||||
stmt.bindLong(1, headerId)
|
||||
stmt.bindLong(2, exp)
|
||||
stmt.executeInsert()
|
||||
db.prepare(insertExpiration).use { stmt ->
|
||||
stmt.bindLong(1, headerId)
|
||||
stmt.bindLong(2, exp)
|
||||
stmt.step()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val deleteExpiredEvents =
|
||||
val deleteExpiredEventsSQL =
|
||||
"""
|
||||
DELETE FROM event_headers
|
||||
WHERE row_id IN (
|
||||
@@ -84,11 +85,11 @@ class ExpirationModule : IModule {
|
||||
);
|
||||
""".trimIndent()
|
||||
|
||||
fun deleteExpiredEvents(db: SQLiteDatabase) {
|
||||
db.compileStatement(deleteExpiredEvents).execute()
|
||||
fun deleteExpiredEvents(db: SQLiteConnection) {
|
||||
db.prepare(deleteExpiredEventsSQL).use { it.step() }
|
||||
}
|
||||
|
||||
override fun deleteAll(db: SQLiteDatabase) {
|
||||
override fun deleteAll(db: SQLiteConnection) {
|
||||
db.execSQL("DELETE FROM event_expirations")
|
||||
}
|
||||
}
|
||||
+12
-11
@@ -20,8 +20,8 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteException
|
||||
import androidx.sqlite.SQLiteConnection
|
||||
import androidx.sqlite.SQLiteException
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
|
||||
|
||||
@@ -30,7 +30,7 @@ class FullTextSearchModule : IModule {
|
||||
val eventHeaderRowIdName = "event_header_row_id"
|
||||
val contentName = "content"
|
||||
|
||||
override fun create(db: SQLiteDatabase) {
|
||||
override fun create(db: SQLiteConnection) {
|
||||
val ftsVersion = FullTextSearchModule().versionFinder(db)
|
||||
db.execSQL(
|
||||
"""
|
||||
@@ -53,7 +53,7 @@ class FullTextSearchModule : IModule {
|
||||
)
|
||||
}
|
||||
|
||||
override fun drop(db: SQLiteDatabase) {
|
||||
override fun drop(db: SQLiteConnection) {
|
||||
db.execSQL("DROP TABLE IF EXISTS $tableName")
|
||||
}
|
||||
|
||||
@@ -66,17 +66,18 @@ class FullTextSearchModule : IModule {
|
||||
fun insert(
|
||||
event: Event,
|
||||
headerId: Long,
|
||||
db: SQLiteDatabase,
|
||||
db: SQLiteConnection,
|
||||
) {
|
||||
if (event is SearchableEvent) {
|
||||
val stmt = db.compileStatement(insertFTS)
|
||||
stmt.bindLong(1, headerId)
|
||||
stmt.bindString(2, event.indexableContent())
|
||||
stmt.executeInsert()
|
||||
db.prepare(insertFTS).use { stmt ->
|
||||
stmt.bindLong(1, headerId)
|
||||
stmt.bindText(2, event.indexableContent())
|
||||
stmt.step()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun versionFinder(db: SQLiteDatabase): Int =
|
||||
fun versionFinder(db: SQLiteConnection): Int =
|
||||
try {
|
||||
try {
|
||||
db.execSQL("CREATE VIRTUAL TABLE dummy_fts5 USING fts5(dummy)")
|
||||
@@ -90,7 +91,7 @@ class FullTextSearchModule : IModule {
|
||||
3
|
||||
}
|
||||
|
||||
override fun deleteAll(db: SQLiteDatabase) {
|
||||
override fun deleteAll(db: SQLiteConnection) {
|
||||
db.execSQL("DELETE FROM event_fts")
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -20,12 +20,12 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import androidx.sqlite.SQLiteConnection
|
||||
|
||||
interface IModule {
|
||||
fun create(db: SQLiteDatabase)
|
||||
fun create(db: SQLiteConnection)
|
||||
|
||||
fun drop(db: SQLiteDatabase)
|
||||
fun drop(db: SQLiteConnection)
|
||||
|
||||
fun deleteAll(db: SQLiteDatabase)
|
||||
fun deleteAll(db: SQLiteConnection)
|
||||
}
|
||||
+144
-79
@@ -20,20 +20,21 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import androidx.sqlite.SQLiteConnection
|
||||
import androidx.sqlite.SQLiteStatement
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Kind
|
||||
import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.core.isAddressable
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.store.sqlite.explainQuery
|
||||
import com.vitorpamplona.quartz.nip01Core.store.sqlite.sql.where
|
||||
import com.vitorpamplona.quartz.utils.EventFactory
|
||||
|
||||
class QueryBuilder(
|
||||
val fts: FullTextSearchModule,
|
||||
val hasher: (db: SQLiteDatabase) -> TagNameValueHasher,
|
||||
val hasher: (db: SQLiteConnection) -> TagNameValueHasher,
|
||||
val indexStrategy: IndexingStrategy,
|
||||
) {
|
||||
// ------------
|
||||
@@ -41,23 +42,23 @@ class QueryBuilder(
|
||||
// ------------
|
||||
fun <T : Event> query(
|
||||
filter: Filter,
|
||||
db: SQLiteDatabase,
|
||||
db: SQLiteConnection,
|
||||
): List<T> = db.runQuery(toSql(filter, hasher(db)))
|
||||
|
||||
fun <T : Event> query(
|
||||
filter: Filter,
|
||||
db: SQLiteDatabase,
|
||||
db: SQLiteConnection,
|
||||
onEach: (T) -> Unit,
|
||||
) = db.runQuery(toSql(filter, hasher(db)), onEach)
|
||||
|
||||
fun <T : Event> query(
|
||||
filters: List<Filter>,
|
||||
db: SQLiteDatabase,
|
||||
db: SQLiteConnection,
|
||||
): List<T> = db.runQuery(toSql(filters, hasher(db)))
|
||||
|
||||
fun <T : Event> query(
|
||||
filters: List<Filter>,
|
||||
db: SQLiteDatabase,
|
||||
db: SQLiteConnection,
|
||||
onEach: (T) -> Unit,
|
||||
) = db.runQuery(toSql(filters, hasher(db)), onEach)
|
||||
|
||||
@@ -66,23 +67,23 @@ class QueryBuilder(
|
||||
// ---------------------------
|
||||
fun rawQuery(
|
||||
filter: Filter,
|
||||
db: SQLiteDatabase,
|
||||
db: SQLiteConnection,
|
||||
): List<RawEvent> = db.runRawQuery(toSql(filter, hasher(db)))
|
||||
|
||||
fun rawQuery(
|
||||
filter: Filter,
|
||||
db: SQLiteDatabase,
|
||||
db: SQLiteConnection,
|
||||
onEach: (RawEvent) -> Unit,
|
||||
) = db.runRawQuery(toSql(filter, hasher(db)), onEach)
|
||||
|
||||
fun rawQuery(
|
||||
filters: List<Filter>,
|
||||
db: SQLiteDatabase,
|
||||
db: SQLiteConnection,
|
||||
): List<RawEvent> = db.runRawQuery(toSql(filters, hasher(db)))
|
||||
|
||||
fun rawQuery(
|
||||
filters: List<Filter>,
|
||||
db: SQLiteDatabase,
|
||||
db: SQLiteConnection,
|
||||
onEach: (RawEvent) -> Unit,
|
||||
) = db.runRawQuery(toSql(filters, hasher(db)), onEach)
|
||||
|
||||
@@ -92,7 +93,7 @@ class QueryBuilder(
|
||||
fun planQuery(
|
||||
filter: Filter,
|
||||
hasher: TagNameValueHasher,
|
||||
db: SQLiteDatabase,
|
||||
db: SQLiteConnection,
|
||||
): String {
|
||||
val query = toSql(filter, hasher)
|
||||
return db.explainQuery(query.sql, query.args.toTypedArray())
|
||||
@@ -101,7 +102,7 @@ class QueryBuilder(
|
||||
fun planQuery(
|
||||
filters: List<Filter>,
|
||||
hasher: TagNameValueHasher,
|
||||
db: SQLiteDatabase,
|
||||
db: SQLiteConnection,
|
||||
): String {
|
||||
val query = toSql(filters, hasher)
|
||||
return db.explainQuery(query.sql, query.args.toTypedArray())
|
||||
@@ -184,62 +185,74 @@ class QueryBuilder(
|
||||
ORDER BY created_at DESC${if (indexStrategy.useAndIndexIdOnOrderBy) ", id ASC" else ""}
|
||||
""".trimIndent()
|
||||
|
||||
private fun <T : Event> SQLiteDatabase.runQuery(query: QuerySpec): List<T> =
|
||||
rawQuery(query.sql, query.args.toTypedArray()).use { cursor ->
|
||||
ArrayList<T>(cursor.count).apply {
|
||||
while (cursor.moveToNext()) {
|
||||
add(cursor.toEvent())
|
||||
}
|
||||
private fun <T : Event> SQLiteConnection.runQuery(query: QuerySpec): List<T> =
|
||||
prepare(query.sql).use { stmt ->
|
||||
query.args.forEachIndexed { index, arg ->
|
||||
stmt.bindText(index + 1, arg)
|
||||
}
|
||||
val results = ArrayList<T>()
|
||||
while (stmt.step()) {
|
||||
results.add(stmt.toEvent())
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.runRawQuery(query: QuerySpec): List<RawEvent> =
|
||||
rawQuery(query.sql, query.args.toTypedArray()).use { cursor ->
|
||||
ArrayList<RawEvent>(cursor.count).apply {
|
||||
while (cursor.moveToNext()) {
|
||||
add(cursor.toRawEvent())
|
||||
}
|
||||
private fun SQLiteConnection.runRawQuery(query: QuerySpec): List<RawEvent> =
|
||||
prepare(query.sql).use { stmt ->
|
||||
query.args.forEachIndexed { index, arg ->
|
||||
stmt.bindText(index + 1, arg)
|
||||
}
|
||||
val results = ArrayList<RawEvent>()
|
||||
while (stmt.step()) {
|
||||
results.add(stmt.toRawEvent())
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
private inline fun <T : Event> SQLiteDatabase.runQuery(
|
||||
private inline fun <T : Event> SQLiteConnection.runQuery(
|
||||
query: QuerySpec,
|
||||
onEach: (T) -> Unit,
|
||||
) = rawQuery(query.sql, query.args.toTypedArray()).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
onEach(cursor.toEvent())
|
||||
) = prepare(query.sql).use { stmt ->
|
||||
query.args.forEachIndexed { index, arg ->
|
||||
stmt.bindText(index + 1, arg)
|
||||
}
|
||||
while (stmt.step()) {
|
||||
onEach(stmt.toEvent())
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun SQLiteDatabase.runRawQuery(
|
||||
private inline fun SQLiteConnection.runRawQuery(
|
||||
query: QuerySpec,
|
||||
onEach: (RawEvent) -> Unit,
|
||||
) = rawQuery(query.sql, query.args.toTypedArray()).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
onEach(cursor.toRawEvent())
|
||||
) = prepare(query.sql).use { stmt ->
|
||||
query.args.forEachIndexed { index, arg ->
|
||||
stmt.bindText(index + 1, arg)
|
||||
}
|
||||
while (stmt.step()) {
|
||||
onEach(stmt.toRawEvent())
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T : Event> Cursor.toEvent() =
|
||||
private fun <T : Event> SQLiteStatement.toEvent() =
|
||||
EventFactory.create<T>(
|
||||
getString(0).intern(),
|
||||
getString(1).intern(),
|
||||
getText(0),
|
||||
getText(1),
|
||||
getLong(2),
|
||||
getInt(3),
|
||||
OptimizedJsonMapper.fromJsonToTagArray(getString(4)),
|
||||
getString(5),
|
||||
getString(6),
|
||||
OptimizedJsonMapper.fromJsonToTagArray(getText(4)),
|
||||
getText(5),
|
||||
getText(6),
|
||||
)
|
||||
|
||||
private fun Cursor.toRawEvent() =
|
||||
private fun SQLiteStatement.toRawEvent() =
|
||||
RawEvent(
|
||||
getString(0),
|
||||
getString(1),
|
||||
getText(0),
|
||||
getText(1),
|
||||
getLong(2),
|
||||
getInt(3),
|
||||
getString(4),
|
||||
getString(5),
|
||||
getString(6),
|
||||
getText(4),
|
||||
getText(5),
|
||||
getText(6),
|
||||
)
|
||||
|
||||
// --------------
|
||||
@@ -247,7 +260,7 @@ class QueryBuilder(
|
||||
// -------------
|
||||
fun count(
|
||||
filter: Filter,
|
||||
db: SQLiteDatabase,
|
||||
db: SQLiteConnection,
|
||||
): Int {
|
||||
val newFilter = filter.toFilterWithDTags()
|
||||
|
||||
@@ -277,27 +290,30 @@ class QueryBuilder(
|
||||
|
||||
fun count(
|
||||
filters: List<Filter>,
|
||||
db: SQLiteDatabase,
|
||||
db: SQLiteConnection,
|
||||
): Int {
|
||||
val rowIdSubqueries = unionSubqueriesIfNeeded(filters, hasher(db)) ?: return db.countEverything()
|
||||
|
||||
return db.countIn(rowIdSubqueries.sql, rowIdSubqueries.args)
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.countEverything() = runCount("SELECT count(*) as count FROM event_headers")
|
||||
private fun SQLiteConnection.countEverything() = runCount("SELECT count(*) as count FROM event_headers")
|
||||
|
||||
private fun SQLiteDatabase.countIn(
|
||||
private fun SQLiteConnection.countIn(
|
||||
rowIdQuery: String,
|
||||
args: List<String>,
|
||||
) = runCount("SELECT COUNT(*) as count FROM ($rowIdQuery)", args)
|
||||
|
||||
private fun SQLiteDatabase.runCount(
|
||||
private fun SQLiteConnection.runCount(
|
||||
sql: String,
|
||||
args: List<String> = emptyList(),
|
||||
): Int =
|
||||
rawQuery(sql, args.toTypedArray()).use { cursor ->
|
||||
cursor.moveToNext()
|
||||
cursor.getInt(0)
|
||||
prepare(sql).use { stmt ->
|
||||
args.forEachIndexed { index, arg ->
|
||||
stmt.bindText(index + 1, arg)
|
||||
}
|
||||
stmt.step()
|
||||
stmt.getInt(0)
|
||||
}
|
||||
|
||||
// --------------
|
||||
@@ -305,7 +321,7 @@ class QueryBuilder(
|
||||
// -------------
|
||||
fun delete(
|
||||
filter: Filter,
|
||||
db: SQLiteDatabase,
|
||||
db: SQLiteConnection,
|
||||
): Int {
|
||||
val rowIdQuery = prepareRowIDSubQueries(filter, hasher(db))
|
||||
|
||||
@@ -318,17 +334,25 @@ class QueryBuilder(
|
||||
|
||||
fun delete(
|
||||
filters: List<Filter>,
|
||||
db: SQLiteDatabase,
|
||||
db: SQLiteConnection,
|
||||
): Int {
|
||||
val rowIdSubqueries = unionSubqueriesIfNeeded(filters, hasher(db)) ?: return 0
|
||||
|
||||
return db.runDelete(rowIdSubqueries.sql, rowIdSubqueries.args)
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.runDelete(
|
||||
private fun SQLiteConnection.runDelete(
|
||||
sql: String,
|
||||
args: List<String> = emptyList(),
|
||||
): Int = delete("event_headers", "row_id IN ($sql)", args.toTypedArray())
|
||||
): Int {
|
||||
prepare("DELETE FROM event_headers WHERE row_id IN ($sql)").use { stmt ->
|
||||
args.forEachIndexed { index, arg ->
|
||||
stmt.bindText(index + 1, arg)
|
||||
}
|
||||
stmt.step()
|
||||
}
|
||||
return changes()
|
||||
}
|
||||
|
||||
// ---------------------------------
|
||||
// Prepare unions of all the filters
|
||||
@@ -448,7 +472,9 @@ class QueryBuilder(
|
||||
where {
|
||||
// the order should match indexes
|
||||
// ids reduce the filter the most
|
||||
filter.ids?.let { equalsOrIn("event_headers.id", it) }
|
||||
filter.ids?.let {
|
||||
equalsOrIn("event_headers.id", it)
|
||||
}
|
||||
|
||||
// it's quite rare to have 2 tags in the filter, but possible
|
||||
nonDTagsIn.keys.forEachIndexed { index, tagName ->
|
||||
@@ -476,18 +502,25 @@ class QueryBuilder(
|
||||
} else {
|
||||
"event_tagsAll${index}_$valueIndex.tag_hash"
|
||||
}
|
||||
|
||||
equals(column, hasher.hash(tagName, tagValue))
|
||||
}
|
||||
}
|
||||
|
||||
// range search is bad but most of the time these are up the top with few elements.
|
||||
if (reverseLookup) {
|
||||
filter.kinds?.let { equalsOrIn("event_tags.kind", it) }
|
||||
filter.authors?.let { equalsOrIn("event_tags.pubkey_hash", it.map { hasher.hash(it) }) }
|
||||
filter.kinds?.let {
|
||||
equalsOrIn("event_tags.kind", it)
|
||||
}
|
||||
filter.authors?.let {
|
||||
equalsOrIn("event_tags.pubkey_hash", it.map { hasher.hash(it) })
|
||||
}
|
||||
|
||||
filter.since?.let { greaterThanOrEquals("event_tags.created_at", it) }
|
||||
filter.until?.let { lessThanOrEquals("event_tags.created_at", it) }
|
||||
filter.since?.let {
|
||||
greaterThanOrEquals("event_tags.created_at", it)
|
||||
}
|
||||
filter.until?.let {
|
||||
lessThanOrEquals("event_tags.created_at", it)
|
||||
}
|
||||
|
||||
// there are indexes for these, starting with tags.
|
||||
filter.tags?.forEach { (tagName, tagValues) ->
|
||||
@@ -496,8 +529,12 @@ class QueryBuilder(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
filter.kinds?.let { equalsOrIn("event_headers.kind", it) }
|
||||
filter.authors?.let { equalsOrIn("event_headers.pubkey", it) }
|
||||
filter.kinds?.let {
|
||||
equalsOrIn("event_headers.kind", it)
|
||||
}
|
||||
filter.authors?.let {
|
||||
equalsOrIn("event_headers.pubkey", it)
|
||||
}
|
||||
|
||||
// there are indexes for these, starting with tags.
|
||||
filter.tags?.forEach { (tagName, tagValues) ->
|
||||
@@ -506,8 +543,12 @@ class QueryBuilder(
|
||||
}
|
||||
}
|
||||
|
||||
filter.since?.let { greaterThanOrEquals("event_headers.created_at", it) }
|
||||
filter.until?.let { lessThanOrEquals("event_headers.created_at", it) }
|
||||
filter.since?.let {
|
||||
greaterThanOrEquals("event_headers.created_at", it)
|
||||
}
|
||||
filter.until?.let {
|
||||
lessThanOrEquals("event_headers.created_at", it)
|
||||
}
|
||||
|
||||
// no need to add the replaceable because query_by_kind_pubkey_created already covers it
|
||||
val isAllAddressable = filter.kinds?.all { it.isAddressable() } ?: false
|
||||
@@ -561,18 +602,30 @@ class QueryBuilder(
|
||||
where {
|
||||
// the order should match indexes
|
||||
// ids reduce the filter the most
|
||||
ids?.let { equalsOrIn("event_headers.id", it) }
|
||||
ids?.let {
|
||||
equalsOrIn("event_headers.id", it)
|
||||
}
|
||||
|
||||
match(fts.tableName, search)
|
||||
|
||||
kinds?.let { equalsOrIn("event_headers.kind", it) }
|
||||
authors?.let { equalsOrIn("event_headers.pubkey", it) }
|
||||
kinds?.let {
|
||||
equalsOrIn("event_headers.kind", it)
|
||||
}
|
||||
authors?.let {
|
||||
equalsOrIn("event_headers.pubkey", it)
|
||||
}
|
||||
|
||||
// there are indexes for these, starting with tags.
|
||||
dTags?.let { equalsOrIn("event_headers.d_tag", it) }
|
||||
dTags?.let {
|
||||
equalsOrIn("event_headers.d_tag", it)
|
||||
}
|
||||
|
||||
since?.let { greaterThanOrEquals("event_headers.created_at", it) }
|
||||
until?.let { lessThanOrEquals("event_headers.created_at", it) }
|
||||
since?.let {
|
||||
greaterThanOrEquals("event_headers.created_at", it)
|
||||
}
|
||||
until?.let {
|
||||
lessThanOrEquals("event_headers.created_at", it)
|
||||
}
|
||||
|
||||
// if this is a dTag filter, it is likely that all kinds are addressables
|
||||
// and so force the use of the addressable index
|
||||
@@ -618,16 +671,28 @@ class QueryBuilder(
|
||||
where {
|
||||
// the order should match indexes
|
||||
// ids reduce the filter the most
|
||||
ids?.let { equalsOrIn("id", it) }
|
||||
ids?.let {
|
||||
equalsOrIn("id", it)
|
||||
}
|
||||
|
||||
kinds?.let { equalsOrIn("kind", it) }
|
||||
authors?.let { equalsOrIn("pubkey", it) }
|
||||
kinds?.let {
|
||||
equalsOrIn("kind", it)
|
||||
}
|
||||
authors?.let {
|
||||
equalsOrIn("pubkey", it)
|
||||
}
|
||||
|
||||
// there are indexes for these, starting with tags.
|
||||
dTags?.let { equalsOrIn("d_tag", it) }
|
||||
dTags?.let {
|
||||
equalsOrIn("d_tag", it)
|
||||
}
|
||||
|
||||
since?.let { greaterThanOrEquals("created_at", it) }
|
||||
until?.let { lessThanOrEquals("created_at", it) }
|
||||
since?.let {
|
||||
greaterThanOrEquals("created_at", it)
|
||||
}
|
||||
until?.let {
|
||||
lessThanOrEquals("created_at", it)
|
||||
}
|
||||
|
||||
// if this is a dTag filter, it is likely that all kinds are addressables
|
||||
// and so force the use of the addressable index
|
||||
+12
-8
@@ -20,25 +20,29 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import androidx.sqlite.SQLiteConnection
|
||||
|
||||
fun SQLiteEventStore.explainQuery(
|
||||
sql: String,
|
||||
args: Array<Any> = emptyArray(),
|
||||
) = readableDatabase.explainQuery(sql, args.map { it.toString() }.toTypedArray())
|
||||
) = connection.explainQuery(sql, args.map { it.toString() }.toTypedArray())
|
||||
|
||||
fun SQLiteDatabase.explainQuery(
|
||||
fun SQLiteConnection.explainQuery(
|
||||
sql: String,
|
||||
args: Array<String> = emptyArray(),
|
||||
): String =
|
||||
rawQuery("EXPLAIN QUERY PLAN $sql", args).use { cursor ->
|
||||
prepare("EXPLAIN QUERY PLAN $sql").use { stmt ->
|
||||
args.forEachIndexed { index, arg ->
|
||||
stmt.bindText(index + 1, arg)
|
||||
}
|
||||
|
||||
val treeIndex = mutableMapOf<Int, PlanNode>()
|
||||
val rootNodes = mutableListOf<PlanNode>()
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getInt(0)
|
||||
val parentId = cursor.getInt(1)
|
||||
val detail = cursor.getString(3)
|
||||
while (stmt.step()) {
|
||||
val id = stmt.getInt(0)
|
||||
val parentId = stmt.getInt(1)
|
||||
val detail = stmt.getText(3)
|
||||
|
||||
val line = PlanNode(detail)
|
||||
|
||||
+1
-1
@@ -87,7 +87,7 @@ It is initialized with a `SQLiteDatabase` instance, and it manages the underlyin
|
||||
To initialize the `EventStore` in your Application class:
|
||||
|
||||
```kotlin
|
||||
val eventStore = EventStore(context, "dbname.db", relayUrlIdentifier)
|
||||
val eventStore = EventStore("dbname.db", relayUrlIdentifier)
|
||||
```
|
||||
|
||||
### Querying Events
|
||||
+4
-4
@@ -20,10 +20,10 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import androidx.sqlite.SQLiteConnection
|
||||
|
||||
class ReplaceableModule : IModule {
|
||||
override fun create(db: SQLiteDatabase) {
|
||||
override fun create(db: SQLiteConnection) {
|
||||
db.execSQL(
|
||||
"""
|
||||
CREATE UNIQUE INDEX replaceable_idx
|
||||
@@ -54,7 +54,7 @@ class ReplaceableModule : IModule {
|
||||
)
|
||||
}
|
||||
|
||||
override fun drop(db: SQLiteDatabase) {}
|
||||
override fun drop(db: SQLiteConnection) {}
|
||||
|
||||
override fun deleteAll(db: SQLiteDatabase) {}
|
||||
override fun deleteAll(db: SQLiteConnection) {}
|
||||
}
|
||||
+12
-11
@@ -20,14 +20,14 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import androidx.sqlite.SQLiteConnection
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent
|
||||
|
||||
class RightToVanishModule(
|
||||
val hasher: (db: SQLiteDatabase) -> TagNameValueHasher,
|
||||
val hasher: (db: SQLiteConnection) -> TagNameValueHasher,
|
||||
) : IModule {
|
||||
override fun create(db: SQLiteDatabase) {
|
||||
override fun create(db: SQLiteConnection) {
|
||||
db.execSQL(
|
||||
"""
|
||||
CREATE TABLE event_vanish (
|
||||
@@ -88,7 +88,7 @@ class RightToVanishModule(
|
||||
)
|
||||
}
|
||||
|
||||
override fun drop(db: SQLiteDatabase) {
|
||||
override fun drop(db: SQLiteConnection) {
|
||||
db.execSQL("DROP TABLE IF EXISTS event_vanish")
|
||||
}
|
||||
|
||||
@@ -102,18 +102,19 @@ class RightToVanishModule(
|
||||
event: Event,
|
||||
relayUrl: String?,
|
||||
headerId: Long,
|
||||
db: SQLiteDatabase,
|
||||
db: SQLiteConnection,
|
||||
) {
|
||||
if (event is RequestToVanishEvent && event.shouldVanishFrom(relayUrl)) {
|
||||
val stmt = db.compileStatement(insertRTV)
|
||||
stmt.bindLong(1, headerId)
|
||||
stmt.bindLong(2, hasher(db).hash(event.pubKey))
|
||||
stmt.bindLong(3, event.createdAt)
|
||||
stmt.executeInsert()
|
||||
db.prepare(insertRTV).use { stmt ->
|
||||
stmt.bindLong(1, headerId)
|
||||
stmt.bindLong(2, hasher(db).hash(event.pubKey))
|
||||
stmt.bindLong(3, event.createdAt)
|
||||
stmt.step()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteAll(db: SQLiteDatabase) {
|
||||
override fun deleteAll(db: SQLiteConnection) {
|
||||
db.execSQL("DELETE FROM event_vanish")
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import androidx.sqlite.SQLiteConnection
|
||||
import androidx.sqlite.SQLiteStatement
|
||||
|
||||
inline fun <T> SQLiteStatement.use(block: (SQLiteStatement) -> T): T {
|
||||
try {
|
||||
return block(this)
|
||||
} finally {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
fun SQLiteConnection.execSQL(sql: String) {
|
||||
prepare(sql).use { it.step() }
|
||||
}
|
||||
|
||||
fun SQLiteConnection.execSQL(
|
||||
sql: String,
|
||||
args: Array<out Any>,
|
||||
) {
|
||||
prepare(sql).use { stmt ->
|
||||
args.forEachIndexed { index, arg ->
|
||||
stmt.bindAny(index + 1, arg)
|
||||
}
|
||||
stmt.step()
|
||||
}
|
||||
}
|
||||
|
||||
fun SQLiteConnection.lastInsertRowId(): Long =
|
||||
prepare("SELECT last_insert_rowid()").use { stmt ->
|
||||
stmt.step()
|
||||
stmt.getLong(0)
|
||||
}
|
||||
|
||||
fun SQLiteConnection.deleteRows(
|
||||
table: String,
|
||||
whereClause: String,
|
||||
args: Array<out String>,
|
||||
): Int {
|
||||
execSQL("DELETE FROM $table WHERE $whereClause", args)
|
||||
return changes()
|
||||
}
|
||||
|
||||
fun SQLiteConnection.changes(): Int =
|
||||
prepare("SELECT changes()").use { stmt ->
|
||||
stmt.step()
|
||||
stmt.getInt(0)
|
||||
}
|
||||
|
||||
inline fun <T> SQLiteConnection.transaction(body: SQLiteConnection.() -> T): T {
|
||||
execSQL("BEGIN IMMEDIATE TRANSACTION")
|
||||
try {
|
||||
val result = body()
|
||||
execSQL("END TRANSACTION")
|
||||
return result
|
||||
} catch (e: Throwable) {
|
||||
execSQL("ROLLBACK TRANSACTION")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
fun SQLiteStatement.bindAny(
|
||||
index: Int,
|
||||
value: Any,
|
||||
) {
|
||||
when (value) {
|
||||
is String -> bindText(index, value)
|
||||
is Long -> bindLong(index, value)
|
||||
is Int -> bindInt(index, value)
|
||||
is Double -> bindDouble(index, value)
|
||||
is ByteArray -> bindBlob(index, value)
|
||||
else -> bindText(index, value.toString())
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> SQLiteConnection.rawQuery(
|
||||
sql: String,
|
||||
args: List<String>,
|
||||
block: (SQLiteStatement) -> T,
|
||||
): T =
|
||||
prepare(sql).use { stmt ->
|
||||
args.forEachIndexed { index, arg ->
|
||||
stmt.bindText(index + 1, arg)
|
||||
}
|
||||
block(stmt)
|
||||
}
|
||||
+85
-55
@@ -20,11 +20,10 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import androidx.core.database.sqlite.transaction
|
||||
import androidx.sqlite.SQLiteConnection
|
||||
import androidx.sqlite.SQLiteDriver
|
||||
import androidx.sqlite.SQLiteException
|
||||
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Kind
|
||||
@@ -35,22 +34,31 @@ import com.vitorpamplona.quartz.nip01Core.store.IEventStore
|
||||
import com.vitorpamplona.quartz.nip40Expiration.isExpired
|
||||
import com.vitorpamplona.quartz.utils.EventFactory
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class SQLiteEventStore(
|
||||
val context: Context,
|
||||
val driver: SQLiteDriver = BundledSQLiteDriver(),
|
||||
val dbName: String? = "events.db",
|
||||
val relayUrl: String? = null,
|
||||
val indexStrategy: IndexingStrategy = DefaultIndexingStrategy(),
|
||||
) : SQLiteOpenHelper(context, dbName, null, DATABASE_VERSION) {
|
||||
) {
|
||||
companion object {
|
||||
const val DATABASE_VERSION = 2
|
||||
}
|
||||
|
||||
val connection: SQLiteConnection by lazy {
|
||||
openAndConfigure()
|
||||
}
|
||||
|
||||
val seedModule = SeedModule()
|
||||
|
||||
val fullTextSearchModule = FullTextSearchModule()
|
||||
val eventIndexModule = EventIndexesModule(seedModule::hasher, indexStrategy)
|
||||
val eventIndexModule =
|
||||
EventIndexesModule(
|
||||
seedModule::hasher,
|
||||
indexStrategy,
|
||||
)
|
||||
|
||||
val replaceableModule = ReplaceableModule()
|
||||
val addressableModule = AddressableModule()
|
||||
@@ -60,7 +68,12 @@ class SQLiteEventStore(
|
||||
val expirationModule = ExpirationModule()
|
||||
val rightToVanishModule = RightToVanishModule(seedModule::hasher)
|
||||
|
||||
val queryBuilder = QueryBuilder(fullTextSearchModule, seedModule::hasher, indexStrategy)
|
||||
val queryBuilder =
|
||||
QueryBuilder(
|
||||
fullTextSearchModule,
|
||||
seedModule::hasher,
|
||||
indexStrategy,
|
||||
)
|
||||
|
||||
val modules =
|
||||
listOf(
|
||||
@@ -75,39 +88,56 @@ class SQLiteEventStore(
|
||||
fullTextSearchModule,
|
||||
)
|
||||
|
||||
override fun onConfigure(db: SQLiteDatabase) {
|
||||
super.onConfigure(db)
|
||||
private fun openAndConfigure(): SQLiteConnection {
|
||||
val db = driver.open(dbName ?: ":memory:")
|
||||
|
||||
// 32MB memory cache
|
||||
db.execSQL("PRAGMA cache_size=-32000;")
|
||||
|
||||
// makes sure the FKs are sane
|
||||
db.setForeignKeyConstraintsEnabled(true)
|
||||
db.execSQL("PRAGMA foreign_keys = ON;")
|
||||
|
||||
// SQLite implements mutations by appending them to a log, which it occasionally
|
||||
// compacts into the database. This is called Write-Ahead Logging (WAL)
|
||||
db.enableWriteAheadLogging()
|
||||
db.execSQL("PRAGMA journal_mode = WAL;")
|
||||
|
||||
// The DB can be corrupted if the OS is shutdown before sync, which generally
|
||||
// doesn't happen on Android
|
||||
db.execSQL("PRAGMA synchronous = OFF;")
|
||||
|
||||
val currentVersion = getUserVersion(db)
|
||||
if (currentVersion == 0) {
|
||||
onCreate(db)
|
||||
setUserVersion(db, DATABASE_VERSION)
|
||||
} else if (currentVersion < DATABASE_VERSION) {
|
||||
onUpgrade(db, currentVersion, DATABASE_VERSION)
|
||||
setUserVersion(db, DATABASE_VERSION)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
fun dbSizeMB(): Int {
|
||||
val f1 = context.getDatabasePath(dbName)
|
||||
val f2 = context.getDatabasePath("$dbName-wal")
|
||||
val total = f1.length() + f2.length()
|
||||
return (total / (1024 * 1024)).toInt()
|
||||
private fun getUserVersion(db: SQLiteConnection): Int =
|
||||
db.prepare("PRAGMA user_version").use { stmt ->
|
||||
stmt.step()
|
||||
stmt.getInt(0)
|
||||
}
|
||||
|
||||
private fun setUserVersion(
|
||||
db: SQLiteConnection,
|
||||
version: Int,
|
||||
) {
|
||||
db.execSQL("PRAGMA user_version = $version")
|
||||
}
|
||||
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
fun onCreate(db: SQLiteConnection) {
|
||||
modules.forEach {
|
||||
it.create(db)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUpgrade(
|
||||
db: SQLiteDatabase,
|
||||
fun onUpgrade(
|
||||
db: SQLiteConnection,
|
||||
oldVersion: Int,
|
||||
newVersion: Int,
|
||||
) {
|
||||
@@ -125,15 +155,14 @@ class SQLiteEventStore(
|
||||
}
|
||||
|
||||
fun clearDB() {
|
||||
val db = writableDatabase
|
||||
modules.reversed().forEach { it.deleteAll(db) }
|
||||
modules.reversed().forEach { it.deleteAll(connection) }
|
||||
}
|
||||
|
||||
suspend fun vacuum() {
|
||||
// 1. ANALYZE: Collects statistics about tables and indices
|
||||
// to help the query planner optimize queries.
|
||||
withContext(Dispatchers.IO) {
|
||||
writableDatabase.execSQL("VACUUM")
|
||||
connection.execSQL("VACUUM")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,13 +170,13 @@ class SQLiteEventStore(
|
||||
// 2. VACUUM: Rebuilds the database file, reclaiming unused space
|
||||
// and reducing fragmentation.
|
||||
withContext(Dispatchers.IO) {
|
||||
writableDatabase.execSQL("ANALYZE")
|
||||
connection.execSQL("ANALYZE")
|
||||
}
|
||||
}
|
||||
|
||||
private fun innerInsertEvent(
|
||||
event: Event,
|
||||
db: SQLiteDatabase,
|
||||
db: SQLiteConnection,
|
||||
) {
|
||||
val headerId = eventIndexModule.insert(event, db)
|
||||
deletionModule.insert(event, db)
|
||||
@@ -156,83 +185,84 @@ class SQLiteEventStore(
|
||||
rightToVanishModule.insert(event, relayUrl, headerId, db)
|
||||
}
|
||||
|
||||
fun insertEvent(event: Event): Boolean {
|
||||
if (event.isExpired()) throw SQLiteConstraintException("blocked: Cannot insert an expired event")
|
||||
if (event.kind.isEphemeral()) return false
|
||||
fun insertEvent(event: Event) {
|
||||
if (event.isExpired()) throw SQLiteException("blocked: Cannot insert an expired event")
|
||||
if (event.kind.isEphemeral()) return
|
||||
|
||||
writableDatabase.transaction {
|
||||
connection.transaction {
|
||||
innerInsertEvent(event, this)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
inner class Transaction(
|
||||
val db: SQLiteDatabase,
|
||||
val db: SQLiteConnection,
|
||||
) : IEventStore.ITransaction {
|
||||
override fun insert(event: Event): Boolean {
|
||||
if (event.isExpired()) throw SQLiteConstraintException("blocked: Cannot insert an expired event")
|
||||
if (event.kind.isEphemeral()) return false
|
||||
override fun insert(event: Event) {
|
||||
if (event.isExpired()) throw SQLiteException("blocked: Cannot insert an expired event")
|
||||
if (event.kind.isEphemeral()) return
|
||||
|
||||
innerInsertEvent(event, db)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
fun transaction(body: Transaction.() -> Unit) {
|
||||
writableDatabase.transaction {
|
||||
connection.transaction {
|
||||
with(Transaction(this)) {
|
||||
body()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : Event> query(filter: Filter): List<T> = queryBuilder.query(filter, readableDatabase)
|
||||
fun <T : Event> query(filter: Filter): List<T> = queryBuilder.query(filter, connection)
|
||||
|
||||
fun <T : Event> query(filters: List<Filter>): List<T> = queryBuilder.query(filters, readableDatabase)
|
||||
fun <T : Event> query(filters: List<Filter>): List<T> = queryBuilder.query(filters, connection)
|
||||
|
||||
fun <T : Event> query(
|
||||
filter: Filter,
|
||||
onEach: (T) -> Unit,
|
||||
) = queryBuilder.query(filter, readableDatabase, onEach)
|
||||
) = queryBuilder.query(filter, connection, onEach)
|
||||
|
||||
fun <T : Event> query(
|
||||
filters: List<Filter>,
|
||||
onEach: (T) -> Unit,
|
||||
) = queryBuilder.query(filters, readableDatabase, onEach)
|
||||
) = queryBuilder.query(filters, connection, onEach)
|
||||
|
||||
fun rawQuery(filter: Filter): List<RawEvent> = queryBuilder.rawQuery(filter, readableDatabase)
|
||||
fun rawQuery(filter: Filter): List<RawEvent> = queryBuilder.rawQuery(filter, connection)
|
||||
|
||||
fun rawQuery(filters: List<Filter>): List<RawEvent> = queryBuilder.rawQuery(filters, readableDatabase)
|
||||
fun rawQuery(filters: List<Filter>): List<RawEvent> = queryBuilder.rawQuery(filters, connection)
|
||||
|
||||
fun rawQuery(
|
||||
filter: Filter,
|
||||
onEach: (RawEvent) -> Unit,
|
||||
) = queryBuilder.rawQuery(filter, readableDatabase, onEach)
|
||||
) = queryBuilder.rawQuery(filter, connection, onEach)
|
||||
|
||||
fun rawQuery(
|
||||
filters: List<Filter>,
|
||||
onEach: (RawEvent) -> Unit,
|
||||
) = queryBuilder.rawQuery(filters, readableDatabase, onEach)
|
||||
) = queryBuilder.rawQuery(filters, connection, onEach)
|
||||
|
||||
fun planQuery(filter: Filter) = queryBuilder.planQuery(filter, seedModule.hasher(readableDatabase), readableDatabase)
|
||||
fun planQuery(filter: Filter) = queryBuilder.planQuery(filter, seedModule.hasher(connection), connection)
|
||||
|
||||
fun planQuery(filters: List<Filter>) = queryBuilder.planQuery(filters, seedModule.hasher(readableDatabase), readableDatabase)
|
||||
fun planQuery(filters: List<Filter>) = queryBuilder.planQuery(filters, seedModule.hasher(connection), connection)
|
||||
|
||||
fun count(filter: Filter): Int = queryBuilder.count(filter, readableDatabase)
|
||||
fun count(filter: Filter): Int = queryBuilder.count(filter, connection)
|
||||
|
||||
fun count(filters: List<Filter>): Int = queryBuilder.count(filters, readableDatabase)
|
||||
fun count(filters: List<Filter>): Int = queryBuilder.count(filters, connection)
|
||||
|
||||
fun delete(filter: Filter) {
|
||||
queryBuilder.delete(filter, writableDatabase)
|
||||
queryBuilder.delete(filter, connection)
|
||||
}
|
||||
|
||||
fun delete(filters: List<Filter>) {
|
||||
queryBuilder.delete(filters, writableDatabase)
|
||||
queryBuilder.delete(filters, connection)
|
||||
}
|
||||
|
||||
fun delete(id: HexKey): Int = writableDatabase.delete("event_headers", "id = ?", arrayOf(id))
|
||||
fun delete(id: HexKey): Int {
|
||||
connection.execSQL("DELETE FROM event_headers WHERE id = ?", arrayOf(id))
|
||||
return connection.changes()
|
||||
}
|
||||
|
||||
fun deleteExpiredEvents() = expirationModule.deleteExpiredEvents(writableDatabase)
|
||||
fun deleteExpiredEvents() = expirationModule.deleteExpiredEvents(connection)
|
||||
}
|
||||
|
||||
class RawEvent(
|
||||
@@ -246,8 +276,8 @@ class RawEvent(
|
||||
) {
|
||||
fun <T : Event> toEvent() =
|
||||
EventFactory.create<T>(
|
||||
id.intern(),
|
||||
pubKey.intern(),
|
||||
id,
|
||||
pubKey,
|
||||
createdAt,
|
||||
kind,
|
||||
OptimizedJsonMapper.fromJsonToTagArray(jsonTags),
|
||||
+12
-11
@@ -20,18 +20,19 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import androidx.sqlite.SQLiteConnection
|
||||
import com.vitorpamplona.quartz.utils.RandomInstance
|
||||
|
||||
class SeedModule : IModule {
|
||||
override fun create(db: SQLiteDatabase) {
|
||||
override fun create(db: SQLiteConnection) {
|
||||
db.execSQL("CREATE TABLE seeds (seed_value INTEGER)")
|
||||
|
||||
val insertSeed = "INSERT INTO seeds (seed_value) VALUES (?)"
|
||||
|
||||
val stmt = db.compileStatement(insertSeed)
|
||||
stmt.bindLong(1, RandomInstance.long())
|
||||
stmt.executeInsert()
|
||||
db.prepare(insertSeed).use { stmt ->
|
||||
stmt.bindLong(1, RandomInstance.long())
|
||||
stmt.step()
|
||||
}
|
||||
|
||||
// Prevent updates to maintain immutability
|
||||
db.execSQL(
|
||||
@@ -65,19 +66,19 @@ class SeedModule : IModule {
|
||||
)
|
||||
}
|
||||
|
||||
fun getSeed(db: SQLiteDatabase): Long =
|
||||
db.rawQuery("SELECT seed_value FROM seeds LIMIT 1", null).use {
|
||||
it.moveToFirst()
|
||||
fun getSeed(db: SQLiteConnection): Long =
|
||||
db.prepare("SELECT seed_value FROM seeds LIMIT 1").use {
|
||||
it.step()
|
||||
it.getLong(0)
|
||||
}
|
||||
|
||||
override fun drop(db: SQLiteDatabase) {
|
||||
override fun drop(db: SQLiteConnection) {
|
||||
db.execSQL("DROP TABLE IF EXISTS seeds")
|
||||
}
|
||||
|
||||
override fun deleteAll(db: SQLiteDatabase) {}
|
||||
override fun deleteAll(db: SQLiteConnection) {}
|
||||
|
||||
private var hasherCache: TagNameValueHasher? = null
|
||||
|
||||
fun hasher(db: SQLiteDatabase): TagNameValueHasher = hasherCache ?: TagNameValueHasher(getSeed(db)).also { hasherCache = it }
|
||||
fun hasher(db: SQLiteConnection): TagNameValueHasher = hasherCache ?: TagNameValueHasher(getSeed(db)).also { hasherCache = it }
|
||||
}
|
||||
+16
-16
@@ -20,71 +20,71 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite.sql
|
||||
|
||||
sealed class Condition {
|
||||
sealed interface Condition {
|
||||
data class Raw(
|
||||
val condition: String,
|
||||
) : Condition()
|
||||
) : Condition
|
||||
|
||||
data class Equals(
|
||||
val column: String,
|
||||
val value: Any?,
|
||||
) : Condition()
|
||||
) : Condition
|
||||
|
||||
data class NotEquals(
|
||||
val column: String,
|
||||
val value: Any?,
|
||||
) : Condition()
|
||||
) : Condition
|
||||
|
||||
data class GreaterThan(
|
||||
val column: String,
|
||||
val value: Any,
|
||||
) : Condition()
|
||||
) : Condition
|
||||
|
||||
data class GreaterThanOrEquals(
|
||||
val column: String,
|
||||
val value: Any,
|
||||
) : Condition()
|
||||
) : Condition
|
||||
|
||||
data class LessThan(
|
||||
val column: String,
|
||||
val value: Any,
|
||||
) : Condition()
|
||||
) : Condition
|
||||
|
||||
data class LessThanOrEquals(
|
||||
val column: String,
|
||||
val value: Any,
|
||||
) : Condition()
|
||||
) : Condition
|
||||
|
||||
data class Like(
|
||||
val column: String,
|
||||
val value: String,
|
||||
) : Condition()
|
||||
) : Condition
|
||||
|
||||
data class Match(
|
||||
val table: String,
|
||||
val value: String,
|
||||
) : Condition()
|
||||
) : Condition
|
||||
|
||||
data class IsNull(
|
||||
val column: String,
|
||||
) : Condition()
|
||||
) : Condition
|
||||
|
||||
data class IsNotNull(
|
||||
val column: String,
|
||||
) : Condition()
|
||||
) : Condition
|
||||
|
||||
data class In(
|
||||
val column: String,
|
||||
val values: List<Any>,
|
||||
) : Condition()
|
||||
) : Condition
|
||||
|
||||
data class And(
|
||||
val conditions: List<Condition>,
|
||||
) : Condition()
|
||||
) : Condition
|
||||
|
||||
data class Or(
|
||||
val conditions: List<Condition>,
|
||||
) : Condition()
|
||||
) : Condition
|
||||
|
||||
class Empty : Condition()
|
||||
class Empty : Condition
|
||||
}
|
||||
+4
-1
@@ -28,7 +28,10 @@ class SqlSelectionBuilder(
|
||||
fun build(): WhereClause {
|
||||
selectionArgs.clear() // Clear previous args for a fresh build
|
||||
val conditions = buildCondition(condition)
|
||||
return WhereClause(conditions, selectionArgs)
|
||||
return WhereClause(
|
||||
conditions,
|
||||
selectionArgs,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
+44
-13
@@ -23,56 +23,83 @@ package com.vitorpamplona.quartz.nip01Core.store.sqlite.sql
|
||||
class WhereClauseBuilder {
|
||||
private val conditions = mutableListOf<Condition>()
|
||||
|
||||
fun raw(condition: String) = apply { conditions.add(Condition.Raw(condition)) }
|
||||
fun raw(condition: String) =
|
||||
apply {
|
||||
conditions.add(Condition.Raw(condition))
|
||||
}
|
||||
|
||||
fun equals(
|
||||
column: String,
|
||||
value: Any?,
|
||||
) = apply { conditions.add(Condition.Equals(column, value)) }
|
||||
) = apply {
|
||||
conditions.add(Condition.Equals(column, value))
|
||||
}
|
||||
|
||||
fun notEquals(
|
||||
column: String,
|
||||
value: Any?,
|
||||
) = apply { conditions.add(Condition.NotEquals(column, value)) }
|
||||
) = apply {
|
||||
conditions.add(Condition.NotEquals(column, value))
|
||||
}
|
||||
|
||||
fun greaterThan(
|
||||
column: String,
|
||||
value: Any,
|
||||
) = apply { conditions.add(Condition.GreaterThan(column, value)) }
|
||||
) = apply {
|
||||
conditions.add(Condition.GreaterThan(column, value))
|
||||
}
|
||||
|
||||
fun greaterThanOrEquals(
|
||||
column: String,
|
||||
value: Any,
|
||||
) = apply { conditions.add(Condition.GreaterThanOrEquals(column, value)) }
|
||||
) = apply {
|
||||
conditions.add(Condition.GreaterThanOrEquals(column, value))
|
||||
}
|
||||
|
||||
fun lessThan(
|
||||
column: String,
|
||||
value: Any,
|
||||
) = apply { conditions.add(Condition.LessThan(column, value)) }
|
||||
) = apply {
|
||||
conditions.add(Condition.LessThan(column, value))
|
||||
}
|
||||
|
||||
fun lessThanOrEquals(
|
||||
column: String,
|
||||
value: Any,
|
||||
) = apply { conditions.add(Condition.LessThanOrEquals(column, value)) }
|
||||
) = apply {
|
||||
conditions.add(Condition.LessThanOrEquals(column, value))
|
||||
}
|
||||
|
||||
fun like(
|
||||
column: String,
|
||||
pattern: String,
|
||||
) = apply { conditions.add(Condition.Like(column, pattern)) }
|
||||
) = apply {
|
||||
conditions.add(Condition.Like(column, pattern))
|
||||
}
|
||||
|
||||
fun match(
|
||||
table: String,
|
||||
pattern: String,
|
||||
) = apply { conditions.add(Condition.Match(table, pattern)) }
|
||||
) = apply {
|
||||
conditions.add(Condition.Match(table, pattern))
|
||||
}
|
||||
|
||||
fun isNull(column: String) = apply { conditions.add(Condition.IsNull(column)) }
|
||||
fun isNull(column: String) =
|
||||
apply {
|
||||
conditions.add(Condition.IsNull(column))
|
||||
}
|
||||
|
||||
fun isNotNull(column: String) = apply { conditions.add(Condition.IsNotNull(column)) }
|
||||
fun isNotNull(column: String) =
|
||||
apply {
|
||||
conditions.add(Condition.IsNotNull(column))
|
||||
}
|
||||
|
||||
fun isIn(
|
||||
column: String,
|
||||
values: List<Any>,
|
||||
) = apply { conditions.add(Condition.In(column, values)) }
|
||||
) = apply {
|
||||
conditions.add(Condition.In(column, values))
|
||||
}
|
||||
|
||||
fun equalsOrIn(
|
||||
column: String,
|
||||
@@ -119,7 +146,11 @@ class WhereClauseBuilder {
|
||||
}
|
||||
|
||||
fun where(block: WhereClauseBuilder.() -> Unit): WhereClause {
|
||||
val condition = WhereClauseBuilder().apply(block).buildAnd() ?: Condition.Empty()
|
||||
val condition =
|
||||
WhereClauseBuilder()
|
||||
.apply(block)
|
||||
.buildAnd() ?: Condition
|
||||
.Empty()
|
||||
return SqlSelectionBuilder(condition).build()
|
||||
}
|
||||
|
||||
@@ -21,5 +21,7 @@
|
||||
package com.vitorpamplona.quartz
|
||||
|
||||
expect class TestResourceLoader() {
|
||||
fun loadDecompressString(file: String): String
|
||||
|
||||
fun loadString(file: String): String
|
||||
}
|
||||
|
||||
+6
-13
@@ -20,15 +20,14 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
import androidx.sqlite.SQLiteException
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import junit.framework.TestCase
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.fail
|
||||
import org.junit.Test
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class AddressableTest : BaseDBTest() {
|
||||
val signer = NostrSignerSync()
|
||||
@@ -76,18 +75,12 @@ class AddressableTest : BaseDBTest() {
|
||||
|
||||
db.assertQuery(version3, Filter(ids = listOf(version3.id)))
|
||||
|
||||
try {
|
||||
assertFailsWith<SQLiteException> {
|
||||
db.insert(version2)
|
||||
fail("It should not allow inserting an older version")
|
||||
} catch (e: Exception) {
|
||||
TestCase.assertTrue(e is SQLiteConstraintException)
|
||||
}
|
||||
|
||||
try {
|
||||
assertFailsWith<SQLiteException> {
|
||||
db.insert(version1)
|
||||
fail("It should not allow inserting an older version")
|
||||
} catch (e: Exception) {
|
||||
TestCase.assertTrue(e is SQLiteConstraintException)
|
||||
}
|
||||
|
||||
db.assertQuery(version3, Filter(ids = listOf(version3.id)))
|
||||
+17
-17
@@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import junit.framework.TestCase
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
fun <T : Event> EventStore.assertQuery(
|
||||
expected: T?,
|
||||
@@ -31,12 +31,12 @@ fun <T : Event> EventStore.assertQuery(
|
||||
val queryResult = query<T>(filter)
|
||||
val countResult = count(filter)
|
||||
if (expected == null) {
|
||||
TestCase.assertEquals(0, queryResult.size)
|
||||
TestCase.assertEquals(0, countResult)
|
||||
assertEquals(0, queryResult.size)
|
||||
assertEquals(0, countResult)
|
||||
} else {
|
||||
TestCase.assertEquals(1, queryResult.size)
|
||||
TestCase.assertEquals(1, countResult)
|
||||
TestCase.assertEquals(expected.toJson(), queryResult.first().toJson())
|
||||
assertEquals(1, queryResult.size)
|
||||
assertEquals(1, countResult)
|
||||
assertEquals(expected.toJson(), queryResult.first().toJson())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,10 +46,10 @@ fun <T : Event> EventStore.assertQuery(
|
||||
) {
|
||||
val queryResult = query<T>(filter)
|
||||
val countResult = count(filter)
|
||||
TestCase.assertEquals(expected.size, queryResult.size)
|
||||
TestCase.assertEquals(expected.size, countResult)
|
||||
assertEquals(expected.size, queryResult.size)
|
||||
assertEquals(expected.size, countResult)
|
||||
expected.forEachIndexed { index, event ->
|
||||
TestCase.assertEquals(event.toJson(), queryResult[index].toJson())
|
||||
assertEquals(event.toJson(), queryResult[index].toJson())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,12 +60,12 @@ fun <T : Event> SQLiteEventStore.assertQuery(
|
||||
val queryResult = query<T>(filter)
|
||||
val countResult = count(filter)
|
||||
if (expected == null) {
|
||||
TestCase.assertEquals(0, queryResult.size)
|
||||
TestCase.assertEquals(0, countResult)
|
||||
assertEquals(0, queryResult.size)
|
||||
assertEquals(0, countResult)
|
||||
} else {
|
||||
TestCase.assertEquals(1, queryResult.size)
|
||||
TestCase.assertEquals(1, countResult)
|
||||
TestCase.assertEquals(expected.toJson(), queryResult.first().toJson())
|
||||
assertEquals(1, queryResult.size)
|
||||
assertEquals(1, countResult)
|
||||
assertEquals(expected.toJson(), queryResult.first().toJson())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,9 +75,9 @@ fun <T : Event> SQLiteEventStore.assertQuery(
|
||||
) {
|
||||
val queryResult = query<T>(filter)
|
||||
val countResult = count(filter)
|
||||
TestCase.assertEquals(expected.size, queryResult.size)
|
||||
TestCase.assertEquals(expected.size, countResult)
|
||||
assertEquals(expected.size, queryResult.size)
|
||||
assertEquals(expected.size, countResult)
|
||||
expected.forEachIndexed { index, event ->
|
||||
TestCase.assertEquals(event.toJson(), queryResult[index].toJson())
|
||||
assertEquals(event.toJson(), queryResult[index].toJson())
|
||||
}
|
||||
}
|
||||
+6
-9
@@ -20,10 +20,10 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import com.vitorpamplona.quartz.nip01Core.store.sqlite.DefaultIndexingStrategy
|
||||
import com.vitorpamplona.quartz.nip01Core.store.sqlite.EventStore
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.BeforeTest
|
||||
|
||||
open class BaseDBTest {
|
||||
private lateinit var dbs: MutableMap<String, EventStore>
|
||||
@@ -36,10 +36,8 @@ open class BaseDBTest {
|
||||
useAndIndexIdOnOrderBy=$useAndIndexIdOnOrderBy
|
||||
""".trimIndent()
|
||||
|
||||
@Before
|
||||
@BeforeTest
|
||||
fun setup() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
|
||||
val booleans = listOf(true, false)
|
||||
|
||||
dbs = mutableMapOf<String, EventStore>()
|
||||
@@ -58,7 +56,6 @@ open class BaseDBTest {
|
||||
)
|
||||
dbs[indexStrategy.name()] =
|
||||
EventStore(
|
||||
context = context,
|
||||
dbName = null,
|
||||
indexStrategy = indexStrategy,
|
||||
)
|
||||
@@ -68,7 +65,7 @@ open class BaseDBTest {
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
@AfterTest
|
||||
fun tearDown() {
|
||||
dbs.forEach { it.value.close() }
|
||||
}
|
||||
+3
-3
@@ -28,9 +28,9 @@ import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class BasicTest : BaseDBTest() {
|
||||
val signer = NostrSignerSync()
|
||||
+37
-37
@@ -20,19 +20,20 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
import androidx.sqlite.SQLiteException
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.nip01Core.store.sqlite.TagNameValueHasher
|
||||
import com.vitorpamplona.quartz.nip01Core.store.sqlite.explainQuery
|
||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import junit.framework.TestCase
|
||||
import junit.framework.TestCase.fail
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class DeletionTest : BaseDBTest() {
|
||||
val signer = NostrSignerSync()
|
||||
@@ -61,12 +62,8 @@ class DeletionTest : BaseDBTest() {
|
||||
db.assertQuery(note2, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(note3, Filter(ids = listOf(note3.id)))
|
||||
|
||||
// trying to insert again should fail.
|
||||
try {
|
||||
assertFailsWith<SQLiteException> {
|
||||
db.insert(note1)
|
||||
fail("Should not be able to insert a deleted event")
|
||||
} catch (e: SQLiteConstraintException) {
|
||||
assertEquals("blocked: a deletion event exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message)
|
||||
}
|
||||
|
||||
db.assertQuery(deletion, Filter(ids = listOf(deletion.id)))
|
||||
@@ -107,12 +104,8 @@ class DeletionTest : BaseDBTest() {
|
||||
db.assertQuery(null, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note3.id)))
|
||||
|
||||
// trying to insert again should fail.
|
||||
try {
|
||||
assertFailsWith<SQLiteException> {
|
||||
db.insert(note1)
|
||||
fail("Should not be able to insert a deleted event")
|
||||
} catch (e: SQLiteConstraintException) {
|
||||
assertEquals("blocked: a deletion event exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message)
|
||||
}
|
||||
|
||||
db.assertQuery(deletion, Filter(ids = listOf(deletion.id)))
|
||||
@@ -146,12 +139,8 @@ class DeletionTest : BaseDBTest() {
|
||||
db.assertQuery(null, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note3.id)))
|
||||
|
||||
// trying to insert again should fail.
|
||||
try {
|
||||
assertFailsWith<SQLiteException> {
|
||||
db.insert(note1)
|
||||
fail("Should not be able to insert a deleted event")
|
||||
} catch (e: SQLiteConstraintException) {
|
||||
assertEquals("blocked: a deletion event exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message)
|
||||
}
|
||||
|
||||
db.assertQuery(deletion, Filter(ids = listOf(deletion.id)))
|
||||
@@ -192,12 +181,8 @@ class DeletionTest : BaseDBTest() {
|
||||
db.assertQuery(null, Filter(ids = listOf(wrap1.id)))
|
||||
db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id)))
|
||||
|
||||
// trying to insert again should fail.
|
||||
try {
|
||||
assertFailsWith<SQLiteException> {
|
||||
db.insert(wrap1)
|
||||
fail("Should not be able to insert a deleted event")
|
||||
} catch (e: SQLiteConstraintException) {
|
||||
assertEquals("blocked: a deletion event exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message)
|
||||
}
|
||||
|
||||
db.assertQuery(deletion, Filter(ids = listOf(deletion.id)))
|
||||
@@ -218,7 +203,7 @@ class DeletionTest : BaseDBTest() {
|
||||
val explainer = db.store.explainQuery(sql)
|
||||
|
||||
if (db.indexStrategy.indexTagsWithKindAndPubkey) {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
|$sql
|
||||
|└── SEARCH event_tags USING COVERING INDEX query_by_tags_hash_kind_pubkey (tag_hash=? AND kind=? AND pubkey_hash=? AND created_at>?)
|
||||
@@ -226,7 +211,7 @@ class DeletionTest : BaseDBTest() {
|
||||
explainer,
|
||||
)
|
||||
} else {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
|$sql
|
||||
|└── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=? AND kind=? AND created_at>?)
|
||||
@@ -245,10 +230,13 @@ class DeletionTest : BaseDBTest() {
|
||||
pubkey = "key1",
|
||||
idValues = listOf("ca29c211f", "ca29c211d"),
|
||||
addresses = emptyList(),
|
||||
hasher = TagNameValueHasher(0),
|
||||
hasher =
|
||||
TagNameValueHasher(
|
||||
0,
|
||||
),
|
||||
).first()
|
||||
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
DELETE FROM event_headers
|
||||
WHERE
|
||||
@@ -275,10 +263,13 @@ class DeletionTest : BaseDBTest() {
|
||||
listOf(
|
||||
Address(30000, "key1", "a"),
|
||||
),
|
||||
hasher = TagNameValueHasher(0),
|
||||
hasher =
|
||||
TagNameValueHasher(
|
||||
0,
|
||||
),
|
||||
).first()
|
||||
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
DELETE FROM event_headers
|
||||
WHERE (
|
||||
@@ -309,10 +300,13 @@ class DeletionTest : BaseDBTest() {
|
||||
Address(30000, "key1", "c"),
|
||||
Address(30000, "key1", "d"),
|
||||
),
|
||||
hasher = TagNameValueHasher(0),
|
||||
hasher =
|
||||
TagNameValueHasher(
|
||||
0,
|
||||
),
|
||||
).first()
|
||||
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
DELETE FROM event_headers
|
||||
WHERE (
|
||||
@@ -345,10 +339,13 @@ class DeletionTest : BaseDBTest() {
|
||||
Address(30001, "key2", "e"),
|
||||
Address(30001, "key2", "f"),
|
||||
),
|
||||
hasher = TagNameValueHasher(0),
|
||||
hasher =
|
||||
TagNameValueHasher(
|
||||
0,
|
||||
),
|
||||
).first()
|
||||
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
DELETE FROM event_headers
|
||||
WHERE (
|
||||
@@ -387,10 +384,13 @@ class DeletionTest : BaseDBTest() {
|
||||
Address(10001, "key2", ""),
|
||||
Address(10001, "key2", ""),
|
||||
),
|
||||
hasher = TagNameValueHasher(0),
|
||||
hasher =
|
||||
TagNameValueHasher(
|
||||
0,
|
||||
),
|
||||
).first()
|
||||
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
DELETE FROM event_headers
|
||||
WHERE
|
||||
+9
-9
@@ -20,15 +20,16 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
import androidx.sqlite.SQLiteException
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import junit.framework.TestCase.fail
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class ExpirationTest : BaseDBTest() {
|
||||
val signer = NostrSignerSync()
|
||||
@@ -58,7 +59,9 @@ class ExpirationTest : BaseDBTest() {
|
||||
|
||||
db.assertQuery(noteToExpire, Filter(ids = listOf(noteToExpire.id)))
|
||||
|
||||
Thread.sleep(2000)
|
||||
runBlocking {
|
||||
delay(2000)
|
||||
}
|
||||
|
||||
db.deleteExpiredEvents()
|
||||
|
||||
@@ -78,11 +81,8 @@ class ExpirationTest : BaseDBTest() {
|
||||
},
|
||||
)
|
||||
|
||||
try {
|
||||
assertFailsWith<SQLiteException> {
|
||||
db.insert(note1)
|
||||
fail("Should not be able to insert expired events")
|
||||
} catch (e: Exception) {
|
||||
assertTrue(e is SQLiteConstraintException)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import org.junit.Test
|
||||
import kotlin.test.Test
|
||||
|
||||
class FilterMatcherTest : BaseDBTest() {
|
||||
val id = "98b574c3527f0ffb30b7271084e3f07480733c7289f8de424d29eae82e36c758"
|
||||
+19
-41
@@ -20,47 +20,38 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteException
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import androidx.sqlite.SQLiteException
|
||||
import com.vitorpamplona.quartz.TestResourceLoader
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.store.sqlite.EventStore
|
||||
import com.vitorpamplona.quartz.nip40Expiration.isExpired
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.util.zip.GZIPInputStream
|
||||
import kotlin.system.measureTimeMillis
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class LargeDBTests {
|
||||
companion object {
|
||||
fun getEventDB(): List<Event> {
|
||||
// This file includes duplicates
|
||||
val fullDBInputStream = javaClass.classLoader?.getResourceAsStream("nostr_vitor_startup_data.json")
|
||||
|
||||
return JacksonMapper.mapper.readValue<ArrayList<Event>>(
|
||||
GZIPInputStream(fullDBInputStream),
|
||||
fun getEventDB(): List<Event> =
|
||||
OptimizedJsonMapper.fromJsonToEventList(
|
||||
TestResourceLoader().loadDecompressString("nostr_vitor_startup_data.json"),
|
||||
)
|
||||
}
|
||||
|
||||
val events = getEventDB().distinctBy { it.id }.filter { !it.isExpired() }.sortedBy { it.createdAt }
|
||||
val events by
|
||||
lazy {
|
||||
getEventDB().distinctBy { it.id }.filter { !it.isExpired() }.sortedBy { it.createdAt }
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var db: EventStore
|
||||
|
||||
@Before
|
||||
@BeforeTest
|
||||
fun setup() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
db = EventStore(context, null)
|
||||
db = EventStore(null)
|
||||
}
|
||||
|
||||
@After
|
||||
@AfterTest
|
||||
fun tearDown() {
|
||||
db.close()
|
||||
}
|
||||
@@ -69,13 +60,7 @@ class LargeDBTests {
|
||||
fun insertHeavyEvent() {
|
||||
events.first { it.id == "3f34b8cb682307ec11753de4669ce8948e95fd6fb360d79136446c5547fd235e" }.let { event ->
|
||||
try {
|
||||
val measure =
|
||||
measureTimeMillis {
|
||||
db.insert(event)
|
||||
}
|
||||
if (measure > 1) {
|
||||
println("Inserted event ${event.id} of kind ${event.kind} in $measure ms")
|
||||
}
|
||||
db.insert(event)
|
||||
} catch (e: SQLiteException) {
|
||||
Log.w("LargeDBTests", "Error inserting event: ${e.message} for event: ${event.toJson()}")
|
||||
}
|
||||
@@ -83,17 +68,10 @@ class LargeDBTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Not testing")
|
||||
fun insertDatabase() {
|
||||
events.forEach { event ->
|
||||
try {
|
||||
val measure =
|
||||
measureTimeMillis {
|
||||
db.insert(event)
|
||||
}
|
||||
if (measure > 1) {
|
||||
println("Inserted event ${event.id} of kind ${event.kind} in $measure ms")
|
||||
}
|
||||
db.insert(event)
|
||||
} catch (e: SQLiteException) {
|
||||
Log.w("LargeDBTests", "Error inserting event: ${e.message} for event: ${event.toJson()}")
|
||||
}
|
||||
+55
-54
@@ -23,32 +23,33 @@ package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
import com.vitorpamplona.quartz.experimental.relationshipStatus.ContactCardEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.store.sqlite.EventStore
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import junit.framework.TestCase
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class QueryAssemblerTest : BaseDBTest() {
|
||||
val hasher = TagNameValueHasher(0)
|
||||
val hasher =
|
||||
_root_ide_package_.com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
.TagNameValueHasher(0)
|
||||
val key1 = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d"
|
||||
val key2 = "f3ac434d61bc0f491a814782ccfdf9c439dae1f0bde9097ad4a245f4c495cd14"
|
||||
val key3 = "12ae0fd81c85e1e7d9ed096397dc3129849425fe6f8afce7213ebf38ddfc6ca9"
|
||||
|
||||
fun EventStore.explain(f: Filter) = store.queryBuilder.planQuery(f, hasher, store.readableDatabase)
|
||||
fun EventStore.explain(f: Filter) = store.queryBuilder.planQuery(f, hasher, store.connection)
|
||||
|
||||
fun EventStore.explain(f: List<Filter>) = store.queryBuilder.planQuery(f, hasher, store.readableDatabase)
|
||||
fun EventStore.explain(f: List<Filter>) = store.queryBuilder.planQuery(f, hasher, store.connection)
|
||||
|
||||
@Test
|
||||
fun testEmpty() =
|
||||
forEachDB { db ->
|
||||
val orderBy = if (db.indexStrategy.useAndIndexIdOnOrderBy) "created_at DESC, id ASC" else "created_at DESC"
|
||||
if (db.indexStrategy.indexEventsByCreatedAtAlone) {
|
||||
Assert.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
ORDER BY $orderBy
|
||||
@@ -57,7 +58,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
db.explain(Filter()),
|
||||
)
|
||||
} else {
|
||||
Assert.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
ORDER BY $orderBy
|
||||
@@ -81,7 +82,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
)
|
||||
val orderBy = if (db.indexStrategy.useAndIndexIdOnOrderBy) "created_at DESC, id ASC" else "created_at DESC"
|
||||
if (db.indexStrategy.indexTagsWithKindAndPubkey) {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
@@ -99,7 +100,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
db.explain(filter),
|
||||
)
|
||||
} else {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
@@ -126,7 +127,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
val orderBy = if (db.indexStrategy.useAndIndexIdOnOrderBy) "created_at DESC, id ASC" else "created_at DESC"
|
||||
|
||||
if (db.indexStrategy.indexEventsByCreatedAtAlone) {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
ORDER BY $orderBy
|
||||
@@ -136,7 +137,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
db.explain(filter),
|
||||
)
|
||||
} else {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
ORDER BY $orderBy
|
||||
@@ -160,7 +161,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
"created_at DESC"
|
||||
}
|
||||
if (db.indexStrategy.indexEventsByCreatedAtAlone) {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
@@ -187,7 +188,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
db.explain(filter),
|
||||
)
|
||||
} else {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
@@ -234,7 +235,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
)
|
||||
val orderBy = if (db.indexStrategy.useAndIndexIdOnOrderBy) "created_at DESC, id ASC" else "created_at DESC"
|
||||
if (db.indexStrategy.indexEventsByCreatedAtAlone) {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
@@ -254,14 +255,15 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
│ │ └── SCAN (subquery-1)
|
||||
│ ├── UNION USING TEMP B-TREE
|
||||
│ │ ├── CO-ROUTINE (subquery-3)
|
||||
│ │ │ ├── SCAN event_fts VIRTUAL TABLE INDEX 4:
|
||||
│ │ │ ├── SCAN event_fts VIRTUAL TABLE INDEX 0:M2
|
||||
│ │ │ ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
│ │ │ └── USE TEMP B-TREE FOR ORDER BY
|
||||
│ │ └── SCAN (subquery-3)
|
||||
│ └── UNION USING TEMP B-TREE
|
||||
│ ├── CO-ROUTINE (subquery-5)
|
||||
│ │ ├── SEARCH event_headers USING COVERING INDEX query_by_kind_created (kind=?)
|
||||
│ │ └── SCAN event_fts VIRTUAL TABLE INDEX 4:
|
||||
│ │ ├── SCAN event_fts VIRTUAL TABLE INDEX 0:M2
|
||||
│ │ ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
│ │ └── USE TEMP B-TREE FOR ORDER BY
|
||||
│ └── SCAN (subquery-5)
|
||||
├── SCAN filtered
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
@@ -270,7 +272,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
db.explain(filter),
|
||||
)
|
||||
} else {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
@@ -291,14 +293,15 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
│ │ └── SCAN (subquery-1)
|
||||
│ ├── UNION USING TEMP B-TREE
|
||||
│ │ ├── CO-ROUTINE (subquery-3)
|
||||
│ │ │ ├── SCAN event_fts VIRTUAL TABLE INDEX 4:
|
||||
│ │ │ ├── SCAN event_fts VIRTUAL TABLE INDEX 0:M2
|
||||
│ │ │ ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
│ │ │ └── USE TEMP B-TREE FOR ORDER BY
|
||||
│ │ └── SCAN (subquery-3)
|
||||
│ └── UNION USING TEMP B-TREE
|
||||
│ ├── CO-ROUTINE (subquery-5)
|
||||
│ │ ├── SEARCH event_headers USING COVERING INDEX query_by_kind_created (kind=?)
|
||||
│ │ └── SCAN event_fts VIRTUAL TABLE INDEX 4:
|
||||
│ │ ├── SCAN event_fts VIRTUAL TABLE INDEX 0:M2
|
||||
│ │ ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
│ │ └── USE TEMP B-TREE FOR ORDER BY
|
||||
│ └── SCAN (subquery-5)
|
||||
├── SCAN filtered
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
@@ -320,7 +323,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
),
|
||||
)
|
||||
if (db.indexStrategy.useAndIndexIdOnOrderBy) {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
WHERE kind = "3"
|
||||
@@ -331,7 +334,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
db.explain(filter),
|
||||
)
|
||||
} else {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
WHERE kind = "3"
|
||||
@@ -360,7 +363,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
)
|
||||
val orderBy = if (db.indexStrategy.useAndIndexIdOnOrderBy) "created_at DESC, id ASC" else "created_at DESC"
|
||||
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
@@ -400,7 +403,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
),
|
||||
)
|
||||
if (db.indexStrategy.useAndIndexIdOnOrderBy) {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
WHERE (kind = "3") AND (d_tag = "")
|
||||
@@ -411,7 +414,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
db.explain(filter),
|
||||
)
|
||||
} else {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
WHERE (kind = "3") AND (d_tag = "")
|
||||
@@ -436,7 +439,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
),
|
||||
)
|
||||
val orderBy = if (db.indexStrategy.useAndIndexIdOnOrderBy) "created_at DESC, id ASC" else "created_at DESC"
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
@@ -467,7 +470,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
)
|
||||
val orderBy = if (db.indexStrategy.useAndIndexIdOnOrderBy) "created_at DESC, id ASC" else "created_at DESC"
|
||||
if (db.indexStrategy.indexTagsByCreatedAtAlone) {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
@@ -485,7 +488,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
db.explain(filter),
|
||||
)
|
||||
} else {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
@@ -518,7 +521,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
),
|
||||
)
|
||||
val orderBy = if (db.indexStrategy.useAndIndexIdOnOrderBy) "created_at DESC, id ASC" else "created_at DESC"
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
@@ -550,7 +553,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
)
|
||||
val orderBy = if (db.indexStrategy.useAndIndexIdOnOrderBy) "created_at DESC, id ASC" else "created_at DESC"
|
||||
if (db.indexStrategy.indexTagsByCreatedAtAlone) {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
@@ -568,7 +571,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
db.explain(filter),
|
||||
)
|
||||
} else {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
@@ -606,7 +609,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
)
|
||||
val orderBy = if (db.indexStrategy.useAndIndexIdOnOrderBy) "created_at DESC, id ASC" else "created_at DESC"
|
||||
if (db.indexStrategy.indexTagsByCreatedAtAlone) {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
@@ -625,7 +628,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
db.explain(filter),
|
||||
)
|
||||
} else {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
@@ -651,7 +654,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
forEachDB { db ->
|
||||
val filter = Filter(ids = listOf(key1))
|
||||
if (db.indexStrategy.useAndIndexIdOnOrderBy) {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
WHERE id = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d"
|
||||
@@ -661,7 +664,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
db.explain(filter),
|
||||
)
|
||||
} else {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
WHERE id = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d"
|
||||
@@ -678,7 +681,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
forEachDB { db ->
|
||||
val filter = Filter(authors = listOf(key1, key2), kinds = listOf(1, 30023), limit = 300)
|
||||
if (db.indexStrategy.useAndIndexIdOnOrderBy) {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
WHERE (kind IN ("1", "30023")) AND (pubkey IN ("7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d", "f3ac434d61bc0f491a814782ccfdf9c439dae1f0bde9097ad4a245f4c495cd14"))
|
||||
@@ -690,7 +693,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
db.explain(filter),
|
||||
)
|
||||
} else {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
WHERE (kind IN ("1", "30023")) AND (pubkey IN ("7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d", "f3ac434d61bc0f491a814782ccfdf9c439dae1f0bde9097ad4a245f4c495cd14"))
|
||||
@@ -709,26 +712,26 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
forEachDB { db ->
|
||||
val filter = Filter(authors = listOf(key1, key2, key3), search = "keywords")
|
||||
if (db.indexStrategy.useAndIndexIdOnOrderBy) {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT event_headers.id, event_headers.pubkey, event_headers.created_at, event_headers.kind, event_headers.tags, event_headers.content, event_headers.sig FROM event_headers
|
||||
INNER JOIN event_fts ON event_headers.row_id = event_fts.event_header_row_id
|
||||
WHERE (event_fts MATCH "keywords") AND (event_headers.pubkey IN ("7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d", "f3ac434d61bc0f491a814782ccfdf9c439dae1f0bde9097ad4a245f4c495cd14", "12ae0fd81c85e1e7d9ed096397dc3129849425fe6f8afce7213ebf38ddfc6ca9"))
|
||||
ORDER BY event_headers.created_at DESC, event_headers.id ASC
|
||||
├── SCAN event_fts VIRTUAL TABLE INDEX 4:
|
||||
├── SCAN event_fts VIRTUAL TABLE INDEX 0:M2
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── USE TEMP B-TREE FOR ORDER BY
|
||||
""".trimIndent(),
|
||||
db.explain(filter),
|
||||
)
|
||||
} else {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT event_headers.id, event_headers.pubkey, event_headers.created_at, event_headers.kind, event_headers.tags, event_headers.content, event_headers.sig FROM event_headers
|
||||
INNER JOIN event_fts ON event_headers.row_id = event_fts.event_header_row_id
|
||||
WHERE (event_fts MATCH "keywords") AND (event_headers.pubkey IN ("7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d", "f3ac434d61bc0f491a814782ccfdf9c439dae1f0bde9097ad4a245f4c495cd14", "12ae0fd81c85e1e7d9ed096397dc3129849425fe6f8afce7213ebf38ddfc6ca9"))
|
||||
ORDER BY event_headers.created_at DESC
|
||||
├── SCAN event_fts VIRTUAL TABLE INDEX 4:
|
||||
├── SCAN event_fts VIRTUAL TABLE INDEX 0:M2
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── USE TEMP B-TREE FOR ORDER BY
|
||||
""".trimIndent(),
|
||||
@@ -742,13 +745,13 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
forEachDB { db ->
|
||||
val filter = Filter(kinds = listOf(1, 1111, 10000), search = "keywords")
|
||||
val orderBy = if (db.indexStrategy.useAndIndexIdOnOrderBy) "event_headers.created_at DESC, event_headers.id ASC" else "event_headers.created_at DESC"
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT event_headers.id, event_headers.pubkey, event_headers.created_at, event_headers.kind, event_headers.tags, event_headers.content, event_headers.sig FROM event_headers
|
||||
INNER JOIN event_fts ON event_headers.row_id = event_fts.event_header_row_id
|
||||
WHERE (event_fts MATCH "keywords") AND (event_headers.kind IN ("1", "1111", "10000"))
|
||||
ORDER BY $orderBy
|
||||
├── SCAN event_fts VIRTUAL TABLE INDEX 4:
|
||||
├── SCAN event_fts VIRTUAL TABLE INDEX 0:M2
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── USE TEMP B-TREE FOR ORDER BY
|
||||
""".trimIndent(),
|
||||
@@ -762,7 +765,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
val filter = Filter(tagsAll = mapOf("p" to listOf(key1, key2)))
|
||||
val orderBy = if (db.indexStrategy.useAndIndexIdOnOrderBy) "created_at DESC, id ASC" else "created_at DESC"
|
||||
if (db.indexStrategy.indexTagsByCreatedAtAlone) {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
@@ -771,8 +774,8 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
ON event_headers.row_id = filtered.row_id
|
||||
ORDER BY $orderBy
|
||||
├── CO-ROUTINE filtered
|
||||
│ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=?)
|
||||
│ ├── SEARCH event_tagsAll0_1 USING INDEX query_by_tags_hash (tag_hash=? AND created_at=?)
|
||||
│ ├── SEARCH event_tagsAll0_1 USING INDEX query_by_tags_hash_kind (tag_hash=?)
|
||||
│ ├── SEARCH event_tags USING INDEX fk_event_tags_header_id (event_header_row_id=?)
|
||||
│ └── USE TEMP B-TREE FOR DISTINCT
|
||||
├── SCAN filtered
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
@@ -781,7 +784,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
db.explain(filter),
|
||||
)
|
||||
} else {
|
||||
TestCase.assertEquals(
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
@@ -999,8 +1002,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
WHERE (kind = "30382") AND (pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (d_tag = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (created_at >= "1764553447") AND ((kind >= 30000 AND kind < 40000))
|
||||
ORDER BY created_at DESC, id ASC
|
||||
├── SEARCH event_headers USING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?)
|
||||
└── USE TEMP B-TREE FOR ORDER BY
|
||||
└── SEARCH event_headers USING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?)
|
||||
""".trimIndent(),
|
||||
db.explain(filter),
|
||||
)
|
||||
@@ -1010,8 +1012,7 @@ class QueryAssemblerTest : BaseDBTest() {
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
WHERE (kind = "30382") AND (pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (d_tag = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (created_at >= "1764553447") AND ((kind >= 30000 AND kind < 40000))
|
||||
ORDER BY created_at DESC
|
||||
├── SEARCH event_headers USING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?)
|
||||
└── USE TEMP B-TREE FOR ORDER BY
|
||||
└── SEARCH event_headers USING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?)
|
||||
""".trimIndent(),
|
||||
db.explain(filter),
|
||||
)
|
||||
+7
-13
@@ -20,15 +20,15 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
import androidx.sqlite.SQLiteException
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.nip01Core.store.sqlite.explainQuery
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import junit.framework.TestCase
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.fail
|
||||
import org.junit.Test
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class ReplaceableTest : BaseDBTest() {
|
||||
val signer = NostrSignerSync()
|
||||
@@ -76,18 +76,12 @@ class ReplaceableTest : BaseDBTest() {
|
||||
|
||||
db.assertQuery(version3, Filter(ids = listOf(version3.id)))
|
||||
|
||||
try {
|
||||
assertFailsWith<SQLiteException> {
|
||||
db.insert(version2)
|
||||
fail("It should not allow inserting an older version")
|
||||
} catch (e: Exception) {
|
||||
TestCase.assertTrue(e is SQLiteConstraintException)
|
||||
}
|
||||
|
||||
try {
|
||||
assertFailsWith<SQLiteException> {
|
||||
db.insert(version1)
|
||||
fail("It should not allow inserting an older version")
|
||||
} catch (e: Exception) {
|
||||
TestCase.assertTrue(e is SQLiteConstraintException)
|
||||
}
|
||||
|
||||
db.assertQuery(version3, Filter(ids = listOf(version3.id)))
|
||||
+5
-13
@@ -20,16 +20,15 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
import androidx.sqlite.SQLiteException
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import junit.framework.TestCase.fail
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class RightToVanishTest : BaseDBTest() {
|
||||
val signer = NostrSignerSync()
|
||||
@@ -59,12 +58,8 @@ class RightToVanishTest : BaseDBTest() {
|
||||
db.assertQuery(null, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(note3, Filter(ids = listOf(note3.id)))
|
||||
|
||||
// trying to insert again should fail.
|
||||
try {
|
||||
assertFailsWith<SQLiteException> {
|
||||
db.insert(note1)
|
||||
fail("Should not be able to insert a deleted event")
|
||||
} catch (e: SQLiteConstraintException) {
|
||||
assertEquals("blocked: a request to vanish event exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message)
|
||||
}
|
||||
|
||||
db.assertQuery(vanish, Filter(ids = listOf(vanish.id)))
|
||||
@@ -107,11 +102,8 @@ class RightToVanishTest : BaseDBTest() {
|
||||
db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id)))
|
||||
|
||||
// trying to insert again should fail.
|
||||
try {
|
||||
assertFailsWith<SQLiteException> {
|
||||
db.insert(wrap1)
|
||||
fail("Should not be able to insert a deleted event")
|
||||
} catch (e: SQLiteConstraintException) {
|
||||
assertEquals("blocked: a request to vanish event exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message)
|
||||
}
|
||||
|
||||
db.assertQuery(vanish, Filter(ids = listOf(vanish.id)))
|
||||
+1
-1
@@ -24,7 +24,7 @@ import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import org.junit.Test
|
||||
import kotlin.test.Test
|
||||
|
||||
class SearchTest : BaseDBTest() {
|
||||
companion object {
|
||||
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"english": [
|
||||
[
|
||||
"00000000000000000000000000000000",
|
||||
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||
"c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04"
|
||||
],
|
||||
[
|
||||
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
|
||||
"legal winner thank year wave sausage worth useful legal winner thank yellow",
|
||||
"2e8905819b8723fe2c1d161860e5ee1830318dbf49a83bd451cfb8440c28bd6fa457fe1296106559a3c80937a1c1069be3a3a5bd381ee6260e8d9739fce1f607"
|
||||
],
|
||||
[
|
||||
"80808080808080808080808080808080",
|
||||
"letter advice cage absurd amount doctor acoustic avoid letter advice cage above",
|
||||
"d71de856f81a8acc65e6fc851a38d4d7ec216fd0796d0a6827a3ad6ed5511a30fa280f12eb2e47ed2ac03b5c462a0358d18d69fe4f985ec81778c1b370b652a8"
|
||||
],
|
||||
[
|
||||
"ffffffffffffffffffffffffffffffff",
|
||||
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong",
|
||||
"ac27495480225222079d7be181583751e86f571027b0497b5b5d11218e0a8a13332572917f0f8e5a589620c6f15b11c61dee327651a14c34e18231052e48c069"
|
||||
],
|
||||
[
|
||||
"000000000000000000000000000000000000000000000000",
|
||||
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent",
|
||||
"035895f2f481b1b0f01fcf8c289c794660b289981a78f8106447707fdd9666ca06da5a9a565181599b79f53b844d8a71dd9f439c52a3d7b3e8a79c906ac845fa"
|
||||
],
|
||||
[
|
||||
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
|
||||
"legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will",
|
||||
"f2b94508732bcbacbcc020faefecfc89feafa6649a5491b8c952cede496c214a0c7b3c392d168748f2d4a612bada0753b52a1c7ac53c1e93abd5c6320b9e95dd"
|
||||
],
|
||||
[
|
||||
"808080808080808080808080808080808080808080808080",
|
||||
"letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter always",
|
||||
"107d7c02a5aa6f38c58083ff74f04c607c2d2c0ecc55501dadd72d025b751bc27fe913ffb796f841c49b1d33b610cf0e91d3aa239027f5e99fe4ce9e5088cd65"
|
||||
],
|
||||
[
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo when",
|
||||
"0cd6e5d827bb62eb8fc1e262254223817fd068a74b5b449cc2f667c3f1f985a76379b43348d952e2265b4cd129090758b3e3c2c49103b5051aac2eaeb890a528"
|
||||
],
|
||||
[
|
||||
"0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
|
||||
"bda85446c68413707090a52022edd26a1c9462295029f2e60cd7c4f2bbd3097170af7a4d73245cafa9c3cca8d561a7c3de6f5d4a10be8ed2a5e608d68f92fcc8"
|
||||
],
|
||||
[
|
||||
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
|
||||
"legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title",
|
||||
"bc09fca1804f7e69da93c2f2028eb238c227f2e9dda30cd63699232578480a4021b146ad717fbb7e451ce9eb835f43620bf5c514db0f8add49f5d121449d3e87"
|
||||
],
|
||||
[
|
||||
"8080808080808080808080808080808080808080808080808080808080808080",
|
||||
"letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless",
|
||||
"c0c519bd0e91a2ed54357d9d1ebef6f5af218a153624cf4f2da911a0ed8f7a09e2ef61af0aca007096df430022f7a2b6fb91661a9589097069720d015e4e982f"
|
||||
],
|
||||
[
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote",
|
||||
"dd48c104698c30cfe2b6142103248622fb7bb0ff692eebb00089b32d22484e1613912f0a5b694407be899ffd31ed3992c456cdf60f5d4564b8ba3f05a69890ad"
|
||||
],
|
||||
[
|
||||
"77c2b00716cec7213839159e404db50d",
|
||||
"jelly better achieve collect unaware mountain thought cargo oxygen act hood bridge",
|
||||
"b5b6d0127db1a9d2226af0c3346031d77af31e918dba64287a1b44b8ebf63cdd52676f672a290aae502472cf2d602c051f3e6f18055e84e4c43897fc4e51a6ff"
|
||||
],
|
||||
[
|
||||
"b63a9c59a6e641f288ebc103017f1da9f8290b3da6bdef7b",
|
||||
"renew stay biology evidence goat welcome casual join adapt armor shuffle fault little machine walk stumble urge swap",
|
||||
"9248d83e06f4cd98debf5b6f010542760df925ce46cf38a1bdb4e4de7d21f5c39366941c69e1bdbf2966e0f6e6dbece898a0e2f0a4c2b3e640953dfe8b7bbdc5"
|
||||
],
|
||||
[
|
||||
"3e141609b97933b66a060dcddc71fad1d91677db872031e85f4c015c5e7e8982",
|
||||
"dignity pass list indicate nasty swamp pool script soccer toe leaf photo multiply desk host tomato cradle drill spread actor shine dismiss champion exotic",
|
||||
"ff7f3184df8696d8bef94b6c03114dbee0ef89ff938712301d27ed8336ca89ef9635da20af07d4175f2bf5f3de130f39c9d9e8dd0472489c19b1a020a940da67"
|
||||
],
|
||||
[
|
||||
"0460ef47585604c5660618db2e6a7e7f",
|
||||
"afford alter spike radar gate glance object seek swamp infant panel yellow",
|
||||
"65f93a9f36b6c85cbe634ffc1f99f2b82cbb10b31edc7f087b4f6cb9e976e9faf76ff41f8f27c99afdf38f7a303ba1136ee48a4c1e7fcd3dba7aa876113a36e4"
|
||||
],
|
||||
[
|
||||
"72f60ebac5dd8add8d2a25a797102c3ce21bc029c200076f",
|
||||
"indicate race push merry suffer human cruise dwarf pole review arch keep canvas theme poem divorce alter left",
|
||||
"3bbf9daa0dfad8229786ace5ddb4e00fa98a044ae4c4975ffd5e094dba9e0bb289349dbe2091761f30f382d4e35c4a670ee8ab50758d2c55881be69e327117ba"
|
||||
],
|
||||
[
|
||||
"2c85efc7f24ee4573d2b81a6ec66cee209b2dcbd09d8eddc51e0215b0b68e416",
|
||||
"clutch control vehicle tonight unusual clog visa ice plunge glimpse recipe series open hour vintage deposit universe tip job dress radar refuse motion taste",
|
||||
"fe908f96f46668b2d5b37d82f558c77ed0d69dd0e7e043a5b0511c48c2f1064694a956f86360c93dd04052a8899497ce9e985ebe0c8c52b955e6ae86d4ff4449"
|
||||
],
|
||||
[
|
||||
"eaebabb2383351fd31d703840b32e9e2",
|
||||
"turtle front uncle idea crush write shrug there lottery flower risk shell",
|
||||
"bdfb76a0759f301b0b899a1e3985227e53b3f51e67e3f2a65363caedf3e32fde42a66c404f18d7b05818c95ef3ca1e5146646856c461c073169467511680876c"
|
||||
],
|
||||
[
|
||||
"7ac45cfe7722ee6c7ba84fbc2d5bd61b45cb2fe5eb65aa78",
|
||||
"kiss carry display unusual confirm curtain upgrade antique rotate hello void custom frequent obey nut hole price segment",
|
||||
"ed56ff6c833c07982eb7119a8f48fd363c4a9b1601cd2de736b01045c5eb8ab4f57b079403485d1c4924f0790dc10a971763337cb9f9c62226f64fff26397c79"
|
||||
],
|
||||
[
|
||||
"4fa1a8bc3e6d80ee1316050e862c1812031493212b7ec3f3bb1b08f168cabeef",
|
||||
"exile ask congress lamp submit jacket era scheme attend cousin alcohol catch course end lucky hurt sentence oven short ball bird grab wing top",
|
||||
"095ee6f817b4c2cb30a5a797360a81a40ab0f9a4e25ecd672a3f58a0b5ba0687c096a6b14d2c0deb3bdefce4f61d01ae07417d502429352e27695163f7447a8c"
|
||||
],
|
||||
[
|
||||
"18ab19a9f54a9274f03e5209a2ac8a91",
|
||||
"board flee heavy tunnel powder denial science ski answer betray cargo cat",
|
||||
"6eff1bb21562918509c73cb990260db07c0ce34ff0e3cc4a8cb3276129fbcb300bddfe005831350efd633909f476c45c88253276d9fd0df6ef48609e8bb7dca8"
|
||||
],
|
||||
[
|
||||
"18a2e1d81b8ecfb2a333adcb0c17a5b9eb76cc5d05db91a4",
|
||||
"board blade invite damage undo sun mimic interest slam gaze truly inherit resist great inject rocket museum chief",
|
||||
"f84521c777a13b61564234bf8f8b62b3afce27fc4062b51bb5e62bdfecb23864ee6ecf07c1d5a97c0834307c5c852d8ceb88e7c97923c0a3b496bedd4e5f88a9"
|
||||
],
|
||||
[
|
||||
"15da872c95a13dd738fbf50e427583ad61f18fd99f628c417a61cf8343c90419",
|
||||
"beyond stage sleep clip because twist token leaf atom beauty genius food business side grid unable middle armed observe pair crouch tonight away coconut",
|
||||
"b15509eaa2d09d3efd3e006ef42151b30367dc6e3aa5e44caba3fe4d3e352e65101fbdb86a96776b91946ff06f8eac594dc6ee1d3e82a42dfe1b40fef6bcc3fd"
|
||||
]
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz
|
||||
|
||||
import com.vitorpamplona.quartz.utils.GZip
|
||||
import dev.whyoleg.cryptography.CryptographyProviderApi
|
||||
import dev.whyoleg.cryptography.providers.base.toByteArray
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
@@ -33,6 +34,11 @@ import platform.Foundation.stringWithContentsOfFile
|
||||
import platform.posix.getenv
|
||||
|
||||
actual class TestResourceLoader {
|
||||
actual fun loadDecompressString(file: String): String {
|
||||
val data = loadFileData(file)
|
||||
return GZip.decompress(data)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
actual fun loadString(file: String): String {
|
||||
val resourceDir = getenv("TEST_RESOURCES_ROOT")?.toKString()
|
||||
|
||||
@@ -20,7 +20,17 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz
|
||||
|
||||
import java.util.zip.GZIPInputStream
|
||||
|
||||
actual class TestResourceLoader {
|
||||
actual fun loadDecompressString(file: String): String =
|
||||
this@TestResourceLoader
|
||||
.javaClass.classLoader
|
||||
?.getResourceAsStream(file)
|
||||
?.let { GZIPInputStream(it) }
|
||||
?.bufferedReader()
|
||||
?.use { it.readText() } ?: throw IllegalArgumentException("Resource not found: $file")
|
||||
|
||||
actual fun loadString(file: String): String =
|
||||
this@TestResourceLoader
|
||||
.javaClass.classLoader
|
||||
|
||||
Reference in New Issue
Block a user