reintroduce strip accents as custom collation does not work with LIKE

This commit is contained in:
Gauthier Roebroeck 2026-02-12 15:56:26 +08:00
parent bac4e7c524
commit 293ac415fa
5 changed files with 72 additions and 20 deletions

View file

@ -3,6 +3,7 @@ package org.gotson.komga.infrastructure.datasource
import com.ibm.icu.text.Collator
import io.github.oshai.kotlinlogging.KotlinLogging
import org.gotson.komga.infrastructure.unicode.Collators
import org.gotson.komga.language.stripAccents
import org.sqlite.Collation
import org.sqlite.Function
import org.sqlite.SQLiteConnection
@ -13,6 +14,7 @@ private val log = KotlinLogging.logger {}
class SqliteUdfDataSource : SQLiteDataSource() {
companion object {
const val UDF_STRIP_ACCENTS = "UDF_STRIP_ACCENTS"
const val COLLATION_UNICODE_1 = "COLLATION_UNICODE_1"
const val COLLATION_UNICODE_3 = "COLLATION_UNICODE_3"
}
@ -26,6 +28,7 @@ class SqliteUdfDataSource : SQLiteDataSource() {
private fun addAllUdf(connection: SQLiteConnection) {
createUdfRegexp(connection)
createUdfStripAccents(connection)
createUnicodeCollation(connection, COLLATION_UNICODE_3, Collators.collator3)
createUnicodeCollation(connection, COLLATION_UNICODE_1, Collators.collator1)
}
@ -46,6 +49,21 @@ class SqliteUdfDataSource : SQLiteDataSource() {
)
}
private fun createUdfStripAccents(connection: SQLiteConnection) {
log.debug { "Adding custom $UDF_STRIP_ACCENTS function" }
Function.create(
connection,
UDF_STRIP_ACCENTS,
object : Function() {
override fun xFunc() =
when (val text = value_text(0)) {
null -> error("Argument must not be null")
else -> result(text.stripAccents())
}
},
)
}
private fun createUnicodeCollation(
connection: SQLiteConnection,
collationName: String,

View file

@ -146,7 +146,11 @@ class BookSearchHelper(
DSL
.select(Tables.BOOK_METADATA_TAG.BOOK_ID)
.from(Tables.BOOK_METADATA_TAG)
.where(Tables.BOOK_METADATA_TAG.TAG.unicode1().equal(tag))
.where(
Tables.BOOK_METADATA_TAG.TAG
.unicode1()
.equal(tag),
)
}
val innerAny = {
DSL
@ -170,8 +174,21 @@ class BookSearchHelper(
.select(Tables.BOOK_METADATA_AUTHOR.BOOK_ID)
.from(Tables.BOOK_METADATA_AUTHOR)
.where(DSL.noCondition())
.apply { if (name != null) and(Tables.BOOK_METADATA_AUTHOR.NAME.unicode1().equal(name)) }
.apply { if (role != null) and(Tables.BOOK_METADATA_AUTHOR.ROLE.unicode1().equal(role)) }
.apply {
if (name != null)
and(
Tables.BOOK_METADATA_AUTHOR.NAME
.unicode1()
.equal(name),
)
}.apply {
if (role != null)
and(
Tables.BOOK_METADATA_AUTHOR.ROLE
.unicode1()
.equal(role),
)
}
}
when (searchCondition.operator) {
is SearchOperator.Is -> {

View file

@ -6,7 +6,6 @@ import org.gotson.komga.domain.model.SearchCondition
import org.gotson.komga.domain.model.SearchContext
import org.gotson.komga.domain.model.SearchOperator
import org.gotson.komga.domain.model.SeriesMetadata
import org.gotson.komga.infrastructure.datasource.SqliteUdfDataSource
import org.gotson.komga.jooq.main.Tables
import org.jooq.Condition
import org.jooq.impl.DSL
@ -101,12 +100,19 @@ class SeriesSearchHelper(
DSL
.select(Tables.SERIES_METADATA_TAG.SERIES_ID)
.from(Tables.SERIES_METADATA_TAG)
.where(Tables.SERIES_METADATA_TAG.TAG.unicode1().equal(tag))
.union(
.where(
Tables.SERIES_METADATA_TAG.TAG
.unicode1()
.equal(tag),
).union(
DSL
.select(Tables.BOOK_METADATA_AGGREGATION_TAG.SERIES_ID)
.from(Tables.BOOK_METADATA_AGGREGATION_TAG)
.where(Tables.BOOK_METADATA_AGGREGATION_TAG.TAG.unicode1().equal(tag)),
.where(
Tables.BOOK_METADATA_AGGREGATION_TAG.TAG
.unicode1()
.equal(tag),
),
)
}
val innerAny = {
@ -141,17 +147,15 @@ class SeriesSearchHelper(
if (name != null)
and(
Tables.BOOK_METADATA_AGGREGATION_AUTHOR.NAME
.collate(
SqliteUdfDataSource.COLLATION_UNICODE_3,
).equalIgnoreCase(name),
.unicode1()
.equal(name),
)
}.apply {
if (role != null)
and(
Tables.BOOK_METADATA_AGGREGATION_AUTHOR.ROLE
.collate(
SqliteUdfDataSource.COLLATION_UNICODE_3,
).equalIgnoreCase(role),
.unicode1()
.equal(role),
)
}
}
@ -209,7 +213,11 @@ class SeriesSearchHelper(
DSL
.select(Tables.SERIES_METADATA_GENRE.SERIES_ID)
.from(Tables.SERIES_METADATA_GENRE)
.where(Tables.SERIES_METADATA_GENRE.GENRE.unicode1().equal(genre))
.where(
Tables.SERIES_METADATA_GENRE.GENRE
.unicode1()
.equal(genre),
)
}
val innerAny = {
DSL
@ -236,7 +244,11 @@ class SeriesSearchHelper(
DSL
.select(Tables.SERIES_METADATA_SHARING.SERIES_ID)
.from(Tables.SERIES_METADATA_SHARING)
.where(Tables.SERIES_METADATA_SHARING.LABEL.unicode1().equal(label))
.where(
Tables.SERIES_METADATA_SHARING.LABEL
.unicode1()
.equal(label),
)
}
val innerAny = {
DSL

View file

@ -17,10 +17,15 @@ import java.util.zip.GZIPOutputStream
fun Field<String>.noCase() = this.collate("NOCASE")
/**
* Warning: SQLite doesn't use collations with LIKE
*/
fun Field<String>.unicode1() = this.collate(SqliteUdfDataSource.COLLATION_UNICODE_1)
fun Field<String>.unicode3() = this.collate(SqliteUdfDataSource.COLLATION_UNICODE_3)
fun Field<String>.udfStripAccents() = DSL.function(SqliteUdfDataSource.UDF_STRIP_ACCENTS, String::class.java, this)
fun Sort.toOrderBy(sorts: Map<String, Field<out Any>>): List<SortField<out Any>> =
this.mapNotNull {
it.toSortField(sorts)

View file

@ -49,7 +49,7 @@ class ReferentialDao(
.selectDistinct(a.NAME, a.ROLE)
.from(a)
.apply { filterOnLibraryIds?.let { leftJoin(b).on(a.BOOK_ID.eq(b.ID)) } }
.where(a.NAME.unicode1().contains(search))
.where(a.NAME.udfStripAccents().contains(search.stripAccents()))
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
.orderBy(a.NAME.unicode3())
.fetchInto(a)
@ -65,7 +65,7 @@ class ReferentialDao(
.from(bmaa)
.leftJoin(s)
.on(bmaa.SERIES_ID.eq(s.ID))
.where(bmaa.NAME.unicode1().contains(search))
.where(bmaa.NAME.udfStripAccents().contains(search.stripAccents()))
.and(s.LIBRARY_ID.eq(libraryId))
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
.orderBy(bmaa.NAME.unicode3())
@ -83,7 +83,7 @@ class ReferentialDao(
.leftJoin(cs)
.on(bmaa.SERIES_ID.eq(cs.SERIES_ID))
.apply { filterOnLibraryIds?.let { leftJoin(s).on(bmaa.SERIES_ID.eq(s.ID)) } }
.where(bmaa.NAME.unicode1().contains(search))
.where(bmaa.NAME.udfStripAccents().contains(search.stripAccents()))
.and(cs.COLLECTION_ID.eq(collectionId))
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
.orderBy(bmaa.NAME.unicode3())
@ -99,7 +99,7 @@ class ReferentialDao(
.selectDistinct(bmaa.NAME, bmaa.ROLE)
.from(bmaa)
.apply { filterOnLibraryIds?.let { leftJoin(s).on(bmaa.SERIES_ID.eq(s.ID)) } }
.where(bmaa.NAME.unicode1().contains(search))
.where(bmaa.NAME.udfStripAccents().contains(search.stripAccents()))
.and(bmaa.SERIES_ID.eq(seriesId))
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
.orderBy(bmaa.NAME.unicode3())
@ -220,7 +220,7 @@ class ReferentialDao(
.selectDistinct(a.NAME)
.from(a)
.apply { filterOnLibraryIds?.let { leftJoin(b).on(a.BOOK_ID.eq(b.ID)) } }
.where(a.NAME.unicode1().contains(search))
.where(a.NAME.udfStripAccents().contains(search.stripAccents()))
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
.orderBy(a.NAME.unicode3())
.fetch(a.NAME)