Adds more settings to pick different ways to build the SQL database
This commit is contained in:
+76
-90
@@ -20,9 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||
@@ -30,91 +28,91 @@ import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import junit.framework.TestCase
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.fail
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class AddressableTest {
|
||||
private lateinit var db: EventStore
|
||||
|
||||
class AddressableTest : BaseDBTest() {
|
||||
val signer = NostrSignerSync()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
db = EventStore(context, null)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testReplacingAddressables() {
|
||||
val time = TimeUtils.now()
|
||||
val version1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time))
|
||||
val version2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1))
|
||||
val version3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2))
|
||||
fun testReplacingAddressables() =
|
||||
forEachDB { db ->
|
||||
val time = TimeUtils.now()
|
||||
val version1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time))
|
||||
val version2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1))
|
||||
val version3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2))
|
||||
|
||||
val addressableQuery = Filter(kinds = listOf(version1.kind), authors = listOf(version1.pubKey), tags = mapOf("d" to listOf(version1.dTag())))
|
||||
val addressableQuery = Filter(kinds = listOf(version1.kind), authors = listOf(version1.pubKey), tags = mapOf("d" to listOf(version1.dTag())))
|
||||
|
||||
db.insert(version1)
|
||||
|
||||
db.assertQuery(version1, Filter(ids = listOf(version1.id)))
|
||||
db.assertQuery(version1, addressableQuery)
|
||||
|
||||
db.insert(version2)
|
||||
|
||||
db.assertQuery(null, Filter(ids = listOf(version1.id)))
|
||||
db.assertQuery(version2, Filter(ids = listOf(version2.id)))
|
||||
db.assertQuery(version2, addressableQuery)
|
||||
|
||||
db.insert(version3)
|
||||
|
||||
db.assertQuery(null, Filter(ids = listOf(version1.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(version2.id)))
|
||||
db.assertQuery(version3, Filter(ids = listOf(version3.id)))
|
||||
db.assertQuery(version3, addressableQuery)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBlockingOldAddressables() {
|
||||
val time = TimeUtils.now()
|
||||
val version1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time))
|
||||
val version2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1))
|
||||
val version3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2))
|
||||
|
||||
val addressableQuery = Filter(kinds = listOf(version1.kind), authors = listOf(version1.pubKey), tags = mapOf("d" to listOf(version1.dTag())))
|
||||
|
||||
db.insert(version3)
|
||||
|
||||
db.assertQuery(version3, Filter(ids = listOf(version3.id)))
|
||||
|
||||
try {
|
||||
db.insert(version2)
|
||||
fail("It should not allow inserting an older version")
|
||||
} catch (e: Exception) {
|
||||
TestCase.assertTrue(e is SQLiteConstraintException)
|
||||
}
|
||||
|
||||
try {
|
||||
db.insert(version1)
|
||||
fail("It should not allow inserting an older version")
|
||||
} catch (e: Exception) {
|
||||
TestCase.assertTrue(e is SQLiteConstraintException)
|
||||
|
||||
db.assertQuery(version1, Filter(ids = listOf(version1.id)))
|
||||
db.assertQuery(version1, addressableQuery)
|
||||
|
||||
db.insert(version2)
|
||||
|
||||
db.assertQuery(null, Filter(ids = listOf(version1.id)))
|
||||
db.assertQuery(version2, Filter(ids = listOf(version2.id)))
|
||||
db.assertQuery(version2, addressableQuery)
|
||||
|
||||
db.insert(version3)
|
||||
|
||||
db.assertQuery(null, Filter(ids = listOf(version1.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(version2.id)))
|
||||
db.assertQuery(version3, Filter(ids = listOf(version3.id)))
|
||||
db.assertQuery(version3, addressableQuery)
|
||||
}
|
||||
|
||||
db.assertQuery(version3, Filter(ids = listOf(version3.id)))
|
||||
db.assertQuery(version3, addressableQuery)
|
||||
db.assertQuery(null, Filter(ids = listOf(version2.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(version1.id)))
|
||||
}
|
||||
@Test
|
||||
fun testBlockingOldAddressables() =
|
||||
forEachDB { db ->
|
||||
val time = TimeUtils.now()
|
||||
val version1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time))
|
||||
val version2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1))
|
||||
val version3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2))
|
||||
|
||||
val addressableQuery = Filter(kinds = listOf(version1.kind), authors = listOf(version1.pubKey), tags = mapOf("d" to listOf(version1.dTag())))
|
||||
|
||||
db.insert(version3)
|
||||
|
||||
db.assertQuery(version3, Filter(ids = listOf(version3.id)))
|
||||
|
||||
try {
|
||||
db.insert(version2)
|
||||
fail("It should not allow inserting an older version")
|
||||
} catch (e: Exception) {
|
||||
TestCase.assertTrue(e is SQLiteConstraintException)
|
||||
}
|
||||
|
||||
try {
|
||||
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)))
|
||||
db.assertQuery(version3, addressableQuery)
|
||||
db.assertQuery(null, Filter(ids = listOf(version2.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(version1.id)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTriggersIndexUsage() {
|
||||
val explainer =
|
||||
db.store.explainQuery(
|
||||
fun testTriggersIndexUsage() =
|
||||
forEachDB { db ->
|
||||
val explainer =
|
||||
db.store.explainQuery(
|
||||
"""
|
||||
SELECT * FROM event_headers
|
||||
WHERE
|
||||
event_headers.kind = 30000 AND
|
||||
event_headers.pubkey = 'aa' AND
|
||||
event_headers.d_tag = 'test-tag' AND
|
||||
event_headers.created_at < 1766686500 AND
|
||||
event_headers.kind >= 30000 AND event_headers.kind < 40000
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT * FROM event_headers
|
||||
WHERE
|
||||
@@ -123,21 +121,9 @@ class AddressableTest {
|
||||
event_headers.d_tag = 'test-tag' AND
|
||||
event_headers.created_at < 1766686500 AND
|
||||
event_headers.kind >= 30000 AND event_headers.kind < 40000
|
||||
└── SEARCH event_headers USING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?)
|
||||
""".trimIndent(),
|
||||
explainer,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT * FROM event_headers
|
||||
WHERE
|
||||
event_headers.kind = 30000 AND
|
||||
event_headers.pubkey = 'aa' AND
|
||||
event_headers.d_tag = 'test-tag' AND
|
||||
event_headers.created_at < 1766686500 AND
|
||||
event_headers.kind >= 30000 AND event_headers.kind < 40000
|
||||
└── SEARCH event_headers USING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?)
|
||||
""".trimIndent(),
|
||||
explainer,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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 android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
|
||||
open class BaseDBTest {
|
||||
private lateinit var dbs: Map<String, EventStore>
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
|
||||
dbs =
|
||||
mapOf(
|
||||
"Default" to EventStore(context, null, indexStrategy = DefaultIndexingStrategy(false, false)),
|
||||
"IndexAll" to EventStore(context, null, indexStrategy = DefaultIndexingStrategy(true, false)),
|
||||
"Order by ID" to EventStore(context, null, indexStrategy = DefaultIndexingStrategy(false, true)),
|
||||
"IndexAll, Order by ID" to EventStore(context, null, indexStrategy = DefaultIndexingStrategy(true, true)),
|
||||
)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
dbs.forEach { it.value.close() }
|
||||
}
|
||||
|
||||
fun forEachDB(action: (EventStore) -> Unit) {
|
||||
dbs.forEach {
|
||||
println("--------------------")
|
||||
println(it.key)
|
||||
println("--------------------")
|
||||
action(it.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
+150
-158
@@ -20,8 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
@@ -30,18 +28,14 @@ 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.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class BasicTest {
|
||||
private lateinit var db: SQLiteEventStore
|
||||
|
||||
class BasicTest : BaseDBTest() {
|
||||
val signer = NostrSignerSync()
|
||||
|
||||
companion object Companion {
|
||||
companion object {
|
||||
val profile =
|
||||
MetadataEvent(
|
||||
id = "490d7439e530423f2540d4f2bdb73a0a2935f3df9e1f2a6f699a140c7db311fe",
|
||||
@@ -82,168 +76,166 @@ class BasicTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
db = SQLiteEventStore(context, null)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInsertDeleteEvent() {
|
||||
val note = signer.sign(TextNoteEvent.build("test1"))
|
||||
fun testInsertDeleteEvent() =
|
||||
forEachDB { db ->
|
||||
val note = signer.sign(TextNoteEvent.build("test1"))
|
||||
|
||||
db.insertEvent(note)
|
||||
db.store.insertEvent(note)
|
||||
|
||||
db.assertQuery(note, Filter(ids = listOf(note.id)))
|
||||
db.store.assertQuery(note, Filter(ids = listOf(note.id)))
|
||||
|
||||
db.delete(note.id)
|
||||
db.store.delete(note.id)
|
||||
|
||||
db.assertQuery(null, Filter(ids = listOf(note.id)))
|
||||
db.store.assertQuery(null, Filter(ids = listOf(note.id)))
|
||||
|
||||
db.insertEvent(note)
|
||||
db.store.insertEvent(note)
|
||||
|
||||
db.assertQuery(note, Filter(ids = listOf(note.id)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEmptyFilter() {
|
||||
val note1 = signer.sign(TextNoteEvent.build("test1", createdAt = 1))
|
||||
val note2 = signer.sign(TextNoteEvent.build("test2", createdAt = 2))
|
||||
|
||||
db.insertEvent(note1)
|
||||
|
||||
db.assertQuery(note1, Filter())
|
||||
|
||||
db.insertEvent(note2)
|
||||
|
||||
db.assertQuery(listOf(note2, note1), Filter())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLimitFilter() {
|
||||
val note1 = signer.sign(TextNoteEvent.build("test1", createdAt = 1))
|
||||
val note2 = signer.sign(TextNoteEvent.build("test2", createdAt = 2))
|
||||
val note3 = signer.sign(TextNoteEvent.build("test3", createdAt = 3))
|
||||
val note4 = signer.sign(TextNoteEvent.build("test4", createdAt = 4))
|
||||
|
||||
db.insertEvent(note1)
|
||||
|
||||
db.assertQuery(note1, Filter(limit = 1))
|
||||
|
||||
db.insertEvent(note2)
|
||||
db.insertEvent(note3)
|
||||
db.insertEvent(note4)
|
||||
|
||||
db.assertQuery(listOf(note4), Filter(limit = 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPubkeyTag() {
|
||||
db.insertEvent(comment)
|
||||
db.insertEvent(profile)
|
||||
|
||||
db.assertQuery(
|
||||
comment,
|
||||
Filter(authors = listOf(comment.pubKey), tags = mapOf("I" to listOf("geo:drt3n"))),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTagOnly() {
|
||||
db.insertEvent(comment)
|
||||
db.insertEvent(profile)
|
||||
|
||||
db.assertQuery(comment, Filter(tags = mapOf("I" to listOf("geo:drt3n"))))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTagWithSinceOnly() {
|
||||
db.insertEvent(comment)
|
||||
db.insertEvent(profile)
|
||||
|
||||
db.assertQuery(
|
||||
comment,
|
||||
Filter(tags = mapOf("I" to listOf("geo:drt3n")), since = comment.createdAt - 1),
|
||||
)
|
||||
db.assertQuery(
|
||||
comment,
|
||||
Filter(tags = mapOf("I" to listOf("geo:drt3n")), since = comment.createdAt),
|
||||
)
|
||||
db.assertQuery(
|
||||
null,
|
||||
Filter(tags = mapOf("I" to listOf("geo:drt3n")), since = comment.createdAt + 1),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTagWithUntilOnly() {
|
||||
db.insertEvent(comment)
|
||||
db.insertEvent(profile)
|
||||
|
||||
db.assertQuery(
|
||||
null,
|
||||
Filter(tags = mapOf("I" to listOf("geo:drt3n")), until = comment.createdAt - 1),
|
||||
)
|
||||
db.assertQuery(
|
||||
comment,
|
||||
Filter(tags = mapOf("I" to listOf("geo:drt3n")), until = comment.createdAt),
|
||||
)
|
||||
db.assertQuery(
|
||||
comment,
|
||||
Filter(tags = mapOf("I" to listOf("geo:drt3n")), until = comment.createdAt + 1),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTagWithUntilOnlyEmitting() {
|
||||
db.insertEvent(comment)
|
||||
db.insertEvent(profile)
|
||||
|
||||
db.query<Event>(Filter(tags = mapOf("I" to listOf("geo:drt3n")))) { event ->
|
||||
assertEquals(comment.toJson(), event.toJson())
|
||||
db.store.assertQuery(note, Filter(ids = listOf(note.id)))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hashCodeTest() {
|
||||
val note1 =
|
||||
signer.sign(
|
||||
TextNoteEvent.build("test1") {
|
||||
hashtag("AaAa")
|
||||
},
|
||||
)
|
||||
val note2 =
|
||||
signer.sign(
|
||||
TextNoteEvent.build("test2") {
|
||||
hashtag("AaAa")
|
||||
},
|
||||
)
|
||||
val note3 =
|
||||
signer.sign(
|
||||
TextNoteEvent.build("test3") {
|
||||
hashtag("BBBB")
|
||||
},
|
||||
)
|
||||
fun testEmptyFilter() =
|
||||
forEachDB { db ->
|
||||
val note1 = signer.sign(TextNoteEvent.build("test1", createdAt = 1))
|
||||
val note2 = signer.sign(TextNoteEvent.build("test2", createdAt = 2))
|
||||
|
||||
db.insertEvent(note1)
|
||||
db.insertEvent(note2)
|
||||
db.insertEvent(note3)
|
||||
db.store.insertEvent(note1)
|
||||
|
||||
val list =
|
||||
db.query<TextNoteEvent>(
|
||||
Filter(
|
||||
tags = mapOf("t" to listOf("AaAa")),
|
||||
),
|
||||
)
|
||||
db.store.assertQuery(note1, Filter())
|
||||
|
||||
assertEquals(2, list.size)
|
||||
list.forEach {
|
||||
assertTrue(it.isTaggedHash("AaAa"))
|
||||
db.store.insertEvent(note2)
|
||||
|
||||
db.store.assertQuery(listOf(note2, note1), Filter())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLimitFilter() =
|
||||
forEachDB { db ->
|
||||
val note1 = signer.sign(TextNoteEvent.build("test1", createdAt = 1))
|
||||
val note2 = signer.sign(TextNoteEvent.build("test2", createdAt = 2))
|
||||
val note3 = signer.sign(TextNoteEvent.build("test3", createdAt = 3))
|
||||
val note4 = signer.sign(TextNoteEvent.build("test4", createdAt = 4))
|
||||
|
||||
db.store.insertEvent(note1)
|
||||
|
||||
db.store.assertQuery(note1, Filter(limit = 1))
|
||||
|
||||
db.store.insertEvent(note2)
|
||||
db.store.insertEvent(note3)
|
||||
db.store.insertEvent(note4)
|
||||
|
||||
db.store.assertQuery(listOf(note4), Filter(limit = 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPubkeyTag() =
|
||||
forEachDB { db ->
|
||||
db.store.insertEvent(comment)
|
||||
db.store.insertEvent(profile)
|
||||
|
||||
db.store.assertQuery(
|
||||
comment,
|
||||
Filter(authors = listOf(comment.pubKey), tags = mapOf("I" to listOf("geo:drt3n"))),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTagOnly() =
|
||||
forEachDB { db ->
|
||||
db.store.insertEvent(comment)
|
||||
db.store.insertEvent(profile)
|
||||
|
||||
db.store.assertQuery(comment, Filter(tags = mapOf("I" to listOf("geo:drt3n"))))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTagWithSinceOnly() =
|
||||
forEachDB { db ->
|
||||
db.store.insertEvent(comment)
|
||||
db.store.insertEvent(profile)
|
||||
|
||||
db.store.assertQuery(
|
||||
comment,
|
||||
Filter(tags = mapOf("I" to listOf("geo:drt3n")), since = comment.createdAt - 1),
|
||||
)
|
||||
db.store.assertQuery(
|
||||
comment,
|
||||
Filter(tags = mapOf("I" to listOf("geo:drt3n")), since = comment.createdAt),
|
||||
)
|
||||
db.store.assertQuery(
|
||||
null,
|
||||
Filter(tags = mapOf("I" to listOf("geo:drt3n")), since = comment.createdAt + 1),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTagWithUntilOnly() =
|
||||
forEachDB { db ->
|
||||
db.store.insertEvent(comment)
|
||||
db.store.insertEvent(profile)
|
||||
|
||||
db.store.assertQuery(
|
||||
null,
|
||||
Filter(tags = mapOf("I" to listOf("geo:drt3n")), until = comment.createdAt - 1),
|
||||
)
|
||||
db.store.assertQuery(
|
||||
comment,
|
||||
Filter(tags = mapOf("I" to listOf("geo:drt3n")), until = comment.createdAt),
|
||||
)
|
||||
db.store.assertQuery(
|
||||
comment,
|
||||
Filter(tags = mapOf("I" to listOf("geo:drt3n")), until = comment.createdAt + 1),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTagWithUntilOnlyEmitting() =
|
||||
forEachDB { db ->
|
||||
db.store.insertEvent(comment)
|
||||
db.store.insertEvent(profile)
|
||||
|
||||
db.store.query<Event>(Filter(tags = mapOf("I" to listOf("geo:drt3n")))) { event ->
|
||||
assertEquals(comment.toJson(), event.toJson())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hashCodeTest() =
|
||||
forEachDB { db ->
|
||||
val note1 =
|
||||
signer.sign(
|
||||
TextNoteEvent.build("test1") {
|
||||
hashtag("AaAa")
|
||||
},
|
||||
)
|
||||
val note2 =
|
||||
signer.sign(
|
||||
TextNoteEvent.build("test2") {
|
||||
hashtag("AaAa")
|
||||
},
|
||||
)
|
||||
val note3 =
|
||||
signer.sign(
|
||||
TextNoteEvent.build("test3") {
|
||||
hashtag("BBBB")
|
||||
},
|
||||
)
|
||||
|
||||
db.store.insertEvent(note1)
|
||||
db.store.insertEvent(note2)
|
||||
db.store.insertEvent(note3)
|
||||
|
||||
val list =
|
||||
db.query<TextNoteEvent>(
|
||||
Filter(
|
||||
tags = mapOf("t" to listOf("AaAa")),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(2, list.size)
|
||||
list.forEach {
|
||||
assertTrue(it.isTaggedHash("AaAa"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+327
-337
@@ -20,9 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
@@ -33,380 +31,372 @@ 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.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class DeletionTest {
|
||||
private lateinit var db: EventStore
|
||||
|
||||
class DeletionTest : BaseDBTest() {
|
||||
val signer = NostrSignerSync()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
db = EventStore(context, null)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInsertDeleteEvent() {
|
||||
val note1 = signer.sign(TextNoteEvent.build("test1"))
|
||||
val note2 = signer.sign(TextNoteEvent.build("test2"))
|
||||
val note3 = signer.sign(TextNoteEvent.build("test3"))
|
||||
fun testInsertDeleteEvent() =
|
||||
forEachDB { db ->
|
||||
val note1 = signer.sign(TextNoteEvent.build("test1"))
|
||||
val note2 = signer.sign(TextNoteEvent.build("test2"))
|
||||
val note3 = signer.sign(TextNoteEvent.build("test3"))
|
||||
|
||||
db.insert(note1)
|
||||
db.insert(note2)
|
||||
db.insert(note3)
|
||||
|
||||
db.assertQuery(note1, Filter(ids = listOf(note1.id)))
|
||||
db.assertQuery(note2, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(note3, Filter(ids = listOf(note3.id)))
|
||||
|
||||
val deletion = signer.sign(DeletionEvent.build(listOf(note1)))
|
||||
|
||||
db.insert(deletion)
|
||||
|
||||
db.assertQuery(deletion, Filter(ids = listOf(deletion.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note1.id)))
|
||||
db.assertQuery(note2, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(note3, Filter(ids = listOf(note3.id)))
|
||||
|
||||
// trying to insert again should fail.
|
||||
try {
|
||||
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.insert(note2)
|
||||
db.insert(note3)
|
||||
|
||||
db.assertQuery(note1, Filter(ids = listOf(note1.id)))
|
||||
db.assertQuery(note2, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(note3, Filter(ids = listOf(note3.id)))
|
||||
|
||||
val deletion = signer.sign(DeletionEvent.build(listOf(note1)))
|
||||
|
||||
db.insert(deletion)
|
||||
|
||||
db.assertQuery(deletion, Filter(ids = listOf(deletion.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note1.id)))
|
||||
db.assertQuery(note2, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(note3, Filter(ids = listOf(note3.id)))
|
||||
|
||||
// trying to insert again should fail.
|
||||
try {
|
||||
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)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note1.id)))
|
||||
db.assertQuery(note2, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(note3, Filter(ids = listOf(note3.id)))
|
||||
}
|
||||
|
||||
db.assertQuery(deletion, Filter(ids = listOf(deletion.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note1.id)))
|
||||
db.assertQuery(note2, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(note3, Filter(ids = listOf(note3.id)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInsertDeleteEventOfAddressable() {
|
||||
val time = TimeUtils.now()
|
||||
val note1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time))
|
||||
val note2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1))
|
||||
val note3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2))
|
||||
fun testInsertDeleteEventOfAddressable() =
|
||||
forEachDB { db ->
|
||||
val time = TimeUtils.now()
|
||||
val note1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time))
|
||||
val note2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1))
|
||||
val note3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2))
|
||||
|
||||
db.insert(note1)
|
||||
|
||||
db.assertQuery(note1, Filter(ids = listOf(note1.id)))
|
||||
|
||||
db.insert(note2)
|
||||
|
||||
db.assertQuery(null, Filter(ids = listOf(note1.id)))
|
||||
db.assertQuery(note2, Filter(ids = listOf(note2.id)))
|
||||
|
||||
db.insert(note3)
|
||||
|
||||
db.assertQuery(null, Filter(ids = listOf(note1.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(note3, Filter(ids = listOf(note3.id)))
|
||||
|
||||
val deletion = signer.sign(DeletionEvent.build(listOf(note1)))
|
||||
|
||||
db.insert(deletion)
|
||||
|
||||
db.assertQuery(deletion, Filter(ids = listOf(deletion.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note1.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note3.id)))
|
||||
|
||||
// trying to insert again should fail.
|
||||
try {
|
||||
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(note1, Filter(ids = listOf(note1.id)))
|
||||
|
||||
db.insert(note2)
|
||||
|
||||
db.assertQuery(null, Filter(ids = listOf(note1.id)))
|
||||
db.assertQuery(note2, Filter(ids = listOf(note2.id)))
|
||||
|
||||
db.insert(note3)
|
||||
|
||||
db.assertQuery(null, Filter(ids = listOf(note1.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(note3, Filter(ids = listOf(note3.id)))
|
||||
|
||||
val deletion = signer.sign(DeletionEvent.build(listOf(note1)))
|
||||
|
||||
db.insert(deletion)
|
||||
|
||||
db.assertQuery(deletion, Filter(ids = listOf(deletion.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note1.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note3.id)))
|
||||
|
||||
// trying to insert again should fail.
|
||||
try {
|
||||
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)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note1.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note3.id)))
|
||||
}
|
||||
|
||||
db.assertQuery(deletion, Filter(ids = listOf(deletion.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note1.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note3.id)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInsertDeleteEventOfAddressable2() {
|
||||
val time = TimeUtils.now()
|
||||
val note1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time))
|
||||
val note2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1))
|
||||
val note3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2))
|
||||
fun testInsertDeleteEventOfAddressable2() =
|
||||
forEachDB { db ->
|
||||
val time = TimeUtils.now()
|
||||
val note1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time))
|
||||
val note2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1))
|
||||
val note3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2))
|
||||
|
||||
db.insert(note1)
|
||||
db.insert(note2)
|
||||
db.insert(note3)
|
||||
|
||||
db.assertQuery(null, Filter(ids = listOf(note1.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(note3, Filter(ids = listOf(note3.id)))
|
||||
|
||||
val deletion = signer.sign(DeletionEvent.buildAddressOnly(listOf(note1)))
|
||||
|
||||
db.insert(deletion)
|
||||
|
||||
db.assertQuery(deletion, Filter(ids = listOf(deletion.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note1.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note3.id)))
|
||||
|
||||
// trying to insert again should fail.
|
||||
try {
|
||||
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.insert(note2)
|
||||
db.insert(note3)
|
||||
|
||||
db.assertQuery(null, Filter(ids = listOf(note1.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(note3, Filter(ids = listOf(note3.id)))
|
||||
|
||||
val deletion = signer.sign(DeletionEvent.buildAddressOnly(listOf(note1)))
|
||||
|
||||
db.insert(deletion)
|
||||
|
||||
db.assertQuery(deletion, Filter(ids = listOf(deletion.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note1.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note3.id)))
|
||||
|
||||
// trying to insert again should fail.
|
||||
try {
|
||||
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)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note1.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note3.id)))
|
||||
}
|
||||
|
||||
db.assertQuery(deletion, Filter(ids = listOf(deletion.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note1.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note3.id)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInsertDeleteWrap() {
|
||||
val me = NostrSignerSync()
|
||||
val myFriend = NostrSignerSync()
|
||||
fun testInsertDeleteWrap() =
|
||||
forEachDB { db ->
|
||||
val me = NostrSignerSync()
|
||||
val myFriend = NostrSignerSync()
|
||||
|
||||
val note1 = me.sign(TextNoteEvent.build("test1"))
|
||||
val wrap1 = GiftWrapEvent.create(note1, me.pubKey)
|
||||
val wrap2 = GiftWrapEvent.create(note1, myFriend.pubKey)
|
||||
val note1 = me.sign(TextNoteEvent.build("test1"))
|
||||
val wrap1 = GiftWrapEvent.create(note1, me.pubKey)
|
||||
val wrap2 = GiftWrapEvent.create(note1, myFriend.pubKey)
|
||||
|
||||
db.insert(wrap1)
|
||||
db.insert(wrap2)
|
||||
|
||||
db.assertQuery(wrap1, Filter(ids = listOf(wrap1.id)))
|
||||
db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id)))
|
||||
|
||||
val randomDeletionToWrap = signer.sign(DeletionEvent.build(listOf(wrap1)))
|
||||
|
||||
db.insert(randomDeletionToWrap)
|
||||
|
||||
db.assertQuery(randomDeletionToWrap, Filter(ids = listOf(randomDeletionToWrap.id)))
|
||||
db.assertQuery(wrap1, Filter(ids = listOf(wrap1.id)))
|
||||
db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id)))
|
||||
|
||||
val deletion = me.sign(DeletionEvent.build(listOf(wrap1)))
|
||||
|
||||
db.insert(deletion)
|
||||
|
||||
db.assertQuery(deletion, Filter(ids = listOf(deletion.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(wrap1.id)))
|
||||
db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id)))
|
||||
|
||||
// trying to insert again should fail.
|
||||
try {
|
||||
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.insert(wrap2)
|
||||
|
||||
db.assertQuery(wrap1, Filter(ids = listOf(wrap1.id)))
|
||||
db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id)))
|
||||
|
||||
val randomDeletionToWrap = signer.sign(DeletionEvent.build(listOf(wrap1)))
|
||||
|
||||
db.insert(randomDeletionToWrap)
|
||||
|
||||
db.assertQuery(randomDeletionToWrap, Filter(ids = listOf(randomDeletionToWrap.id)))
|
||||
db.assertQuery(wrap1, Filter(ids = listOf(wrap1.id)))
|
||||
db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id)))
|
||||
|
||||
val deletion = me.sign(DeletionEvent.build(listOf(wrap1)))
|
||||
|
||||
db.insert(deletion)
|
||||
|
||||
db.assertQuery(deletion, Filter(ids = listOf(deletion.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(wrap1.id)))
|
||||
db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id)))
|
||||
|
||||
// trying to insert again should fail.
|
||||
try {
|
||||
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)))
|
||||
db.assertQuery(null, Filter(ids = listOf(wrap1.id)))
|
||||
db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id)))
|
||||
}
|
||||
|
||||
db.assertQuery(deletion, Filter(ids = listOf(deletion.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(wrap1.id)))
|
||||
db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id)))
|
||||
}
|
||||
@Test
|
||||
fun testTriggersIndexUsage() =
|
||||
forEachDB { db ->
|
||||
val sql =
|
||||
"""
|
||||
SELECT 1 FROM event_tags
|
||||
WHERE
|
||||
event_tags.tag_hash IN (3221122, 223322) AND
|
||||
event_tags.kind = 5 AND
|
||||
event_tags.pubkey_hash = 22332323 AND
|
||||
event_tags.created_at >= 1766686500
|
||||
""".trimIndent()
|
||||
|
||||
val explainer =
|
||||
db.store.explainQuery(sql)
|
||||
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
${sql.replace("\n","\n ")}
|
||||
└── SEARCH event_tags USING COVERING INDEX query_by_tags_hash_kind_pubkey (tag_hash=? AND kind=? AND pubkey_hash=? AND created_at>?)
|
||||
""".trimIndent(),
|
||||
explainer,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTriggersIndexUsage() {
|
||||
val sql =
|
||||
"""
|
||||
SELECT 1 FROM event_tags
|
||||
INNER JOIN event_headers
|
||||
ON event_headers.row_id = event_tags.event_header_row_id
|
||||
WHERE
|
||||
event_tags.tag_hash IN (3221122, 223322) AND
|
||||
event_tags.kind = 5 AND
|
||||
event_tags.pubkey_hash = 22332323 AND
|
||||
event_tags.created_at >= 1766686500
|
||||
""".trimIndent()
|
||||
fun testDeleteById() =
|
||||
forEachDB { db ->
|
||||
val sql =
|
||||
db.store.deletionModule
|
||||
.deleteSQL(
|
||||
pubkey = "key1",
|
||||
idValues = listOf("ca29c211f", "ca29c211d"),
|
||||
addresses = emptyList(),
|
||||
hasher = TagNameValueHasher(0),
|
||||
).first()
|
||||
|
||||
val explainer =
|
||||
db.store.explainQuery(sql)
|
||||
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
${sql.replace("\n","\n ")}
|
||||
├── SEARCH event_tags USING INDEX query_by_tags_hash_kind_pubkey (tag_hash=? AND kind=? AND pubkey_hash=? AND created_at>?)
|
||||
└── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
""".trimIndent(),
|
||||
explainer,
|
||||
)
|
||||
}
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
DELETE FROM event_headers
|
||||
WHERE
|
||||
id IN ("ca29c211f","ca29c211d") AND
|
||||
pubkey_owner_hash = "1573573083296714675"
|
||||
├── SEARCH event_headers USING INDEX event_headers_id (id=?)
|
||||
├── SEARCH event_vanish USING INTEGER PRIMARY KEY (rowid=?)
|
||||
├── SEARCH event_expirations USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── SEARCH event_tags USING COVERING INDEX fk_event_tags_header_id (event_header_row_id=?)
|
||||
""".trimIndent(),
|
||||
db.store.explainQuery(sql.sql, sql.args),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDeleteById() {
|
||||
val sql =
|
||||
db.store.deletionModule
|
||||
.deleteSQL(
|
||||
pubkey = "key1",
|
||||
idValues = listOf("ca29c211f", "ca29c211d"),
|
||||
addresses = emptyList(),
|
||||
hasher = TagNameValueHasher(0),
|
||||
).first()
|
||||
fun testDeleteAddressable() =
|
||||
forEachDB { db ->
|
||||
val sql =
|
||||
db.store.deletionModule
|
||||
.deleteSQL(
|
||||
pubkey = "key1",
|
||||
idValues = emptyList(),
|
||||
addresses =
|
||||
listOf(
|
||||
Address(30000, "key1", "a"),
|
||||
),
|
||||
hasher = TagNameValueHasher(0),
|
||||
).first()
|
||||
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
DELETE FROM event_headers
|
||||
WHERE
|
||||
id IN ("ca29c211f","ca29c211d") AND
|
||||
pubkey_owner_hash = "1573573083296714675"
|
||||
├── SEARCH event_headers USING INDEX event_headers_id (id=?)
|
||||
├── SEARCH event_vanish USING INTEGER PRIMARY KEY (rowid=?)
|
||||
├── SEARCH event_expirations USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── SEARCH event_tags USING COVERING INDEX fk_event_tags_header_id (event_header_row_id=?)
|
||||
""".trimIndent(),
|
||||
db.store.explainQuery(sql.sql, sql.args),
|
||||
)
|
||||
}
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
DELETE FROM event_headers
|
||||
WHERE (
|
||||
(kind = "30000" AND pubkey = "key1" AND d_tag = "a")
|
||||
) AND
|
||||
kind >= 30000 AND kind < 40000
|
||||
├── SEARCH event_headers USING COVERING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?)
|
||||
├── SEARCH event_vanish USING INTEGER PRIMARY KEY (rowid=?)
|
||||
├── SEARCH event_expirations USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── SEARCH event_tags USING COVERING INDEX fk_event_tags_header_id (event_header_row_id=?)
|
||||
""".trimIndent(),
|
||||
db.store.explainQuery(sql.sql, sql.args),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDeleteAddressable() {
|
||||
val sql =
|
||||
db.store.deletionModule
|
||||
.deleteSQL(
|
||||
pubkey = "key1",
|
||||
idValues = emptyList(),
|
||||
addresses =
|
||||
listOf(
|
||||
Address(30000, "key1", "a"),
|
||||
),
|
||||
hasher = TagNameValueHasher(0),
|
||||
).first()
|
||||
fun testDeleteAddressablesSingleKind() =
|
||||
forEachDB { db ->
|
||||
val sql =
|
||||
db.store.deletionModule
|
||||
.deleteSQL(
|
||||
pubkey = "key1",
|
||||
idValues = emptyList(),
|
||||
addresses =
|
||||
listOf(
|
||||
Address(30000, "key1", "a"),
|
||||
Address(30000, "key1", "b"),
|
||||
Address(30000, "key1", "c"),
|
||||
Address(30000, "key1", "d"),
|
||||
),
|
||||
hasher = TagNameValueHasher(0),
|
||||
).first()
|
||||
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
DELETE FROM event_headers
|
||||
WHERE (
|
||||
(kind = "30000" AND pubkey = "key1" AND d_tag = "a")
|
||||
) AND
|
||||
kind >= 30000 AND kind < 40000
|
||||
├── SEARCH event_headers USING COVERING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?)
|
||||
├── SEARCH event_vanish USING INTEGER PRIMARY KEY (rowid=?)
|
||||
├── SEARCH event_expirations USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── SEARCH event_tags USING COVERING INDEX fk_event_tags_header_id (event_header_row_id=?)
|
||||
""".trimIndent(),
|
||||
db.store.explainQuery(sql.sql, sql.args),
|
||||
)
|
||||
}
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
DELETE FROM event_headers
|
||||
WHERE (
|
||||
(kind = "30000" AND pubkey = "key1" AND d_tag IN ("a","b","c","d"))
|
||||
) AND
|
||||
kind >= 30000 AND kind < 40000
|
||||
├── SEARCH event_headers USING COVERING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?)
|
||||
├── SEARCH event_vanish USING INTEGER PRIMARY KEY (rowid=?)
|
||||
├── SEARCH event_expirations USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── SEARCH event_tags USING COVERING INDEX fk_event_tags_header_id (event_header_row_id=?)
|
||||
""".trimIndent(),
|
||||
db.store.explainQuery(sql.sql, sql.args),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDeleteAddressablesSingleKind() {
|
||||
val sql =
|
||||
db.store.deletionModule
|
||||
.deleteSQL(
|
||||
pubkey = "key1",
|
||||
idValues = emptyList(),
|
||||
addresses =
|
||||
listOf(
|
||||
Address(30000, "key1", "a"),
|
||||
Address(30000, "key1", "b"),
|
||||
Address(30000, "key1", "c"),
|
||||
Address(30000, "key1", "d"),
|
||||
),
|
||||
hasher = TagNameValueHasher(0),
|
||||
).first()
|
||||
fun testDeleteAddressablesMultipleKinds() =
|
||||
forEachDB { db ->
|
||||
val sql =
|
||||
db.store.deletionModule
|
||||
.deleteSQL(
|
||||
pubkey = "key1",
|
||||
idValues = emptyList(),
|
||||
addresses =
|
||||
listOf(
|
||||
Address(30000, "key1", "a"),
|
||||
Address(30000, "key1", "b"),
|
||||
Address(30101, "key1", "c"),
|
||||
Address(30101, "key1", "d"),
|
||||
Address(30001, "key2", "e"),
|
||||
Address(30001, "key2", "f"),
|
||||
),
|
||||
hasher = TagNameValueHasher(0),
|
||||
).first()
|
||||
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
DELETE FROM event_headers
|
||||
WHERE (
|
||||
(kind = "30000" AND pubkey = "key1" AND d_tag IN ("a","b","c","d"))
|
||||
) AND
|
||||
kind >= 30000 AND kind < 40000
|
||||
├── SEARCH event_headers USING COVERING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?)
|
||||
├── SEARCH event_vanish USING INTEGER PRIMARY KEY (rowid=?)
|
||||
├── SEARCH event_expirations USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── SEARCH event_tags USING COVERING INDEX fk_event_tags_header_id (event_header_row_id=?)
|
||||
""".trimIndent(),
|
||||
db.store.explainQuery(sql.sql, sql.args),
|
||||
)
|
||||
}
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
DELETE FROM event_headers
|
||||
WHERE (
|
||||
(kind = "30000" AND pubkey = "key1" AND d_tag IN ("a","b"))
|
||||
OR
|
||||
(kind = "30101" AND pubkey = "key1" AND d_tag IN ("c","d"))
|
||||
) AND
|
||||
kind >= 30000 AND kind < 40000
|
||||
├── MULTI-INDEX OR
|
||||
│ ├── INDEX 1
|
||||
│ │ └── SEARCH event_headers USING COVERING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?)
|
||||
│ └── INDEX 2
|
||||
│ └── SEARCH event_headers USING COVERING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?)
|
||||
├── SEARCH event_vanish USING INTEGER PRIMARY KEY (rowid=?)
|
||||
├── SEARCH event_expirations USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── SEARCH event_tags USING COVERING INDEX fk_event_tags_header_id (event_header_row_id=?)
|
||||
""".trimIndent(),
|
||||
db.store.explainQuery(sql.sql, sql.args),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDeleteAddressablesMultipleKinds() {
|
||||
val sql =
|
||||
db.store.deletionModule
|
||||
.deleteSQL(
|
||||
pubkey = "key1",
|
||||
idValues = emptyList(),
|
||||
addresses =
|
||||
listOf(
|
||||
Address(30000, "key1", "a"),
|
||||
Address(30000, "key1", "b"),
|
||||
Address(30101, "key1", "c"),
|
||||
Address(30101, "key1", "d"),
|
||||
Address(30001, "key2", "e"),
|
||||
Address(30001, "key2", "f"),
|
||||
),
|
||||
hasher = TagNameValueHasher(0),
|
||||
).first()
|
||||
fun testDeleteReplaceables() =
|
||||
forEachDB { db ->
|
||||
val sql =
|
||||
db.store.deletionModule
|
||||
.deleteSQL(
|
||||
pubkey = "key1",
|
||||
idValues = emptyList(),
|
||||
addresses =
|
||||
listOf(
|
||||
Address(10000, "key1", ""),
|
||||
Address(10000, "key1", ""),
|
||||
Address(10001, "key1", ""),
|
||||
Address(10001, "key1", ""),
|
||||
Address(10001, "key2", ""),
|
||||
Address(10001, "key2", ""),
|
||||
),
|
||||
hasher = TagNameValueHasher(0),
|
||||
).first()
|
||||
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
DELETE FROM event_headers
|
||||
WHERE (
|
||||
(kind = "30000" AND pubkey = "key1" AND d_tag IN ("a","b"))
|
||||
OR
|
||||
(kind = "30101" AND pubkey = "key1" AND d_tag IN ("c","d"))
|
||||
) AND
|
||||
kind >= 30000 AND kind < 40000
|
||||
├── MULTI-INDEX OR
|
||||
│ ├── INDEX 1
|
||||
│ │ └── SEARCH event_headers USING COVERING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?)
|
||||
│ └── INDEX 2
|
||||
│ └── SEARCH event_headers USING COVERING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?)
|
||||
├── SEARCH event_vanish USING INTEGER PRIMARY KEY (rowid=?)
|
||||
├── SEARCH event_expirations USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── SEARCH event_tags USING COVERING INDEX fk_event_tags_header_id (event_header_row_id=?)
|
||||
""".trimIndent(),
|
||||
db.store.explainQuery(sql.sql, sql.args),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDeleteReplaceables() {
|
||||
val sql =
|
||||
db.store.deletionModule
|
||||
.deleteSQL(
|
||||
pubkey = "key1",
|
||||
idValues = emptyList(),
|
||||
addresses =
|
||||
listOf(
|
||||
Address(10000, "key1", ""),
|
||||
Address(10000, "key1", ""),
|
||||
Address(10001, "key1", ""),
|
||||
Address(10001, "key1", ""),
|
||||
Address(10001, "key2", ""),
|
||||
Address(10001, "key2", ""),
|
||||
),
|
||||
hasher = TagNameValueHasher(0),
|
||||
).first()
|
||||
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
DELETE FROM event_headers
|
||||
WHERE
|
||||
kind IN ("10000","10001") AND
|
||||
pubkey = "key1" AND
|
||||
((kind in (0,3)) OR (kind >= 10000 AND kind < 20000))
|
||||
├── SEARCH event_headers USING COVERING INDEX replaceable_idx (kind=? AND pubkey=?)
|
||||
├── SEARCH event_vanish USING INTEGER PRIMARY KEY (rowid=?)
|
||||
├── SEARCH event_expirations USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── SEARCH event_tags USING COVERING INDEX fk_event_tags_header_id (event_header_row_id=?)
|
||||
""".trimIndent(),
|
||||
db.store.explainQuery(sql.sql, sql.args),
|
||||
)
|
||||
}
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
DELETE FROM event_headers
|
||||
WHERE
|
||||
kind IN ("10000","10001") AND
|
||||
pubkey = "key1" AND
|
||||
((kind in (0,3)) OR (kind >= 10000 AND kind < 20000))
|
||||
├── SEARCH event_headers USING COVERING INDEX replaceable_idx (kind=? AND pubkey=?)
|
||||
├── SEARCH event_vanish USING INTEGER PRIMARY KEY (rowid=?)
|
||||
├── SEARCH event_expirations USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── SEARCH event_tags USING COVERING INDEX fk_event_tags_header_id (event_header_row_id=?)
|
||||
""".trimIndent(),
|
||||
db.store.explainQuery(sql.sql, sql.args),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+43
-58
@@ -20,84 +20,69 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
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.After
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class ExpirationTest {
|
||||
private lateinit var db: EventStore
|
||||
|
||||
class ExpirationTest : BaseDBTest() {
|
||||
val signer = NostrSignerSync()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
db = EventStore(context, null)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDeletingExpiredEvents() {
|
||||
val time = TimeUtils.now()
|
||||
fun testDeletingExpiredEvents() =
|
||||
forEachDB { db ->
|
||||
val time = TimeUtils.now()
|
||||
|
||||
val noteSafe =
|
||||
signer.sign(
|
||||
TextNoteEvent.build("test1", createdAt = time + 1) {
|
||||
expiration(time + 100)
|
||||
},
|
||||
)
|
||||
val noteSafe =
|
||||
signer.sign(
|
||||
TextNoteEvent.build("test1", createdAt = time + 1) {
|
||||
expiration(time + 100)
|
||||
},
|
||||
)
|
||||
|
||||
db.insert(noteSafe)
|
||||
db.insert(noteSafe)
|
||||
|
||||
val noteToExpire =
|
||||
signer.sign(
|
||||
TextNoteEvent.build("test1", createdAt = time + 1) {
|
||||
expiration(time + 1)
|
||||
},
|
||||
)
|
||||
val noteToExpire =
|
||||
signer.sign(
|
||||
TextNoteEvent.build("test1", createdAt = time + 1) {
|
||||
expiration(time + 1)
|
||||
},
|
||||
)
|
||||
|
||||
db.insert(noteToExpire)
|
||||
db.insert(noteToExpire)
|
||||
|
||||
db.assertQuery(noteToExpire, Filter(ids = listOf(noteToExpire.id)))
|
||||
db.assertQuery(noteToExpire, Filter(ids = listOf(noteToExpire.id)))
|
||||
|
||||
Thread.sleep(2000)
|
||||
Thread.sleep(2000)
|
||||
|
||||
db.deleteExpiredEvents()
|
||||
db.deleteExpiredEvents()
|
||||
|
||||
db.assertQuery(null, Filter(ids = listOf(noteToExpire.id)))
|
||||
db.assertQuery(noteSafe, Filter(ids = listOf(noteSafe.id)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInsertingExpiredEvents() {
|
||||
val time = TimeUtils.now()
|
||||
|
||||
val note1 =
|
||||
signer.sign(
|
||||
TextNoteEvent.build("test1", createdAt = time - 12) {
|
||||
expiration(time - 10)
|
||||
},
|
||||
)
|
||||
|
||||
try {
|
||||
db.insert(note1)
|
||||
fail("Should not be able to insert expired events")
|
||||
} catch (e: Exception) {
|
||||
assertTrue(e is SQLiteConstraintException)
|
||||
db.assertQuery(null, Filter(ids = listOf(noteToExpire.id)))
|
||||
db.assertQuery(noteSafe, Filter(ids = listOf(noteSafe.id)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInsertingExpiredEvents() =
|
||||
forEachDB { db ->
|
||||
val time = TimeUtils.now()
|
||||
|
||||
val note1 =
|
||||
signer.sign(
|
||||
TextNoteEvent.build("test1", createdAt = time - 12) {
|
||||
expiration(time - 10)
|
||||
},
|
||||
)
|
||||
|
||||
try {
|
||||
db.insert(note1)
|
||||
fail("Should not be able to insert expired events")
|
||||
} catch (e: Exception) {
|
||||
assertTrue(e is SQLiteConstraintException)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+72
-80
@@ -20,28 +20,11 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class FilterMatcherTest {
|
||||
private lateinit var db: EventStore
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
db = EventStore(context, null)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
class FilterMatcherTest : BaseDBTest() {
|
||||
val id = "98b574c3527f0ffb30b7271084e3f07480733c7289f8de424d29eae82e36c758"
|
||||
val pubkey = "46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d"
|
||||
val createdAt: Long = 1683596206
|
||||
@@ -83,91 +66,100 @@ class FilterMatcherTest {
|
||||
)
|
||||
|
||||
@Test
|
||||
fun matchIds() {
|
||||
db.insert(note)
|
||||
fun matchIds() =
|
||||
forEachDB { db ->
|
||||
db.insert(note)
|
||||
|
||||
db.assertQuery(note, Filter(ids = listOf(id)))
|
||||
db.assertQuery(note, Filter(ids = listOf(id, rootETag)))
|
||||
db.assertQuery(null, Filter(ids = listOf(rootETag)))
|
||||
}
|
||||
db.assertQuery(note, Filter(ids = listOf(id)))
|
||||
db.assertQuery(note, Filter(ids = listOf(id, rootETag)))
|
||||
db.assertQuery(null, Filter(ids = listOf(rootETag)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun matchPubkeys() {
|
||||
db.insert(note)
|
||||
fun matchPubkeys() =
|
||||
forEachDB { db ->
|
||||
db.insert(note)
|
||||
|
||||
db.assertQuery(note, Filter(authors = listOf(pubkey)))
|
||||
db.assertQuery(note, Filter(authors = listOf(pubkey, rootETag)))
|
||||
db.assertQuery(null, Filter(authors = listOf(rootETag)))
|
||||
}
|
||||
db.assertQuery(note, Filter(authors = listOf(pubkey)))
|
||||
db.assertQuery(note, Filter(authors = listOf(pubkey, rootETag)))
|
||||
db.assertQuery(null, Filter(authors = listOf(rootETag)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun matchTags() {
|
||||
db.insert(note)
|
||||
fun matchTags() =
|
||||
forEachDB { db ->
|
||||
db.insert(note)
|
||||
|
||||
db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1))))
|
||||
db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1, pTag2, pTag3))))
|
||||
db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1, id))))
|
||||
db.assertQuery(null, Filter(tags = mapOf("p" to listOf(id))))
|
||||
}
|
||||
db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1))))
|
||||
db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1, pTag2, pTag3))))
|
||||
db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1, id))))
|
||||
db.assertQuery(null, Filter(tags = mapOf("p" to listOf(id))))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun matchDualTags() {
|
||||
db.insert(note)
|
||||
fun matchDualTags() =
|
||||
forEachDB { db ->
|
||||
db.insert(note)
|
||||
|
||||
db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1), "e" to listOf(rootETag))))
|
||||
db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1, pTag2, pTag3), "e" to listOf(rootETag, replyETag))))
|
||||
db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1, id), "e" to listOf(rootETag, replyETag))))
|
||||
db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1, id), "e" to listOf(rootETag, pubkey))))
|
||||
db.assertQuery(null, Filter(tags = mapOf("p" to listOf(pTag1, id), "e" to listOf(id, pubkey))))
|
||||
db.assertQuery(null, Filter(tags = mapOf("p" to listOf(id), "e" to listOf(rootETag))))
|
||||
}
|
||||
db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1), "e" to listOf(rootETag))))
|
||||
db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1, pTag2, pTag3), "e" to listOf(rootETag, replyETag))))
|
||||
db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1, id), "e" to listOf(rootETag, replyETag))))
|
||||
db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1, id), "e" to listOf(rootETag, pubkey))))
|
||||
db.assertQuery(null, Filter(tags = mapOf("p" to listOf(pTag1, id), "e" to listOf(id, pubkey))))
|
||||
db.assertQuery(null, Filter(tags = mapOf("p" to listOf(id), "e" to listOf(rootETag))))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun matchAllTags() {
|
||||
db.insert(note)
|
||||
fun matchAllTags() =
|
||||
forEachDB { db ->
|
||||
db.insert(note)
|
||||
|
||||
db.assertQuery(note, Filter(tagsAll = mapOf("p" to listOf(pTag1))))
|
||||
db.assertQuery(note, Filter(tagsAll = mapOf("p" to listOf(pTag1, pTag2, pTag3))))
|
||||
db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(pTag1, id))))
|
||||
db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(id))))
|
||||
}
|
||||
db.assertQuery(note, Filter(tagsAll = mapOf("p" to listOf(pTag1))))
|
||||
db.assertQuery(note, Filter(tagsAll = mapOf("p" to listOf(pTag1, pTag2, pTag3))))
|
||||
db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(pTag1, id))))
|
||||
db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(id))))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun matchDualAllTags() {
|
||||
db.insert(note)
|
||||
fun matchDualAllTags() =
|
||||
forEachDB { db ->
|
||||
db.insert(note)
|
||||
|
||||
db.assertQuery(note, Filter(tagsAll = mapOf("p" to listOf(pTag1), "e" to listOf(rootETag))))
|
||||
db.assertQuery(note, Filter(tagsAll = mapOf("p" to listOf(pTag1, pTag2, pTag3), "e" to listOf(rootETag, replyETag))))
|
||||
db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(pTag1, id), "e" to listOf(rootETag, replyETag))))
|
||||
db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(pTag1, id), "e" to listOf(rootETag, pubkey))))
|
||||
db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(pTag1, id), "e" to listOf(id, pubkey))))
|
||||
db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(id), "e" to listOf(rootETag))))
|
||||
}
|
||||
db.assertQuery(note, Filter(tagsAll = mapOf("p" to listOf(pTag1), "e" to listOf(rootETag))))
|
||||
db.assertQuery(note, Filter(tagsAll = mapOf("p" to listOf(pTag1, pTag2, pTag3), "e" to listOf(rootETag, replyETag))))
|
||||
db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(pTag1, id), "e" to listOf(rootETag, replyETag))))
|
||||
db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(pTag1, id), "e" to listOf(rootETag, pubkey))))
|
||||
db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(pTag1, id), "e" to listOf(id, pubkey))))
|
||||
db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(id), "e" to listOf(rootETag))))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun matchKinds() {
|
||||
db.insert(note)
|
||||
fun matchKinds() =
|
||||
forEachDB { db ->
|
||||
db.insert(note)
|
||||
|
||||
db.assertQuery(note, Filter(kinds = listOf(kind)))
|
||||
db.assertQuery(note, Filter(kinds = listOf(kind, 1221)))
|
||||
db.assertQuery(null, Filter(kinds = listOf(1221)))
|
||||
}
|
||||
db.assertQuery(note, Filter(kinds = listOf(kind)))
|
||||
db.assertQuery(note, Filter(kinds = listOf(kind, 1221)))
|
||||
db.assertQuery(null, Filter(kinds = listOf(1221)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun matchSince() {
|
||||
db.insert(note)
|
||||
fun matchSince() =
|
||||
forEachDB { db ->
|
||||
db.insert(note)
|
||||
|
||||
db.assertQuery(note, Filter(since = createdAt))
|
||||
db.assertQuery(note, Filter(since = createdAt - 1))
|
||||
db.assertQuery(null, Filter(since = createdAt + 1))
|
||||
}
|
||||
db.assertQuery(note, Filter(since = createdAt))
|
||||
db.assertQuery(note, Filter(since = createdAt - 1))
|
||||
db.assertQuery(null, Filter(since = createdAt + 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun matchUntil() {
|
||||
db.insert(note)
|
||||
fun matchUntil() =
|
||||
forEachDB { db ->
|
||||
db.insert(note)
|
||||
|
||||
db.assertQuery(note, Filter(until = createdAt))
|
||||
db.assertQuery(note, Filter(until = createdAt + 1))
|
||||
db.assertQuery(null, Filter(until = createdAt - 1))
|
||||
}
|
||||
db.assertQuery(note, Filter(until = createdAt))
|
||||
db.assertQuery(note, Filter(until = createdAt + 1))
|
||||
db.assertQuery(null, Filter(until = createdAt - 1))
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -31,6 +31,7 @@ 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
|
||||
@@ -82,6 +83,7 @@ class LargeDBTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Not testing")
|
||||
fun insertDatabase() {
|
||||
events.forEach { event ->
|
||||
try {
|
||||
|
||||
+272
-286
@@ -26,6 +26,7 @@ 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.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
|
||||
@@ -38,59 +39,68 @@ import org.junit.Test
|
||||
|
||||
class QueryAssemblerTest {
|
||||
val hasher = TagNameValueHasher(0)
|
||||
val builder = QueryBuilder(FullTextSearchModule(), { hasher })
|
||||
|
||||
val key1 = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d"
|
||||
val key2 = "f3ac434d61bc0f491a814782ccfdf9c439dae1f0bde9097ad4a245f4c495cd14"
|
||||
val key3 = "12ae0fd81c85e1e7d9ed096397dc3129849425fe6f8afce7213ebf38ddfc6ca9"
|
||||
|
||||
private lateinit var db: EventStore
|
||||
private lateinit var dbAllIndexes: EventStore
|
||||
private lateinit var dbNoKindIndexes: EventStore
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
db = EventStore(context, null)
|
||||
dbAllIndexes = EventStore(context, null, indexStrategy = DefaultIndexingStrategy(true))
|
||||
dbNoKindIndexes = EventStore(context, null, indexStrategy = DefaultIndexingStrategy(false))
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
db.close()
|
||||
dbAllIndexes.close()
|
||||
dbNoKindIndexes.close()
|
||||
}
|
||||
|
||||
fun explain(f: Filter) = builder.planQuery(f, hasher, db.store.readableDatabase)
|
||||
fun EventStore.explain(f: Filter) = store.queryBuilder.planQuery(f, hasher, store.readableDatabase)
|
||||
|
||||
fun explain(f: List<Filter>) = builder.planQuery(f, hasher, db.store.readableDatabase)
|
||||
fun EventStore.explain(f: List<Filter>) = store.queryBuilder.planQuery(f, hasher, store.readableDatabase)
|
||||
|
||||
@Test
|
||||
fun testEmpty() {
|
||||
Assert.assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers ORDER BY created_at DESC, id
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
ORDER BY created_at DESC
|
||||
└── SCAN event_headers USING INDEX query_by_created_at_id
|
||||
""".trimIndent(),
|
||||
explain(Filter()),
|
||||
dbAllIndexes.explain(Filter()),
|
||||
)
|
||||
|
||||
Assert.assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
ORDER BY created_at DESC
|
||||
└── SCAN event_headers USING INDEX query_by_created_at_id
|
||||
""".trimIndent(),
|
||||
dbNoKindIndexes.explain(Filter()),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCheckDeletionEventExists() {
|
||||
val query =
|
||||
explain(
|
||||
Filter(
|
||||
kinds = listOf(5),
|
||||
authors = listOf(key1),
|
||||
tags = mapOf("e" to listOf(key2)),
|
||||
since = 1750889190,
|
||||
),
|
||||
val filter =
|
||||
Filter(
|
||||
kinds = listOf(5),
|
||||
authors = listOf(key1),
|
||||
tags = mapOf("e" to listOf(key2)),
|
||||
since = 1750889190,
|
||||
)
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "2657743813502222172") AND (event_tags.kind = "5") AND (event_tags.pubkey_hash = "1730514094536529999") AND (event_tags.created_at >= "1750889190")
|
||||
SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "2657743813502222172") AND (event_tags.kind = "5") AND (event_tags.pubkey_hash = "1730514094536529999") AND (event_tags.created_at >= "1750889190")
|
||||
) AS filtered
|
||||
ON event_headers.row_id = filtered.row_id
|
||||
ORDER BY created_at DESC, id
|
||||
ORDER BY created_at DESC
|
||||
├── CO-ROUTINE filtered
|
||||
│ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind_pubkey (tag_hash=? AND kind=? AND pubkey_hash=? AND created_at>?)
|
||||
│ └── USE TEMP B-TREE FOR DISTINCT
|
||||
@@ -98,44 +108,55 @@ class QueryAssemblerTest {
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── USE TEMP B-TREE FOR ORDER BY
|
||||
""".trimIndent(),
|
||||
query,
|
||||
dbAllIndexes.explain(filter),
|
||||
)
|
||||
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "2657743813502222172") AND (event_tags.kind = "5") AND (event_tags.pubkey_hash = "1730514094536529999") AND (event_tags.created_at >= "1750889190")
|
||||
) AS filtered
|
||||
ON event_headers.row_id = filtered.row_id
|
||||
ORDER BY created_at DESC
|
||||
├── CO-ROUTINE filtered
|
||||
│ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind_pubkey (tag_hash=? AND kind=? AND pubkey_hash=? AND created_at>?)
|
||||
│ └── USE TEMP B-TREE FOR DISTINCT
|
||||
├── SCAN filtered
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── USE TEMP B-TREE FOR ORDER BY
|
||||
""".trimIndent(),
|
||||
dbNoKindIndexes.explain(filter),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLimit() {
|
||||
val explainer = explain(Filter(limit = 10))
|
||||
val filter = Filter(limit = 10)
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
SELECT event_headers.row_id as row_id FROM event_headers WHERE 1 = 1 ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 10
|
||||
) AS filtered
|
||||
ON event_headers.row_id = filtered.row_id
|
||||
ORDER BY created_at DESC, id
|
||||
├── CO-ROUTINE filtered
|
||||
│ └── SCAN event_headers USING COVERING INDEX query_by_created_at_id
|
||||
├── SCAN filtered
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── USE TEMP B-TREE FOR ORDER BY
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 10
|
||||
└── SCAN event_headers USING INDEX query_by_created_at_id
|
||||
""".trimIndent(),
|
||||
explainer,
|
||||
dbAllIndexes.explain(filter),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLimits() {
|
||||
val explainer = explain(listOf(Filter(limit = 10), Filter(limit = 30)))
|
||||
val filter = listOf(Filter(limit = 10), Filter(limit = 30))
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers WHERE 1 = 1 ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 10)
|
||||
SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers ORDER BY event_headers.created_at DESC LIMIT 10)
|
||||
UNION
|
||||
SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers WHERE 1 = 1 ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 30)
|
||||
SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers ORDER BY event_headers.created_at DESC LIMIT 30)
|
||||
) AS filtered
|
||||
ON event_headers.row_id = filtered.row_id
|
||||
ORDER BY created_at DESC, id
|
||||
ORDER BY created_at DESC
|
||||
├── CO-ROUTINE filtered
|
||||
│ └── COMPOUND QUERY
|
||||
│ ├── LEFT-MOST SUBQUERY
|
||||
@@ -150,37 +171,35 @@ class QueryAssemblerTest {
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── USE TEMP B-TREE FOR ORDER BY
|
||||
""".trimIndent(),
|
||||
explainer,
|
||||
dbAllIndexes.explain(filter),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAllFeatures() {
|
||||
val sql =
|
||||
explain(
|
||||
listOf(
|
||||
Filter(limit = 10),
|
||||
Filter(
|
||||
authors = listOf(key1),
|
||||
kinds = listOf(1, 1111),
|
||||
search = "keywords",
|
||||
limit = 100,
|
||||
),
|
||||
Filter(kinds = listOf(20), search = "cats", limit = 30),
|
||||
val filter =
|
||||
listOf(
|
||||
Filter(limit = 10),
|
||||
Filter(
|
||||
authors = listOf(key1),
|
||||
kinds = listOf(1, 1111),
|
||||
search = "keywords",
|
||||
limit = 100,
|
||||
),
|
||||
Filter(kinds = listOf(20), search = "cats", limit = 30),
|
||||
)
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers WHERE 1 = 1 ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 10)
|
||||
SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers ORDER BY event_headers.created_at DESC LIMIT 10)
|
||||
UNION
|
||||
SELECT row_id FROM (SELECT event_fts.event_header_row_id as row_id FROM event_fts INNER JOIN event_headers ON event_headers.row_id = event_fts.event_header_row_id WHERE (event_headers.kind IN ("1", "1111")) AND (event_headers.pubkey = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d") AND (event_fts MATCH "keywords") ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 100)
|
||||
SELECT row_id FROM (SELECT event_fts.event_header_row_id as row_id FROM event_fts INNER JOIN event_headers ON event_headers.row_id = event_fts.event_header_row_id WHERE (event_headers.kind IN ("1", "1111")) AND (event_headers.pubkey = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d") AND (event_fts MATCH "keywords") ORDER BY event_headers.created_at DESC LIMIT 100)
|
||||
UNION
|
||||
SELECT row_id FROM (SELECT event_fts.event_header_row_id as row_id FROM event_fts INNER JOIN event_headers ON event_headers.row_id = event_fts.event_header_row_id WHERE (event_headers.kind = "20") AND (event_fts MATCH "cats") ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 30)
|
||||
SELECT row_id FROM (SELECT event_fts.event_header_row_id as row_id FROM event_fts INNER JOIN event_headers ON event_headers.row_id = event_fts.event_header_row_id WHERE (event_headers.kind = "20") AND (event_fts MATCH "cats") ORDER BY event_headers.created_at DESC LIMIT 30)
|
||||
) AS filtered
|
||||
ON event_headers.row_id = filtered.row_id
|
||||
ORDER BY created_at DESC, id
|
||||
ORDER BY created_at DESC
|
||||
├── CO-ROUTINE filtered
|
||||
│ └── COMPOUND QUERY
|
||||
│ ├── LEFT-MOST SUBQUERY
|
||||
@@ -195,99 +214,119 @@ class QueryAssemblerTest {
|
||||
│ │ └── SCAN (subquery-3)
|
||||
│ └── UNION USING TEMP B-TREE
|
||||
│ ├── CO-ROUTINE (subquery-5)
|
||||
│ │ ├── SCAN event_fts VIRTUAL TABLE INDEX 4:
|
||||
│ │ ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
│ │ └── USE TEMP B-TREE FOR ORDER BY
|
||||
│ │ ├── SEARCH event_headers USING COVERING INDEX query_by_kind_created (kind=?)
|
||||
│ │ └── SCAN event_fts VIRTUAL TABLE INDEX 4:
|
||||
│ └── SCAN (subquery-5)
|
||||
├── SCAN filtered
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── USE TEMP B-TREE FOR ORDER BY
|
||||
""".trimIndent(),
|
||||
sql,
|
||||
dbAllIndexes.explain(filter),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testKind() {
|
||||
val sql =
|
||||
explain(
|
||||
listOf(
|
||||
Filter(
|
||||
kinds = listOf(ContactListEvent.KIND),
|
||||
limit = 30,
|
||||
),
|
||||
fun testSingleFilterConversionToSimpleQuery() {
|
||||
val filter =
|
||||
listOf(
|
||||
Filter(
|
||||
kinds = listOf(ContactListEvent.KIND),
|
||||
limit = 30,
|
||||
),
|
||||
)
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
WHERE kind = "3"
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 30
|
||||
└── SEARCH event_headers USING INDEX query_by_kind_created (kind=?)
|
||||
""".trimIndent(),
|
||||
dbAllIndexes.explain(filter),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testKinds() {
|
||||
val filter =
|
||||
listOf(
|
||||
Filter(
|
||||
kinds = listOf(ContactListEvent.KIND),
|
||||
limit = 30,
|
||||
),
|
||||
Filter(
|
||||
kinds = listOf(TextNoteEvent.KIND),
|
||||
limit = 30,
|
||||
),
|
||||
)
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
SELECT event_headers.row_id as row_id FROM event_headers WHERE event_headers.kind = "3" ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 30
|
||||
SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers WHERE event_headers.kind = "3" ORDER BY event_headers.created_at DESC LIMIT 30)
|
||||
UNION
|
||||
SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers WHERE event_headers.kind = "1" ORDER BY event_headers.created_at DESC LIMIT 30)
|
||||
) AS filtered
|
||||
ON event_headers.row_id = filtered.row_id
|
||||
ORDER BY created_at DESC, id
|
||||
ORDER BY created_at DESC
|
||||
├── CO-ROUTINE filtered
|
||||
│ ├── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=?)
|
||||
│ └── USE TEMP B-TREE FOR ORDER BY
|
||||
│ └── COMPOUND QUERY
|
||||
│ ├── LEFT-MOST SUBQUERY
|
||||
│ │ ├── CO-ROUTINE (subquery-1)
|
||||
│ │ │ └── SEARCH event_headers USING COVERING INDEX query_by_kind_created (kind=?)
|
||||
│ │ └── SCAN (subquery-1)
|
||||
│ └── UNION USING TEMP B-TREE
|
||||
│ ├── CO-ROUTINE (subquery-3)
|
||||
│ │ └── SEARCH event_headers USING COVERING INDEX query_by_kind_created (kind=?)
|
||||
│ └── SCAN (subquery-3)
|
||||
├── SCAN filtered
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── USE TEMP B-TREE FOR ORDER BY
|
||||
""".trimIndent(),
|
||||
sql,
|
||||
dbAllIndexes.explain(filter),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testKindAndDTag() {
|
||||
val sql =
|
||||
explain(
|
||||
listOf(
|
||||
Filter(
|
||||
kinds = listOf(ContactListEvent.KIND),
|
||||
tags = mapOf("d" to listOf("")),
|
||||
limit = 30,
|
||||
),
|
||||
val filter =
|
||||
listOf(
|
||||
Filter(
|
||||
kinds = listOf(ContactListEvent.KIND),
|
||||
tags = mapOf("d" to listOf("")),
|
||||
limit = 30,
|
||||
),
|
||||
)
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
SELECT event_headers.row_id as row_id FROM event_headers WHERE (event_headers.kind = "3") AND (event_headers.d_tag = "") ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 30
|
||||
) AS filtered
|
||||
ON event_headers.row_id = filtered.row_id
|
||||
ORDER BY created_at DESC, id
|
||||
├── CO-ROUTINE filtered
|
||||
│ ├── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=?)
|
||||
│ └── USE TEMP B-TREE FOR ORDER BY
|
||||
├── SCAN filtered
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── USE TEMP B-TREE FOR ORDER BY
|
||||
WHERE (kind = "3") AND (d_tag = "")
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 30
|
||||
└── SEARCH event_headers USING INDEX query_by_kind_created (kind=?)
|
||||
""".trimIndent(),
|
||||
sql,
|
||||
dbAllIndexes.explain(filter),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFollowersOf() {
|
||||
val sql =
|
||||
explain(
|
||||
listOf(
|
||||
Filter(
|
||||
kinds = listOf(ContactListEvent.KIND),
|
||||
tags = mapOf("p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")),
|
||||
limit = 30,
|
||||
),
|
||||
val filter =
|
||||
listOf(
|
||||
Filter(
|
||||
kinds = listOf(ContactListEvent.KIND),
|
||||
tags = mapOf("p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")),
|
||||
limit = 30,
|
||||
),
|
||||
)
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") ORDER BY event_tags.created_at DESC LIMIT 30
|
||||
SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") ORDER BY event_tags.created_at DESC LIMIT 30
|
||||
) AS filtered
|
||||
ON event_headers.row_id = filtered.row_id
|
||||
ORDER BY created_at DESC, id
|
||||
ORDER BY created_at DESC
|
||||
├── CO-ROUTINE filtered
|
||||
│ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=? AND kind=?)
|
||||
│ └── USE TEMP B-TREE FOR DISTINCT
|
||||
@@ -295,29 +334,27 @@ class QueryAssemblerTest {
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── USE TEMP B-TREE FOR ORDER BY
|
||||
""".trimIndent(),
|
||||
sql,
|
||||
dbAllIndexes.explain(filter),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNotificationsOf() {
|
||||
val sql =
|
||||
explain(
|
||||
listOf(
|
||||
Filter(
|
||||
tags = mapOf("p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")),
|
||||
limit = 30,
|
||||
),
|
||||
val filter =
|
||||
listOf(
|
||||
Filter(
|
||||
tags = mapOf("p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")),
|
||||
limit = 30,
|
||||
),
|
||||
)
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE event_tags.tag_hash = "-4551135004136952885" ORDER BY event_tags.created_at DESC LIMIT 30
|
||||
SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE event_tags.tag_hash = "-4551135004136952885" ORDER BY event_tags.created_at DESC LIMIT 30
|
||||
) AS filtered
|
||||
ON event_headers.row_id = filtered.row_id
|
||||
ORDER BY created_at DESC, id
|
||||
ORDER BY created_at DESC
|
||||
├── CO-ROUTINE filtered
|
||||
│ ├── SEARCH event_tags USING INDEX query_by_tags_hash (tag_hash=?)
|
||||
│ └── USE TEMP B-TREE FOR DISTINCT
|
||||
@@ -325,30 +362,28 @@ class QueryAssemblerTest {
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── USE TEMP B-TREE FOR ORDER BY
|
||||
""".trimIndent(),
|
||||
sql,
|
||||
dbAllIndexes.explain(filter),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTagsAndKinds() {
|
||||
val sql =
|
||||
explain(
|
||||
listOf(
|
||||
Filter(
|
||||
kinds = listOf(ContactListEvent.KIND),
|
||||
tags = mapOf("p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")),
|
||||
limit = 30,
|
||||
),
|
||||
val filter =
|
||||
listOf(
|
||||
Filter(
|
||||
kinds = listOf(ContactListEvent.KIND),
|
||||
tags = mapOf("p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")),
|
||||
limit = 30,
|
||||
),
|
||||
)
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") ORDER BY event_tags.created_at DESC LIMIT 30
|
||||
SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") ORDER BY event_tags.created_at DESC LIMIT 30
|
||||
) AS filtered
|
||||
ON event_headers.row_id = filtered.row_id
|
||||
ORDER BY created_at DESC, id
|
||||
ORDER BY created_at DESC
|
||||
├── CO-ROUTINE filtered
|
||||
│ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=? AND kind=?)
|
||||
│ └── USE TEMP B-TREE FOR DISTINCT
|
||||
@@ -356,30 +391,28 @@ class QueryAssemblerTest {
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── USE TEMP B-TREE FOR ORDER BY
|
||||
""".trimIndent(),
|
||||
sql,
|
||||
dbAllIndexes.explain(filter),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTagsAndAuthors() {
|
||||
val sql =
|
||||
explain(
|
||||
listOf(
|
||||
Filter(
|
||||
authors = listOf(key1),
|
||||
tags = mapOf("p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")),
|
||||
limit = 30,
|
||||
),
|
||||
val filter =
|
||||
listOf(
|
||||
Filter(
|
||||
authors = listOf(key1),
|
||||
tags = mapOf("p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")),
|
||||
limit = 30,
|
||||
),
|
||||
)
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.pubkey_hash = "1730514094536529999") ORDER BY event_tags.created_at DESC LIMIT 30
|
||||
SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.pubkey_hash = "1730514094536529999") ORDER BY event_tags.created_at DESC LIMIT 30
|
||||
) AS filtered
|
||||
ON event_headers.row_id = filtered.row_id
|
||||
ORDER BY created_at DESC, id
|
||||
ORDER BY created_at DESC
|
||||
├── CO-ROUTINE filtered
|
||||
│ ├── SEARCH event_tags USING INDEX query_by_tags_hash (tag_hash=?)
|
||||
│ └── USE TEMP B-TREE FOR DISTINCT
|
||||
@@ -387,34 +420,32 @@ class QueryAssemblerTest {
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── USE TEMP B-TREE FOR ORDER BY
|
||||
""".trimIndent(),
|
||||
sql,
|
||||
dbAllIndexes.explain(filter),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTwoTags() {
|
||||
val sql =
|
||||
explain(
|
||||
listOf(
|
||||
Filter(
|
||||
kinds = listOf(1),
|
||||
tags =
|
||||
mapOf(
|
||||
"p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
|
||||
"t" to listOf("hashtag"),
|
||||
),
|
||||
limit = 30,
|
||||
),
|
||||
val filter =
|
||||
listOf(
|
||||
Filter(
|
||||
kinds = listOf(1),
|
||||
tags =
|
||||
mapOf(
|
||||
"p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
|
||||
"t" to listOf("hashtag"),
|
||||
),
|
||||
limit = 30,
|
||||
),
|
||||
)
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_tags as event_tagsIn1 ON event_tagsIn1.event_header_row_id = event_tags.event_header_row_id AND event_tagsIn1.created_at = event_tags.created_at WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tagsIn1.tag_hash = "-6379614208644810021") AND (event_tags.kind = "1") ORDER BY event_tags.created_at DESC LIMIT 30
|
||||
SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_tags as event_tagsIn1 ON event_tagsIn1.event_header_row_id = event_tags.event_header_row_id AND event_tagsIn1.created_at = event_tags.created_at WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tagsIn1.tag_hash = "-6379614208644810021") AND (event_tags.kind = "1") ORDER BY event_tags.created_at DESC LIMIT 30
|
||||
) AS filtered
|
||||
ON event_headers.row_id = filtered.row_id
|
||||
ORDER BY created_at DESC, id
|
||||
ORDER BY created_at DESC
|
||||
├── CO-ROUTINE filtered
|
||||
│ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=? AND kind=?)
|
||||
│ ├── SEARCH event_tagsIn1 USING INDEX query_by_tags_hash (tag_hash=? AND created_at=?)
|
||||
@@ -423,101 +454,87 @@ class QueryAssemblerTest {
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── USE TEMP B-TREE FOR ORDER BY
|
||||
""".trimIndent(),
|
||||
sql,
|
||||
dbAllIndexes.explain(filter),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIdQuery() {
|
||||
val sql = explain(Filter(ids = listOf(key1)))
|
||||
val filter = Filter(ids = listOf(key1))
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
SELECT event_headers.row_id as row_id FROM event_headers WHERE event_headers.id = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d"
|
||||
) AS filtered
|
||||
ON event_headers.row_id = filtered.row_id
|
||||
ORDER BY created_at DESC, id
|
||||
├── SEARCH event_headers USING COVERING INDEX event_headers_id (id=?)
|
||||
└── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
WHERE id = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d"
|
||||
ORDER BY created_at DESC
|
||||
└── SEARCH event_headers USING INDEX event_headers_id (id=?)
|
||||
""".trimIndent(),
|
||||
sql,
|
||||
dbAllIndexes.explain(filter),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAuthors() {
|
||||
val sql = explain(Filter(authors = listOf(key1, key2), kinds = listOf(1, 30023), limit = 300))
|
||||
val filter = Filter(authors = listOf(key1, key2), kinds = listOf(1, 30023), limit = 300)
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
SELECT event_headers.row_id as row_id FROM event_headers WHERE (event_headers.kind IN ("1", "30023")) AND (event_headers.pubkey IN ("7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d", "f3ac434d61bc0f491a814782ccfdf9c439dae1f0bde9097ad4a245f4c495cd14")) ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 300
|
||||
) AS filtered
|
||||
ON event_headers.row_id = filtered.row_id
|
||||
ORDER BY created_at DESC, id
|
||||
├── CO-ROUTINE filtered
|
||||
│ ├── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=?)
|
||||
│ └── USE TEMP B-TREE FOR ORDER BY
|
||||
├── SCAN filtered
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
WHERE (kind IN ("1", "30023")) AND (pubkey IN ("7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d", "f3ac434d61bc0f491a814782ccfdf9c439dae1f0bde9097ad4a245f4c495cd14"))
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 300
|
||||
├── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=?)
|
||||
└── USE TEMP B-TREE FOR ORDER BY
|
||||
""".trimIndent(),
|
||||
sql,
|
||||
dbAllIndexes.explain(filter),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAuthorsAndSearch() {
|
||||
val sql = explain(Filter(authors = listOf(key1, key2, key3), search = "keywords"))
|
||||
val filter = Filter(authors = listOf(key1, key2, key3), search = "keywords")
|
||||
println(dbAllIndexes.explain(filter))
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
SELECT event_fts.event_header_row_id as row_id FROM event_fts INNER JOIN event_headers ON event_headers.row_id = event_fts.event_header_row_id WHERE (event_headers.pubkey IN ("7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d", "f3ac434d61bc0f491a814782ccfdf9c439dae1f0bde9097ad4a245f4c495cd14", "12ae0fd81c85e1e7d9ed096397dc3129849425fe6f8afce7213ebf38ddfc6ca9")) AND (event_fts MATCH "keywords")
|
||||
) AS filtered
|
||||
ON event_headers.row_id = filtered.row_id
|
||||
ORDER BY created_at DESC, id
|
||||
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:
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── USE TEMP B-TREE FOR ORDER BY
|
||||
""".trimIndent(),
|
||||
sql,
|
||||
dbAllIndexes.explain(filter),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testKindAndSearch() {
|
||||
val sql = explain(Filter(kinds = listOf(1, 1111, 10000), search = "keywords"))
|
||||
val filter = Filter(kinds = listOf(1, 1111, 10000), search = "keywords")
|
||||
println(dbAllIndexes.explain(filter))
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
SELECT event_fts.event_header_row_id as row_id FROM event_fts INNER JOIN event_headers ON event_headers.row_id = event_fts.event_header_row_id WHERE (event_headers.kind IN ("1", "1111", "10000")) AND (event_fts MATCH "keywords")
|
||||
) AS filtered
|
||||
ON event_headers.row_id = filtered.row_id
|
||||
ORDER BY created_at DESC, id
|
||||
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 event_headers.created_at DESC
|
||||
├── SCAN event_fts VIRTUAL TABLE INDEX 4:
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── USE TEMP B-TREE FOR ORDER BY
|
||||
""".trimIndent(),
|
||||
sql,
|
||||
dbAllIndexes.explain(filter),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAllTag() {
|
||||
val sql = explain(Filter(tagsAll = mapOf("p" to listOf(key1, key2))))
|
||||
val filter = Filter(tagsAll = mapOf("p" to listOf(key1, key2)))
|
||||
TestCase.assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_tags as event_tagsAll0_1 ON event_tagsAll0_1.event_header_row_id = event_tags.event_header_row_id AND event_tagsAll0_1.created_at = event_tags.created_at WHERE (event_tags.tag_hash = "884286737453847614") AND (event_tagsAll0_1.tag_hash = "-4988851810256311323")
|
||||
SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_tags as event_tagsAll0_1 ON event_tagsAll0_1.event_header_row_id = event_tags.event_header_row_id AND event_tagsAll0_1.created_at = event_tags.created_at WHERE (event_tags.tag_hash = "884286737453847614") AND (event_tagsAll0_1.tag_hash = "-4988851810256311323")
|
||||
) AS filtered
|
||||
ON event_headers.row_id = filtered.row_id
|
||||
ORDER BY created_at DESC, id
|
||||
ORDER BY created_at DESC
|
||||
├── 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=?)
|
||||
@@ -526,32 +543,30 @@ class QueryAssemblerTest {
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── USE TEMP B-TREE FOR ORDER BY
|
||||
""".trimIndent(),
|
||||
sql,
|
||||
dbAllIndexes.explain(filter),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testReportLikeFilter() {
|
||||
val sql =
|
||||
explain(
|
||||
Filter(
|
||||
kinds = listOf(ContactListEvent.KIND),
|
||||
authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
|
||||
tags =
|
||||
mapOf(
|
||||
"p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
|
||||
),
|
||||
limit = 500,
|
||||
),
|
||||
val filter =
|
||||
Filter(
|
||||
kinds = listOf(ContactListEvent.KIND),
|
||||
authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
|
||||
tags =
|
||||
mapOf(
|
||||
"p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
|
||||
),
|
||||
limit = 500,
|
||||
)
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") AND (event_tags.pubkey_hash = "5446767199141196776") ORDER BY event_tags.created_at DESC LIMIT 500
|
||||
SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") AND (event_tags.pubkey_hash = "5446767199141196776") ORDER BY event_tags.created_at DESC LIMIT 500
|
||||
) AS filtered
|
||||
ON event_headers.row_id = filtered.row_id
|
||||
ORDER BY created_at DESC, id
|
||||
ORDER BY created_at DESC
|
||||
├── CO-ROUTINE filtered
|
||||
│ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind_pubkey (tag_hash=? AND kind=? AND pubkey_hash=?)
|
||||
│ └── USE TEMP B-TREE FOR DISTINCT
|
||||
@@ -559,32 +574,29 @@ class QueryAssemblerTest {
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── USE TEMP B-TREE FOR ORDER BY
|
||||
""".trimIndent(),
|
||||
sql,
|
||||
dbAllIndexes.explain(filter),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFollowersSinceNov2025() {
|
||||
val sql =
|
||||
explain(
|
||||
Filter(
|
||||
kinds = listOf(ContactListEvent.KIND),
|
||||
tags =
|
||||
mapOf(
|
||||
"p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
|
||||
),
|
||||
since = 1764553447, // Nov 2025
|
||||
),
|
||||
val filter =
|
||||
Filter(
|
||||
kinds = listOf(ContactListEvent.KIND),
|
||||
tags =
|
||||
mapOf(
|
||||
"p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
|
||||
),
|
||||
since = 1764553447, // Nov 2025
|
||||
)
|
||||
println(sql)
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") AND (event_tags.created_at >= "1764553447")
|
||||
SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") AND (event_tags.created_at >= "1764553447")
|
||||
) AS filtered
|
||||
ON event_headers.row_id = filtered.row_id
|
||||
ORDER BY created_at DESC, id
|
||||
ORDER BY created_at DESC
|
||||
├── CO-ROUTINE filtered
|
||||
│ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=? AND kind=? AND created_at>?)
|
||||
│ └── USE TEMP B-TREE FOR DISTINCT
|
||||
@@ -592,119 +604,93 @@ class QueryAssemblerTest {
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── USE TEMP B-TREE FOR ORDER BY
|
||||
""".trimIndent(),
|
||||
sql,
|
||||
dbAllIndexes.explain(filter),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAllAddressablesOfAKindDownload() {
|
||||
val sql =
|
||||
explain(
|
||||
Filter(
|
||||
kinds = listOf(DraftWrapEvent.KIND),
|
||||
authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
|
||||
since = 1764553447, // Nov 2025
|
||||
),
|
||||
val filter =
|
||||
Filter(
|
||||
kinds = listOf(DraftWrapEvent.KIND),
|
||||
authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
|
||||
since = 1764553447, // Nov 2025
|
||||
)
|
||||
println(sql)
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
SELECT event_headers.row_id as row_id FROM event_headers WHERE (event_headers.kind = "31234") AND (event_headers.pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (event_headers.created_at >= "1764553447") AND ((event_headers.kind >= 30000 AND event_headers.kind < 40000))
|
||||
) AS filtered
|
||||
ON event_headers.row_id = filtered.row_id
|
||||
ORDER BY created_at DESC, id
|
||||
├── SEARCH event_headers USING COVERING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=? AND created_at>?)
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── USE TEMP B-TREE FOR ORDER BY
|
||||
WHERE (kind = "31234") AND (pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (created_at >= "1764553447")
|
||||
ORDER BY created_at DESC
|
||||
└── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=? AND created_at>?)
|
||||
""".trimIndent(),
|
||||
sql,
|
||||
dbAllIndexes.explain(filter),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testReplaceablesOfMultipleKindsDownloadByDateLimit() {
|
||||
val sql =
|
||||
explain(
|
||||
Filter(
|
||||
kinds = listOf(MetadataEvent.KIND, SearchRelayListEvent.KIND, AdvertisedRelayListEvent.KIND),
|
||||
authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
|
||||
limit = 20,
|
||||
since = 1764553447, // Nov 2025
|
||||
),
|
||||
val filter =
|
||||
Filter(
|
||||
kinds = listOf(MetadataEvent.KIND, SearchRelayListEvent.KIND, AdvertisedRelayListEvent.KIND),
|
||||
authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
|
||||
limit = 20,
|
||||
since = 1764553447, // Nov 2025
|
||||
)
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
SELECT event_headers.row_id as row_id FROM event_headers WHERE (event_headers.kind IN ("0", "10007", "10002")) AND (event_headers.pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (event_headers.created_at >= "1764553447") ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 20
|
||||
) AS filtered
|
||||
ON event_headers.row_id = filtered.row_id
|
||||
ORDER BY created_at DESC, id
|
||||
├── CO-ROUTINE filtered
|
||||
│ ├── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=? AND created_at>?)
|
||||
│ └── USE TEMP B-TREE FOR ORDER BY
|
||||
├── SCAN filtered
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
WHERE (kind IN ("0", "10007", "10002")) AND (pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (created_at >= "1764553447")
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 20
|
||||
├── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=? AND created_at>?)
|
||||
└── USE TEMP B-TREE FOR ORDER BY
|
||||
""".trimIndent(),
|
||||
sql,
|
||||
dbAllIndexes.explain(filter),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testReplaceablesOfMultipleKindsDownload() {
|
||||
val sql =
|
||||
explain(
|
||||
Filter(
|
||||
kinds = listOf(MetadataEvent.KIND, SearchRelayListEvent.KIND, AdvertisedRelayListEvent.KIND),
|
||||
authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
|
||||
),
|
||||
val filter =
|
||||
Filter(
|
||||
kinds = listOf(MetadataEvent.KIND, SearchRelayListEvent.KIND, AdvertisedRelayListEvent.KIND),
|
||||
authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
SELECT event_headers.row_id as row_id FROM event_headers WHERE (event_headers.kind IN ("0", "10007", "10002")) AND (event_headers.pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")
|
||||
) AS filtered
|
||||
ON event_headers.row_id = filtered.row_id
|
||||
ORDER BY created_at DESC, id
|
||||
├── SEARCH event_headers USING COVERING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=?)
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
WHERE (kind IN ("0", "10007", "10002")) AND (pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")
|
||||
ORDER BY created_at DESC
|
||||
├── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=?)
|
||||
└── USE TEMP B-TREE FOR ORDER BY
|
||||
""".trimIndent(),
|
||||
sql,
|
||||
dbAllIndexes.explain(filter),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testContactCardDownloadFromTrustedKeys() {
|
||||
val sql =
|
||||
explain(
|
||||
Filter(
|
||||
kinds = listOf(ContactCardEvent.KIND),
|
||||
authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
|
||||
tags =
|
||||
mapOf(
|
||||
"d" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
|
||||
),
|
||||
since = 1764553447, // Nov 2025
|
||||
),
|
||||
val filter =
|
||||
Filter(
|
||||
kinds = listOf(ContactCardEvent.KIND),
|
||||
authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
|
||||
tags =
|
||||
mapOf(
|
||||
"d" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
|
||||
),
|
||||
since = 1764553447, // Nov 2025
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
|
||||
INNER JOIN (
|
||||
SELECT event_headers.row_id as row_id FROM event_headers WHERE (event_headers.kind = "30382") AND (event_headers.pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (event_headers.d_tag = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (event_headers.created_at >= "1764553447") AND ((event_headers.kind >= 30000 AND event_headers.kind < 40000))
|
||||
) AS filtered
|
||||
ON event_headers.row_id = filtered.row_id
|
||||
ORDER BY created_at DESC, id
|
||||
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=?)
|
||||
├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
└── USE TEMP B-TREE FOR ORDER BY
|
||||
""".trimIndent(),
|
||||
sql,
|
||||
dbAllIndexes.explain(filter),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+103
-116
@@ -20,9 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
@@ -30,136 +28,125 @@ import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import junit.framework.TestCase
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.fail
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class ReplaceableTest {
|
||||
private lateinit var db: EventStore
|
||||
|
||||
class ReplaceableTest : BaseDBTest() {
|
||||
val signer = NostrSignerSync()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
db = EventStore(context, null)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testReplacing() {
|
||||
val time = TimeUtils.now()
|
||||
val version1 = signer.sign(MetadataEvent.createNew("Vitor 1", createdAt = time))
|
||||
val version2 = signer.sign(MetadataEvent.createNew("Vitor 2", createdAt = time + 1))
|
||||
val version3 = signer.sign(MetadataEvent.createNew("Vitor 3", createdAt = time + 2))
|
||||
fun testReplacing() =
|
||||
forEachDB { db ->
|
||||
val time = TimeUtils.now()
|
||||
val version1 = signer.sign(MetadataEvent.createNew("Vitor 1", createdAt = time))
|
||||
val version2 = signer.sign(MetadataEvent.createNew("Vitor 2", createdAt = time + 1))
|
||||
val version3 = signer.sign(MetadataEvent.createNew("Vitor 3", createdAt = time + 2))
|
||||
|
||||
val addressableQuery = Filter(kinds = listOf(version1.kind), authors = listOf(version1.pubKey), tags = mapOf("d" to listOf(version1.dTag())))
|
||||
val addressableQuery = Filter(kinds = listOf(version1.kind), authors = listOf(version1.pubKey), tags = mapOf("d" to listOf(version1.dTag())))
|
||||
|
||||
db.insert(version1)
|
||||
|
||||
db.assertQuery(version1, Filter(ids = listOf(version1.id)))
|
||||
db.assertQuery(version1, addressableQuery)
|
||||
|
||||
db.insert(version2)
|
||||
|
||||
db.assertQuery(null, Filter(ids = listOf(version1.id)))
|
||||
db.assertQuery(version2, Filter(ids = listOf(version2.id)))
|
||||
db.assertQuery(version2, addressableQuery)
|
||||
|
||||
db.insert(version3)
|
||||
|
||||
db.assertQuery(null, Filter(ids = listOf(version1.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(version2.id)))
|
||||
db.assertQuery(version3, Filter(ids = listOf(version3.id)))
|
||||
db.assertQuery(version3, addressableQuery)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBlockingOldVersions() {
|
||||
val time = TimeUtils.now()
|
||||
val version1 = signer.sign(MetadataEvent.createNew("Vitor 1", createdAt = time))
|
||||
val version2 = signer.sign(MetadataEvent.createNew("Vitor 2", createdAt = time + 1))
|
||||
val version3 = signer.sign(MetadataEvent.createNew("Vitor 3", createdAt = time + 2))
|
||||
|
||||
val addressableQuery = Filter(kinds = listOf(version1.kind), authors = listOf(version1.pubKey), tags = mapOf("d" to listOf(version1.dTag())))
|
||||
|
||||
db.insert(version3)
|
||||
|
||||
db.assertQuery(version3, Filter(ids = listOf(version3.id)))
|
||||
|
||||
try {
|
||||
db.insert(version2)
|
||||
fail("It should not allow inserting an older version")
|
||||
} catch (e: Exception) {
|
||||
TestCase.assertTrue(e is SQLiteConstraintException)
|
||||
}
|
||||
|
||||
try {
|
||||
db.insert(version1)
|
||||
fail("It should not allow inserting an older version")
|
||||
} catch (e: Exception) {
|
||||
TestCase.assertTrue(e is SQLiteConstraintException)
|
||||
|
||||
db.assertQuery(version1, Filter(ids = listOf(version1.id)))
|
||||
db.assertQuery(version1, addressableQuery)
|
||||
|
||||
db.insert(version2)
|
||||
|
||||
db.assertQuery(null, Filter(ids = listOf(version1.id)))
|
||||
db.assertQuery(version2, Filter(ids = listOf(version2.id)))
|
||||
db.assertQuery(version2, addressableQuery)
|
||||
|
||||
db.insert(version3)
|
||||
|
||||
db.assertQuery(null, Filter(ids = listOf(version1.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(version2.id)))
|
||||
db.assertQuery(version3, Filter(ids = listOf(version3.id)))
|
||||
db.assertQuery(version3, addressableQuery)
|
||||
}
|
||||
|
||||
db.assertQuery(version3, Filter(ids = listOf(version3.id)))
|
||||
db.assertQuery(version3, addressableQuery)
|
||||
db.assertQuery(null, Filter(ids = listOf(version2.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(version1.id)))
|
||||
}
|
||||
@Test
|
||||
fun testBlockingOldVersions() =
|
||||
forEachDB { db ->
|
||||
val time = TimeUtils.now()
|
||||
val version1 = signer.sign(MetadataEvent.createNew("Vitor 1", createdAt = time))
|
||||
val version2 = signer.sign(MetadataEvent.createNew("Vitor 2", createdAt = time + 1))
|
||||
val version3 = signer.sign(MetadataEvent.createNew("Vitor 3", createdAt = time + 2))
|
||||
|
||||
val addressableQuery = Filter(kinds = listOf(version1.kind), authors = listOf(version1.pubKey), tags = mapOf("d" to listOf(version1.dTag())))
|
||||
|
||||
db.insert(version3)
|
||||
|
||||
db.assertQuery(version3, Filter(ids = listOf(version3.id)))
|
||||
|
||||
try {
|
||||
db.insert(version2)
|
||||
fail("It should not allow inserting an older version")
|
||||
} catch (e: Exception) {
|
||||
TestCase.assertTrue(e is SQLiteConstraintException)
|
||||
}
|
||||
|
||||
try {
|
||||
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)))
|
||||
db.assertQuery(version3, addressableQuery)
|
||||
db.assertQuery(null, Filter(ids = listOf(version2.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(version1.id)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTriggersIndexUsageKind0() {
|
||||
val sql =
|
||||
"""
|
||||
SELECT * FROM event_headers
|
||||
WHERE
|
||||
event_headers.kind = 0 AND
|
||||
event_headers.pubkey = 'aa' AND
|
||||
event_headers.created_at < 1766686500
|
||||
""".trimIndent()
|
||||
fun testTriggersIndexUsageKind0() =
|
||||
forEachDB { db ->
|
||||
val sql =
|
||||
"""
|
||||
SELECT * FROM event_headers
|
||||
WHERE
|
||||
event_headers.kind = 0 AND
|
||||
event_headers.pubkey = 'aa' AND
|
||||
event_headers.created_at < 1766686500
|
||||
""".trimIndent()
|
||||
|
||||
val explainer = db.store.explainQuery(sql)
|
||||
val explainer = db.store.explainQuery(sql)
|
||||
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT * FROM event_headers
|
||||
WHERE
|
||||
event_headers.kind = 0 AND
|
||||
event_headers.pubkey = 'aa' AND
|
||||
event_headers.created_at < 1766686500
|
||||
└── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=? AND created_at<?)
|
||||
""".trimIndent(),
|
||||
explainer,
|
||||
)
|
||||
}
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT * FROM event_headers
|
||||
WHERE
|
||||
event_headers.kind = 0 AND
|
||||
event_headers.pubkey = 'aa' AND
|
||||
event_headers.created_at < 1766686500
|
||||
└── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=? AND created_at<?)
|
||||
""".trimIndent(),
|
||||
explainer,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTriggersIndexUsageKind3() {
|
||||
val sql =
|
||||
"""
|
||||
SELECT * FROM event_headers
|
||||
WHERE
|
||||
event_headers.kind = 3 AND
|
||||
event_headers.pubkey = 'aa' AND
|
||||
event_headers.created_at < 1766686500
|
||||
""".trimIndent()
|
||||
fun testTriggersIndexUsageKind3() =
|
||||
forEachDB { db ->
|
||||
val sql =
|
||||
"""
|
||||
SELECT * FROM event_headers
|
||||
WHERE
|
||||
event_headers.kind = 3 AND
|
||||
event_headers.pubkey = 'aa' AND
|
||||
event_headers.created_at < 1766686500
|
||||
""".trimIndent()
|
||||
|
||||
val explainer = db.store.explainQuery(sql)
|
||||
val explainer = db.store.explainQuery(sql)
|
||||
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT * FROM event_headers
|
||||
WHERE
|
||||
event_headers.kind = 3 AND
|
||||
event_headers.pubkey = 'aa' AND
|
||||
event_headers.created_at < 1766686500
|
||||
└── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=? AND created_at<?)
|
||||
""".trimIndent(),
|
||||
explainer,
|
||||
)
|
||||
}
|
||||
assertEquals(
|
||||
"""
|
||||
SELECT * FROM event_headers
|
||||
WHERE
|
||||
event_headers.kind = 3 AND
|
||||
event_headers.pubkey = 'aa' AND
|
||||
event_headers.created_at < 1766686500
|
||||
└── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=? AND created_at<?)
|
||||
""".trimIndent(),
|
||||
explainer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+74
-89
@@ -20,9 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
@@ -30,107 +28,94 @@ 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.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class RightToVanishTest {
|
||||
private lateinit var db: EventStore
|
||||
|
||||
class RightToVanishTest : BaseDBTest() {
|
||||
val signer = NostrSignerSync()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
db = EventStore(context, null, relayUrl = "testUrl")
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInsertDeleteEvent() {
|
||||
val time = TimeUtils.now()
|
||||
val note1 = signer.sign(TextNoteEvent.build("test1", createdAt = time))
|
||||
val note2 = signer.sign(TextNoteEvent.build("test2", createdAt = time + 1))
|
||||
val note3 = signer.sign(TextNoteEvent.build("test3", createdAt = time + 2))
|
||||
fun testInsertDeleteEvent() =
|
||||
forEachDB { db ->
|
||||
val time = TimeUtils.now()
|
||||
val note1 = signer.sign(TextNoteEvent.build("test1", createdAt = time))
|
||||
val note2 = signer.sign(TextNoteEvent.build("test2", createdAt = time + 1))
|
||||
val note3 = signer.sign(TextNoteEvent.build("test3", createdAt = time + 2))
|
||||
|
||||
db.insert(note1)
|
||||
db.insert(note2)
|
||||
db.insert(note3)
|
||||
|
||||
db.assertQuery(note1, Filter(ids = listOf(note1.id)))
|
||||
db.assertQuery(note2, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(note3, Filter(ids = listOf(note3.id)))
|
||||
|
||||
val vanish = signer.sign(RequestToVanishEvent.build("testUrl", createdAt = time + 2))
|
||||
|
||||
db.insert(vanish)
|
||||
|
||||
db.assertQuery(vanish, Filter(ids = listOf(vanish.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note1.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(note3, Filter(ids = listOf(note3.id)))
|
||||
|
||||
// trying to insert again should fail.
|
||||
try {
|
||||
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.insert(note2)
|
||||
db.insert(note3)
|
||||
|
||||
db.assertQuery(vanish, Filter(ids = listOf(vanish.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note1.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(note3, Filter(ids = listOf(note3.id)))
|
||||
}
|
||||
db.assertQuery(note1, Filter(ids = listOf(note1.id)))
|
||||
db.assertQuery(note2, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(note3, Filter(ids = listOf(note3.id)))
|
||||
|
||||
val vanish = signer.sign(RequestToVanishEvent.build("wss://quartz.local", createdAt = time + 2))
|
||||
|
||||
db.insert(vanish)
|
||||
|
||||
db.assertQuery(vanish, Filter(ids = listOf(vanish.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note1.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(note3, Filter(ids = listOf(note3.id)))
|
||||
|
||||
// trying to insert again should fail.
|
||||
try {
|
||||
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)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note1.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(note2.id)))
|
||||
db.assertQuery(note3, Filter(ids = listOf(note3.id)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInsertDeleteGiftWrap() {
|
||||
val time = TimeUtils.now()
|
||||
fun testInsertDeleteGiftWrap() =
|
||||
forEachDB { db ->
|
||||
val time = TimeUtils.now()
|
||||
|
||||
val me = NostrSignerSync()
|
||||
val myFriend = NostrSignerSync()
|
||||
val me = NostrSignerSync()
|
||||
val myFriend = NostrSignerSync()
|
||||
|
||||
val note1 = me.sign(TextNoteEvent.build("test1", createdAt = time))
|
||||
val wrap1 = GiftWrapEvent.create(note1, me.pubKey)
|
||||
val wrap2 = GiftWrapEvent.create(note1, myFriend.pubKey)
|
||||
val note1 = me.sign(TextNoteEvent.build("test1", createdAt = time))
|
||||
val wrap1 = GiftWrapEvent.create(note1, me.pubKey)
|
||||
val wrap2 = GiftWrapEvent.create(note1, myFriend.pubKey)
|
||||
|
||||
db.insert(wrap1)
|
||||
db.insert(wrap2)
|
||||
|
||||
db.assertQuery(wrap1, Filter(ids = listOf(wrap1.id)))
|
||||
db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id)))
|
||||
|
||||
val randomVanishToWrap = signer.sign(RequestToVanishEvent.build("testUrl", createdAt = time + 2))
|
||||
|
||||
db.insert(randomVanishToWrap)
|
||||
|
||||
db.assertQuery(wrap1, Filter(ids = listOf(wrap1.id)))
|
||||
db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id)))
|
||||
|
||||
val vanish = me.sign(RequestToVanishEvent.build("testUrl", createdAt = time + 2))
|
||||
|
||||
db.insert(vanish)
|
||||
|
||||
db.assertQuery(vanish, Filter(ids = listOf(vanish.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(wrap1.id)))
|
||||
db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id)))
|
||||
|
||||
// trying to insert again should fail.
|
||||
try {
|
||||
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.insert(wrap2)
|
||||
|
||||
db.assertQuery(vanish, Filter(ids = listOf(vanish.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(wrap1.id)))
|
||||
db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id)))
|
||||
}
|
||||
db.assertQuery(wrap1, Filter(ids = listOf(wrap1.id)))
|
||||
db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id)))
|
||||
|
||||
val randomVanishToWrap = signer.sign(RequestToVanishEvent.build("wss://quartz.local", createdAt = time + 2))
|
||||
|
||||
db.insert(randomVanishToWrap)
|
||||
|
||||
db.assertQuery(wrap1, Filter(ids = listOf(wrap1.id)))
|
||||
db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id)))
|
||||
|
||||
val vanish = me.sign(RequestToVanishEvent.build("wss://quartz.local", createdAt = time + 2))
|
||||
|
||||
db.insert(vanish)
|
||||
|
||||
db.assertQuery(vanish, Filter(ids = listOf(vanish.id)))
|
||||
db.assertQuery(null, Filter(ids = listOf(wrap1.id)))
|
||||
db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id)))
|
||||
|
||||
// trying to insert again should fail.
|
||||
try {
|
||||
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)))
|
||||
db.assertQuery(null, Filter(ids = listOf(wrap1.id)))
|
||||
db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id)))
|
||||
}
|
||||
}
|
||||
|
||||
+17
-33
@@ -20,20 +20,14 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
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.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class SearchTest {
|
||||
private lateinit var db: SQLiteEventStore
|
||||
|
||||
companion object Companion {
|
||||
class SearchTest : BaseDBTest() {
|
||||
companion object {
|
||||
val profile =
|
||||
MetadataEvent(
|
||||
id = "490d7439e530423f2540d4f2bdb73a0a2935f3df9e1f2a6f699a140c7db311fe",
|
||||
@@ -74,35 +68,25 @@ class SearchTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
db = SQLiteEventStore(context, null)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTagWithSearch() {
|
||||
db.insertEvent(comment)
|
||||
db.insertEvent(profile)
|
||||
fun testTagWithSearch() =
|
||||
forEachDB { db ->
|
||||
db.store.insertEvent(comment)
|
||||
db.store.insertEvent(profile)
|
||||
|
||||
db.assertQuery(null, Filter(search = "testing1"))
|
||||
db.assertQuery(comment, Filter(search = "testing"))
|
||||
db.assertQuery(comment, Filter(kinds = listOf(CommentEvent.KIND), search = "testing"))
|
||||
db.assertQuery(null, Filter(kinds = listOf(TextNoteEvent.KIND), search = "testing"))
|
||||
db.assertQuery(null, Filter(search = "testing1"))
|
||||
db.assertQuery(comment, Filter(search = "testing"))
|
||||
db.assertQuery(comment, Filter(kinds = listOf(CommentEvent.KIND), search = "testing"))
|
||||
db.assertQuery(null, Filter(kinds = listOf(TextNoteEvent.KIND), search = "testing"))
|
||||
|
||||
db.delete(comment.id)
|
||||
db.store.delete(comment.id)
|
||||
|
||||
db.assertQuery(null, Filter(search = "testing"))
|
||||
db.assertQuery(null, Filter(kinds = listOf(CommentEvent.KIND), search = "testing"))
|
||||
db.assertQuery(null, Filter(search = "testing"))
|
||||
db.assertQuery(null, Filter(kinds = listOf(CommentEvent.KIND), search = "testing"))
|
||||
|
||||
db.insertEvent(comment)
|
||||
db.store.insertEvent(comment)
|
||||
|
||||
db.assertQuery(comment, Filter(search = "testing"))
|
||||
db.assertQuery(comment, Filter(kinds = listOf(CommentEvent.KIND), search = "testing"))
|
||||
}
|
||||
db.assertQuery(comment, Filter(search = "testing"))
|
||||
db.assertQuery(comment, Filter(kinds = listOf(CommentEvent.KIND), search = "testing"))
|
||||
}
|
||||
}
|
||||
|
||||
+27
-7
@@ -29,7 +29,7 @@ import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||
|
||||
class EventIndexesModule(
|
||||
val hasher: (db: SQLiteDatabase) -> TagNameValueHasher,
|
||||
val tagIndexStrategy: IndexingStrategy = DefaultIndexingStrategy(),
|
||||
val indexStrategy: IndexingStrategy = DefaultIndexingStrategy(),
|
||||
) : IModule {
|
||||
override fun create(db: SQLiteDatabase) {
|
||||
db.execSQL(
|
||||
@@ -67,18 +67,38 @@ class EventIndexesModule(
|
||||
// queries by ID (load events)
|
||||
db.execSQL("CREATE UNIQUE INDEX event_headers_id ON event_headers (id)")
|
||||
|
||||
// queries by limit (latest records), since, until (sync all) alone
|
||||
db.execSQL("CREATE INDEX query_by_created_at_id ON event_headers (created_at DESC, id)")
|
||||
val orderBy =
|
||||
if (indexStrategy.useAndIndexIdOnOrderBy) {
|
||||
"created_at DESC, id ASC"
|
||||
} else {
|
||||
"created_at DESC"
|
||||
}
|
||||
|
||||
// queries by limit (latest records), since, until (sync all) alone without any filter by kind.. rare
|
||||
// serves as fall back for the lack of query_by_tags_hash index by default
|
||||
db.execSQL("CREATE INDEX query_by_created_at_id ON event_headers ($orderBy)")
|
||||
|
||||
// queries by kind only, mostly used in Global Feeds when author is not important.
|
||||
db.execSQL("CREATE INDEX query_by_kind_created ON event_headers (kind, $orderBy)")
|
||||
|
||||
// queries by kind + pubkey, but not d-tag, even if they are replaceables and addressables, by date.
|
||||
db.execSQL("CREATE INDEX query_by_kind_pubkey_created ON event_headers (kind, pubkey, created_at DESC)")
|
||||
db.execSQL("CREATE INDEX query_by_kind_pubkey_created ON event_headers (kind, pubkey, $orderBy)")
|
||||
|
||||
// makes deletions on the event_header fast
|
||||
db.execSQL("CREATE INDEX fk_event_tags_header_id ON event_tags (event_header_row_id)")
|
||||
|
||||
// This is a very slow index to build (80% of the insert time goes here) but it is extremely effective.
|
||||
db.execSQL("CREATE INDEX query_by_tags_hash ON event_tags (tag_hash, created_at DESC)")
|
||||
// ---------------------------------------------------------------
|
||||
// This are a very slow indexes (80% of the insert time goes here)
|
||||
// ---------------------------------------------------------------
|
||||
if (indexStrategy.indexTagQueriesWithoutKinds) {
|
||||
// First one is only needed if the user is searching by tags without a kind.
|
||||
db.execSQL("CREATE INDEX query_by_tags_hash ON event_tags (tag_hash, created_at DESC)")
|
||||
}
|
||||
|
||||
// This is the default index for most clients: tags by specific kinds that are supported by the client.
|
||||
db.execSQL("CREATE INDEX query_by_tags_hash_kind ON event_tags (tag_hash, kind, created_at DESC)")
|
||||
|
||||
// this one is to allow search of tags by kind and author at the same time: DMs, reports,
|
||||
db.execSQL("CREATE INDEX query_by_tags_hash_kind_pubkey ON event_tags (tag_hash, kind, pubkey_hash, created_at DESC)")
|
||||
|
||||
// Prevent updates to maintain immutability
|
||||
@@ -173,7 +193,7 @@ class EventIndexesModule(
|
||||
// rebalancing the tree every new insert
|
||||
val indexableTags = ArrayList<Long>()
|
||||
for (idx in event.tags.indices) {
|
||||
if (tagIndexStrategy.shouldIndex(event.kind, event.tags[idx])) {
|
||||
if (indexStrategy.shouldIndex(event.kind, event.tags[idx])) {
|
||||
indexableTags.add(hasher.hash(event.tags[idx][0], event.tags[idx][1]))
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -29,9 +29,9 @@ class EventStore(
|
||||
context: Context,
|
||||
dbName: String? = "events.db",
|
||||
val relayUrl: String? = "wss://quartz.local",
|
||||
val tagIndexStrategy: IndexingStrategy = DefaultIndexingStrategy(),
|
||||
val indexStrategy: IndexingStrategy = DefaultIndexingStrategy(),
|
||||
) : IEventStore {
|
||||
val store = SQLiteEventStore(context, dbName, relayUrl, tagIndexStrategy)
|
||||
val store = SQLiteEventStore(context, dbName, relayUrl, indexStrategy)
|
||||
|
||||
override fun insert(event: Event) = store.insertEvent(event)
|
||||
|
||||
|
||||
+7
-1
@@ -23,6 +23,9 @@ package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Tag
|
||||
|
||||
interface IndexingStrategy {
|
||||
val indexTagQueriesWithoutKinds: Boolean
|
||||
val useAndIndexIdOnOrderBy: Boolean
|
||||
|
||||
fun shouldIndex(
|
||||
kind: Int,
|
||||
tag: Tag,
|
||||
@@ -32,7 +35,10 @@ interface IndexingStrategy {
|
||||
/**
|
||||
* By default, we index all tags that have a single letter name and some value
|
||||
*/
|
||||
class DefaultIndexingStrategy : IndexingStrategy {
|
||||
class DefaultIndexingStrategy(
|
||||
override val indexTagQueriesWithoutKinds: Boolean = false,
|
||||
override val useAndIndexIdOnOrderBy: Boolean = false,
|
||||
) : IndexingStrategy {
|
||||
override fun shouldIndex(
|
||||
kind: Int,
|
||||
tag: Tag,
|
||||
|
||||
+214
-24
@@ -23,6 +23,8 @@ package com.vitorpamplona.quartz.nip01Core.store.sqlite
|
||||
import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
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
|
||||
@@ -34,6 +36,7 @@ import kotlin.collections.component2
|
||||
class QueryBuilder(
|
||||
val fts: FullTextSearchModule,
|
||||
val hasher: (db: SQLiteDatabase) -> TagNameValueHasher,
|
||||
val indexStrategy: IndexingStrategy,
|
||||
) {
|
||||
// ------------
|
||||
// Main methods
|
||||
@@ -110,13 +113,37 @@ class QueryBuilder(
|
||||
filter: Filter,
|
||||
hasher: TagNameValueHasher,
|
||||
): QuerySpec {
|
||||
val newFilter = filter.toFilterWithDTags()
|
||||
|
||||
if (newFilter.isSimpleQuery()) {
|
||||
return makeSimpleQuery(
|
||||
ids = newFilter.ids,
|
||||
authors = newFilter.authors,
|
||||
kinds = newFilter.kinds,
|
||||
dTags = newFilter.dTags,
|
||||
since = newFilter.since,
|
||||
until = newFilter.until,
|
||||
limit = newFilter.limit,
|
||||
)
|
||||
}
|
||||
|
||||
if (newFilter.isSimpleSearch()) {
|
||||
return makeSimpleSearch(
|
||||
search = newFilter.search!!,
|
||||
ids = newFilter.ids,
|
||||
authors = newFilter.authors,
|
||||
kinds = newFilter.kinds,
|
||||
dTags = newFilter.dTags,
|
||||
since = newFilter.since,
|
||||
until = newFilter.until,
|
||||
limit = newFilter.limit,
|
||||
)
|
||||
}
|
||||
|
||||
val rowIdSubqueries = prepareRowIDSubQueries(filter, hasher)
|
||||
|
||||
return if (rowIdSubqueries == null) {
|
||||
QuerySpec(
|
||||
makeEverythingQuery(),
|
||||
emptyList(),
|
||||
)
|
||||
QuerySpec(makeEverythingQuery())
|
||||
} else {
|
||||
QuerySpec(
|
||||
makeQueryIn(rowIdSubqueries.sql),
|
||||
@@ -129,6 +156,8 @@ class QueryBuilder(
|
||||
filters: List<Filter>,
|
||||
hasher: TagNameValueHasher,
|
||||
): QuerySpec {
|
||||
if (filters.size == 1) return toSql(filters.first(), hasher)
|
||||
|
||||
val rowIdSubqueries = unionSubqueriesIfNeeded(filters, hasher)
|
||||
|
||||
return if (rowIdSubqueries == null) {
|
||||
@@ -144,7 +173,7 @@ class QueryBuilder(
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeEverythingQuery() = "SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers ORDER BY created_at DESC, id"
|
||||
private fun makeEverythingQuery() = "SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers ORDER BY created_at DESC${if (indexStrategy.useAndIndexIdOnOrderBy) ", id ASC" else ""}"
|
||||
|
||||
private fun makeQueryIn(rowIdQuery: String) =
|
||||
"""
|
||||
@@ -153,7 +182,7 @@ class QueryBuilder(
|
||||
$rowIdQuery
|
||||
) AS filtered
|
||||
ON event_headers.row_id = filtered.row_id
|
||||
ORDER BY created_at DESC, id
|
||||
ORDER BY created_at DESC${if (indexStrategy.useAndIndexIdOnOrderBy) ", id ASC" else ""}
|
||||
""".trimIndent()
|
||||
|
||||
private fun <T : Event> SQLiteDatabase.runQuery(query: QuerySpec): List<T> =
|
||||
@@ -359,12 +388,12 @@ class QueryBuilder(
|
||||
buildString {
|
||||
// always do tags if there are any
|
||||
if (reverseLookup) {
|
||||
append("SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags ")
|
||||
append("SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags")
|
||||
|
||||
// it's quite rare to have 2 tags in the filter, but possible
|
||||
nonDTagsIn.keys.forEachIndexed { index, tagName ->
|
||||
if (defaultTagKey != null) {
|
||||
append("INNER JOIN event_tags as event_tagsIn$index ON event_tagsIn$index.event_header_row_id = event_tags.event_header_row_id AND event_tagsIn$index.created_at = event_tags.created_at ")
|
||||
append(" INNER JOIN event_tags as event_tagsIn$index ON event_tagsIn$index.event_header_row_id = event_tags.event_header_row_id AND event_tagsIn$index.created_at = event_tags.created_at")
|
||||
} else {
|
||||
defaultTagKey = TagNameForQuery.InTags(tagName)
|
||||
}
|
||||
@@ -373,7 +402,7 @@ class QueryBuilder(
|
||||
nonDTagsAll.keys.forEachIndexed { index, tagName ->
|
||||
nonDTagsAll[tagName]!!.forEachIndexed { valueIndex, tagValue ->
|
||||
if (defaultTagKey != null) {
|
||||
append("INNER JOIN event_tags as event_tagsAll${index}_$valueIndex ON event_tagsAll${index}_$valueIndex.event_header_row_id = event_tags.event_header_row_id AND event_tagsAll${index}_$valueIndex.created_at = event_tags.created_at ")
|
||||
append(" INNER JOIN event_tags as event_tagsAll${index}_$valueIndex ON event_tagsAll${index}_$valueIndex.event_header_row_id = event_tags.event_header_row_id AND event_tagsAll${index}_$valueIndex.created_at = event_tags.created_at")
|
||||
} else {
|
||||
defaultTagKey = TagNameForQuery.AllTags(tagName, valueIndex)
|
||||
}
|
||||
@@ -381,21 +410,21 @@ class QueryBuilder(
|
||||
}
|
||||
|
||||
if (needHeaders) {
|
||||
append("INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id ")
|
||||
append(" INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id")
|
||||
}
|
||||
|
||||
if (mustJoinSearch) {
|
||||
append("INNER JOIN ${fts.tableName} ON ${fts.tableName}.${fts.eventHeaderRowIdName} = event_tags.event_header_row_id ")
|
||||
append(" INNER JOIN ${fts.tableName} ON ${fts.tableName}.${fts.eventHeaderRowIdName} = event_tags.event_header_row_id")
|
||||
}
|
||||
} else if (mustJoinSearch) {
|
||||
append("SELECT ${fts.tableName}.${fts.eventHeaderRowIdName} as row_id FROM ${fts.tableName} ")
|
||||
append("SELECT ${fts.tableName}.${fts.eventHeaderRowIdName} as row_id FROM ${fts.tableName}")
|
||||
|
||||
if (hasHeaders) {
|
||||
append("INNER JOIN event_headers ON event_headers.row_id = ${fts.tableName}.${fts.eventHeaderRowIdName}")
|
||||
append(" INNER JOIN event_headers ON event_headers.row_id = ${fts.tableName}.${fts.eventHeaderRowIdName}")
|
||||
}
|
||||
} else {
|
||||
// no tags and no search.
|
||||
append("SELECT event_headers.row_id as row_id FROM event_headers ")
|
||||
append("SELECT event_headers.row_id as row_id FROM event_headers")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,22 +509,183 @@ class QueryBuilder(
|
||||
}
|
||||
}
|
||||
|
||||
val whereClause =
|
||||
if (filter.limit != null) {
|
||||
if (reverseLookup) {
|
||||
"${clause.conditions} ORDER BY event_tags.created_at DESC LIMIT ${filter.limit}"
|
||||
} else {
|
||||
"${clause.conditions} ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT ${filter.limit}"
|
||||
val sql =
|
||||
buildString {
|
||||
append(projection)
|
||||
if (clause.conditions.isNotEmpty()) {
|
||||
append(" WHERE ${clause.conditions}")
|
||||
}
|
||||
if (filter.limit != null) {
|
||||
if (reverseLookup) {
|
||||
append(" ORDER BY event_tags.created_at DESC")
|
||||
append(" LIMIT ")
|
||||
append(filter.limit)
|
||||
} else {
|
||||
append(" ORDER BY event_headers.created_at DESC")
|
||||
append(" LIMIT ")
|
||||
append(filter.limit)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
clause.conditions
|
||||
}
|
||||
|
||||
return QuerySpec("$projection WHERE $whereClause", clause.args)
|
||||
return QuerySpec(sql, clause.args)
|
||||
}
|
||||
|
||||
private fun makeSimpleSearch(
|
||||
search: String,
|
||||
ids: List<HexKey>? = null,
|
||||
authors: List<HexKey>? = null,
|
||||
kinds: List<Kind>? = null,
|
||||
dTags: List<String>? = null,
|
||||
since: Long? = null,
|
||||
until: Long? = null,
|
||||
limit: Int? = null,
|
||||
): QuerySpec {
|
||||
val clause =
|
||||
where {
|
||||
// the order should match indexes
|
||||
// ids reduce the filter the most
|
||||
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) }
|
||||
|
||||
// there are indexes for these, starting with tags.
|
||||
dTags?.let { equalsOrIn("event_headers.d_tag", 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
|
||||
if (dTags != null && kinds != null) {
|
||||
if (kinds.all { it.isAddressable() }) {
|
||||
// matches unique index kind >= 30000 AND kind < 40000
|
||||
raw("(event_headers.kind >= 30000 AND kind < 40000)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val sql =
|
||||
buildString {
|
||||
append("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")
|
||||
append("\nINNER JOIN ${fts.tableName} ON event_headers.row_id = ${fts.tableName}.${fts.eventHeaderRowIdName}")
|
||||
if (clause.conditions.isNotEmpty()) {
|
||||
append("\nWHERE ${clause.conditions}")
|
||||
}
|
||||
append("\nORDER BY event_headers.created_at DESC")
|
||||
if (indexStrategy.useAndIndexIdOnOrderBy) {
|
||||
append(", event_headers.id ASC")
|
||||
}
|
||||
if (limit != null) {
|
||||
append("\nLIMIT ")
|
||||
append(limit)
|
||||
}
|
||||
}
|
||||
|
||||
println(sql)
|
||||
|
||||
return QuerySpec(sql, clause.args)
|
||||
}
|
||||
|
||||
private fun makeSimpleQuery(
|
||||
ids: List<HexKey>? = null,
|
||||
authors: List<HexKey>? = null,
|
||||
kinds: List<Kind>? = null,
|
||||
dTags: List<String>? = null,
|
||||
since: Long? = null,
|
||||
until: Long? = null,
|
||||
limit: Int? = null,
|
||||
): QuerySpec {
|
||||
val clause =
|
||||
where {
|
||||
// the order should match indexes
|
||||
// ids reduce the filter the most
|
||||
ids?.let { equalsOrIn("id", 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) }
|
||||
|
||||
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
|
||||
if (dTags != null && kinds != null) {
|
||||
if (kinds.all { it.isAddressable() }) {
|
||||
// matches unique index kind >= 30000 AND kind < 40000
|
||||
raw("(kind >= 30000 AND kind < 40000)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val sql =
|
||||
buildString {
|
||||
append("SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers")
|
||||
if (clause.conditions.isNotEmpty()) {
|
||||
append("\nWHERE ")
|
||||
append(clause.conditions)
|
||||
}
|
||||
append("\nORDER BY created_at DESC")
|
||||
if (indexStrategy.useAndIndexIdOnOrderBy) {
|
||||
append(", event_headers.id ASC")
|
||||
}
|
||||
if (limit != null) {
|
||||
append("\nLIMIT ")
|
||||
append(limit)
|
||||
}
|
||||
}
|
||||
|
||||
println(sql)
|
||||
|
||||
return QuerySpec(sql, clause.args)
|
||||
}
|
||||
|
||||
class FilterWithDTags(
|
||||
val ids: List<HexKey>? = null,
|
||||
val authors: List<HexKey>? = null,
|
||||
val kinds: List<Kind>? = null,
|
||||
val dTags: List<String>? = null,
|
||||
val nonDTagsIn: Map<String, List<String>>? = null,
|
||||
val nonDTagsAll: Map<String, List<String>>? = null,
|
||||
val since: Long? = null,
|
||||
val until: Long? = null,
|
||||
val limit: Int? = null,
|
||||
val search: String? = null,
|
||||
) {
|
||||
fun isSimpleSearch() =
|
||||
search != null && search.isNotEmpty() &&
|
||||
(nonDTagsIn == null || nonDTagsIn.isEmpty()) &&
|
||||
(nonDTagsAll == null || nonDTagsAll.isEmpty())
|
||||
|
||||
// can be resolved with just event_headers
|
||||
fun isSimpleQuery() =
|
||||
(nonDTagsIn == null || nonDTagsIn.isEmpty()) &&
|
||||
(nonDTagsAll == null || nonDTagsAll.isEmpty()) &&
|
||||
(search == null || search.isEmpty())
|
||||
}
|
||||
|
||||
fun Filter.toFilterWithDTags(): FilterWithDTags =
|
||||
FilterWithDTags(
|
||||
ids = ids,
|
||||
authors = authors,
|
||||
kinds = kinds,
|
||||
dTags = tags?.get("d") ?: tagsAll?.get("d"),
|
||||
nonDTagsIn = tags?.filter { it.key != "d" }?.ifEmpty { null },
|
||||
nonDTagsAll = tagsAll?.filter { it.key != "d" }?.ifEmpty { null },
|
||||
since = since,
|
||||
until = until,
|
||||
limit = limit,
|
||||
search = search,
|
||||
)
|
||||
|
||||
data class QuerySpec(
|
||||
val sql: String,
|
||||
val args: List<String>,
|
||||
val args: List<String> = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
+3
-3
@@ -41,7 +41,7 @@ class SQLiteEventStore(
|
||||
val context: Context,
|
||||
val dbName: String? = "events.db",
|
||||
val relayUrl: String? = null,
|
||||
val tagIndexStrategy: IndexingStrategy = DefaultIndexingStrategy(),
|
||||
val indexStrategy: IndexingStrategy = DefaultIndexingStrategy(),
|
||||
) : SQLiteOpenHelper(context, dbName, null, DATABASE_VERSION) {
|
||||
companion object {
|
||||
const val DATABASE_VERSION = 2
|
||||
@@ -50,7 +50,7 @@ class SQLiteEventStore(
|
||||
val seedModule = SeedModule()
|
||||
|
||||
val fullTextSearchModule = FullTextSearchModule()
|
||||
val eventIndexModule = EventIndexesModule(seedModule::hasher, tagIndexStrategy)
|
||||
val eventIndexModule = EventIndexesModule(seedModule::hasher, indexStrategy)
|
||||
|
||||
val replaceableModule = ReplaceableModule()
|
||||
val addressableModule = AddressableModule()
|
||||
@@ -60,7 +60,7 @@ class SQLiteEventStore(
|
||||
val expirationModule = ExpirationModule()
|
||||
val rightToVanishModule = RightToVanishModule(seedModule::hasher)
|
||||
|
||||
val queryBuilder = QueryBuilder(fullTextSearchModule, seedModule::hasher)
|
||||
val queryBuilder = QueryBuilder(fullTextSearchModule, seedModule::hasher, indexStrategy)
|
||||
|
||||
val modules =
|
||||
listOf(
|
||||
|
||||
+2
@@ -85,4 +85,6 @@ sealed class Condition {
|
||||
data class Or(
|
||||
val conditions: List<Condition>,
|
||||
) : Condition()
|
||||
|
||||
class Empty : Condition()
|
||||
}
|
||||
|
||||
+3
@@ -38,6 +38,9 @@ class SqlSelectionBuilder(
|
||||
*/
|
||||
private fun buildCondition(cond: Condition): String =
|
||||
when (cond) {
|
||||
is Condition.Empty -> {
|
||||
""
|
||||
}
|
||||
is Condition.Raw -> {
|
||||
cond.condition
|
||||
}
|
||||
|
||||
+1
-1
@@ -119,7 +119,7 @@ class WhereClauseBuilder {
|
||||
}
|
||||
|
||||
fun where(block: WhereClauseBuilder.() -> Unit): WhereClause {
|
||||
val condition = WhereClauseBuilder().apply(block).buildAnd() ?: Condition.And(emptyList())
|
||||
val condition = WhereClauseBuilder().apply(block).buildAnd() ?: Condition.Empty()
|
||||
return SqlSelectionBuilder(condition).build()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user