diff --git a/komga/build.gradle.kts b/komga/build.gradle.kts index a9d8c6484..6c84eee68 100644 --- a/komga/build.gradle.kts +++ b/komga/build.gradle.kts @@ -91,11 +91,8 @@ dependencies { implementation("com.github.ben-manes.caffeine:caffeine:2.9.0") -// While waiting for https://github.com/xerial/sqlite-jdbc/pull/491 and https://github.com/xerial/sqlite-jdbc/pull/494 -// runtimeOnly("org.xerial:sqlite-jdbc:3.32.3.2") -// jooqGenerator("org.xerial:sqlite-jdbc:3.32.3.2") - implementation("com.github.gotson:sqlite-jdbc:3.32.3.8") - jooqGenerator("com.github.gotson:sqlite-jdbc:3.32.3.8") + implementation("org.xerial:sqlite-jdbc:3.36.0.3") + jooqGenerator("org.xerial:sqlite-jdbc:3.36.0.3") testImplementation("org.springframework.boot:spring-boot-starter-test") { exclude(module = "mockito-core") diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/datasource/SqliteUdfDataSource.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/datasource/SqliteUdfDataSource.kt index 00c53f4aa..ce11a9a69 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/datasource/SqliteUdfDataSource.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/datasource/SqliteUdfDataSource.kt @@ -1,8 +1,10 @@ package org.gotson.komga.infrastructure.datasource +import com.ibm.icu.text.Collator import mu.KotlinLogging import org.gotson.komga.infrastructure.language.stripAccents import org.springframework.jdbc.datasource.SimpleDriverDataSource +import org.sqlite.Collation import org.sqlite.Function import org.sqlite.SQLiteConnection import java.sql.Connection @@ -13,6 +15,7 @@ class SqliteUdfDataSource : SimpleDriverDataSource() { companion object { const val udfStripAccents = "UDF_STRIP_ACCENTS" + const val collationUnicode3 = "COLLATION_UNICODE_3" } override fun getConnection(): Connection = @@ -24,6 +27,7 @@ class SqliteUdfDataSource : SimpleDriverDataSource() { private fun addAllUdf(connection: SQLiteConnection) { createUdfRegexp(connection) createUdfStripAccents(connection) + createUnicode3Collation(connection) } private fun createUdfRegexp(connection: SQLiteConnection) { @@ -54,4 +58,19 @@ class SqliteUdfDataSource : SimpleDriverDataSource() { } ) } + + private fun createUnicode3Collation(connection: SQLiteConnection) { + log.debug { "Adding custom $collationUnicode3 collation" } + Collation.create( + connection, collationUnicode3, + object : Collation() { + val collator = Collator.getInstance().apply { + strength = Collator.TERTIARY + decomposition = Collator.CANONICAL_DECOMPOSITION + } + + override fun xCompare(str1: String, str2: String): Int = collator.compare(str1, str2) + } + ) + } } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookDtoDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookDtoDao.kt index 695bdff2f..5b7b7af3d 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookDtoDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookDtoDao.kt @@ -2,6 +2,7 @@ package org.gotson.komga.infrastructure.jooq import org.gotson.komga.domain.model.BookSearchWithReadProgress import org.gotson.komga.domain.model.ReadStatus +import org.gotson.komga.infrastructure.datasource.SqliteUdfDataSource import org.gotson.komga.infrastructure.search.LuceneEntity import org.gotson.komga.infrastructure.search.LuceneHelper import org.gotson.komga.infrastructure.web.toFilePath @@ -48,19 +49,19 @@ class BookDtoDao( private val bt = Tables.BOOK_METADATA_TAG private val sorts = mapOf( - "name" to lower(b.NAME.udfStripAccents()), + "name" to b.NAME.collate(SqliteUdfDataSource.collationUnicode3), "created" to b.CREATED_DATE, "createdDate" to b.CREATED_DATE, "lastModified" to b.LAST_MODIFIED_DATE, "lastModifiedDate" to b.LAST_MODIFIED_DATE, "fileSize" to b.FILE_SIZE, "size" to b.FILE_SIZE, - "url" to lower(b.URL), - "media.status" to lower(m.STATUS), - "media.comment" to lower(m.COMMENT), - "media.mediaType" to lower(m.MEDIA_TYPE), + "url" to b.URL.noCase(), + "media.status" to m.STATUS.noCase(), + "media.comment" to m.COMMENT.noCase(), + "media.mediaType" to m.MEDIA_TYPE.noCase(), + "metadata.title" to d.TITLE.collate(SqliteUdfDataSource.collationUnicode3), "metadata.numberSort" to d.NUMBER_SORT, - "metadata.title" to lower(d.TITLE.udfStripAccents()), "metadata.releaseDate" to d.RELEASE_DATE, "readProgress.lastModified" to r.LAST_MODIFIED_DATE, "readList.number" to rlb.NUMBER, diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReadListDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReadListDao.kt index 31f28b055..4690dfac2 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReadListDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReadListDao.kt @@ -2,6 +2,7 @@ package org.gotson.komga.infrastructure.jooq import org.gotson.komga.domain.model.ReadList import org.gotson.komga.domain.persistence.ReadListRepository +import org.gotson.komga.infrastructure.datasource.SqliteUdfDataSource import org.gotson.komga.infrastructure.search.LuceneEntity import org.gotson.komga.infrastructure.search.LuceneHelper import org.gotson.komga.jooq.Tables @@ -9,7 +10,6 @@ import org.gotson.komga.jooq.tables.records.ReadlistRecord import org.jooq.DSLContext import org.jooq.Record import org.jooq.ResultQuery -import org.jooq.impl.DSL import org.springframework.data.domain.Page import org.springframework.data.domain.PageImpl import org.springframework.data.domain.PageRequest @@ -32,7 +32,7 @@ class ReadListDao( private val b = Tables.BOOK private val sorts = mapOf( - "name" to DSL.lower(rl.NAME.udfStripAccents()), + "name" to rl.NAME.collate(SqliteUdfDataSource.collationUnicode3), ) override fun findByIdOrNull(readListId: String): ReadList? = diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReferentialDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReferentialDao.kt index b79dd6018..1b632006a 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReferentialDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReferentialDao.kt @@ -2,12 +2,12 @@ package org.gotson.komga.infrastructure.jooq import org.gotson.komga.domain.model.Author import org.gotson.komga.domain.persistence.ReferentialRepository +import org.gotson.komga.infrastructure.datasource.SqliteUdfDataSource import org.gotson.komga.infrastructure.language.stripAccents import org.gotson.komga.jooq.Tables import org.gotson.komga.jooq.tables.records.BookMetadataAggregationAuthorRecord import org.gotson.komga.jooq.tables.records.BookMetadataAuthorRecord import org.jooq.DSLContext -import org.jooq.impl.DSL.lower import org.jooq.impl.DSL.noCondition import org.jooq.impl.DSL.select import org.springframework.data.domain.Page @@ -42,7 +42,7 @@ class ReferentialDao( .apply { filterOnLibraryIds?.let { leftJoin(b).on(a.BOOK_ID.eq(b.ID)) } } .where(a.NAME.udfStripAccents().containsIgnoreCase(search.stripAccents())) .apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } } - .orderBy(lower(a.NAME.udfStripAccents()), a.ROLE) + .orderBy(a.NAME.collate(SqliteUdfDataSource.collationUnicode3)) .fetchInto(a) .map { it.toDomain() } @@ -53,7 +53,7 @@ class ReferentialDao( .where(bmaa.NAME.udfStripAccents().containsIgnoreCase(search.stripAccents())) .and(s.LIBRARY_ID.eq(libraryId)) .apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } } - .orderBy(lower(bmaa.NAME.udfStripAccents()), bmaa.ROLE) + .orderBy(bmaa.NAME.collate(SqliteUdfDataSource.collationUnicode3)) .fetchInto(bmaa) .map { it.toDomain() } @@ -65,7 +65,7 @@ class ReferentialDao( .where(bmaa.NAME.udfStripAccents().containsIgnoreCase(search.stripAccents())) .and(cs.COLLECTION_ID.eq(collectionId)) .apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } } - .orderBy(lower(bmaa.NAME.udfStripAccents()), bmaa.ROLE) + .orderBy(bmaa.NAME.collate(SqliteUdfDataSource.collationUnicode3)) .fetchInto(bmaa) .map { it.toDomain() } @@ -76,7 +76,7 @@ class ReferentialDao( .where(bmaa.NAME.udfStripAccents().containsIgnoreCase(search.stripAccents())) .and(bmaa.SERIES_ID.eq(seriesId)) .apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } } - .orderBy(lower(bmaa.NAME.udfStripAccents()), bmaa.ROLE) + .orderBy(bmaa.NAME.collate(SqliteUdfDataSource.collationUnicode3)) .fetchInto(bmaa) .map { it.toDomain() } @@ -138,7 +138,7 @@ class ReferentialDao( } val count = dsl.fetchCount(query) - val sort = lower(bmaa.NAME.udfStripAccents()) + val sort = bmaa.NAME.collate(SqliteUdfDataSource.collationUnicode3) val items = query .orderBy(sort) @@ -161,7 +161,7 @@ class ReferentialDao( .apply { filterOnLibraryIds?.let { leftJoin(b).on(a.BOOK_ID.eq(b.ID)) } } .where(a.NAME.udfStripAccents().containsIgnoreCase(search.stripAccents())) .apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } } - .orderBy(a.NAME.udfStripAccents()) + .orderBy(a.NAME.collate(SqliteUdfDataSource.collationUnicode3)) .fetch(a.NAME) override fun findAllAuthorsRoles(filterOnLibraryIds: Collection?): List = @@ -185,7 +185,7 @@ class ReferentialDao( .where(s.LIBRARY_ID.`in`(it)) } } - .orderBy(lower(g.GENRE.udfStripAccents())) + .orderBy(g.GENRE.collate(SqliteUdfDataSource.collationUnicode3)) .fetchSet(g.GENRE) override fun findAllGenresByLibrary(libraryId: String, filterOnLibraryIds: Collection?): Set = @@ -194,7 +194,7 @@ class ReferentialDao( .leftJoin(s).on(g.SERIES_ID.eq(s.ID)) .where(s.LIBRARY_ID.eq(libraryId)) .apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } } - .orderBy(lower(g.GENRE.udfStripAccents())) + .orderBy(g.GENRE.collate(SqliteUdfDataSource.collationUnicode3)) .fetchSet(g.GENRE) override fun findAllGenresByCollection(collectionId: String, filterOnLibraryIds: Collection?): Set = @@ -204,7 +204,7 @@ class ReferentialDao( .apply { filterOnLibraryIds?.let { leftJoin(s).on(g.SERIES_ID.eq(s.ID)) } } .where(cs.COLLECTION_ID.eq(collectionId)) .apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } } - .orderBy(lower(g.GENRE.udfStripAccents())) + .orderBy(g.GENRE.collate(SqliteUdfDataSource.collationUnicode3)) .fetchSet(g.GENRE) override fun findAllSeriesAndBookTags(filterOnLibraryIds: Collection?): Set = @@ -265,7 +265,7 @@ class ReferentialDao( .where(s.LIBRARY_ID.`in`(it)) } } - .orderBy(lower(st.TAG.udfStripAccents())) + .orderBy(st.TAG.collate(SqliteUdfDataSource.collationUnicode3)) .fetchSet(st.TAG) override fun findAllSeriesTagsByLibrary(libraryId: String, filterOnLibraryIds: Collection?): Set = @@ -274,7 +274,7 @@ class ReferentialDao( .leftJoin(s).on(st.SERIES_ID.eq(s.ID)) .where(s.LIBRARY_ID.eq(libraryId)) .apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } } - .orderBy(lower(st.TAG.udfStripAccents())) + .orderBy(st.TAG.collate(SqliteUdfDataSource.collationUnicode3)) .fetchSet(st.TAG) override fun findAllBookTagsBySeries(seriesId: String, filterOnLibraryIds: Collection?): Set = @@ -283,7 +283,7 @@ class ReferentialDao( .leftJoin(b).on(bt.BOOK_ID.eq(b.ID)) .where(b.SERIES_ID.eq(seriesId)) .apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } } - .orderBy(lower(bt.TAG.udfStripAccents())) + .orderBy(bt.TAG.collate(SqliteUdfDataSource.collationUnicode3)) .fetchSet(bt.TAG) override fun findAllBookTagsByReadList(readListId: String, filterOnLibraryIds: Collection?): Set = @@ -293,7 +293,7 @@ class ReferentialDao( .leftJoin(rb).on(bt.BOOK_ID.eq(rb.BOOK_ID)) .where(rb.READLIST_ID.eq(readListId)) .apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } } - .orderBy(lower(bt.TAG.udfStripAccents())) + .orderBy(bt.TAG.collate(SqliteUdfDataSource.collationUnicode3)) .fetchSet(bt.TAG) override fun findAllSeriesTagsByCollection(collectionId: String, filterOnLibraryIds: Collection?): Set = @@ -303,7 +303,7 @@ class ReferentialDao( .apply { filterOnLibraryIds?.let { leftJoin(s).on(st.SERIES_ID.eq(s.ID)) } } .where(cs.COLLECTION_ID.eq(collectionId)) .apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } } - .orderBy(lower(st.TAG.udfStripAccents())) + .orderBy(st.TAG.collate(SqliteUdfDataSource.collationUnicode3)) .fetchSet(st.TAG) override fun findAllBookTags(filterOnLibraryIds: Collection?): Set = @@ -315,7 +315,7 @@ class ReferentialDao( .where(b.LIBRARY_ID.`in`(it)) } } - .orderBy(lower(st.TAG.udfStripAccents())) + .orderBy(st.TAG.collate(SqliteUdfDataSource.collationUnicode3)) .fetchSet(st.TAG) override fun findAllLanguages(filterOnLibraryIds: Collection?): Set = @@ -354,7 +354,7 @@ class ReferentialDao( .apply { filterOnLibraryIds?.let { leftJoin(s).on(sd.SERIES_ID.eq(s.ID)) } } .where(sd.PUBLISHER.ne("")) .apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } } - .orderBy(lower(sd.PUBLISHER.udfStripAccents())) + .orderBy(sd.PUBLISHER.collate(SqliteUdfDataSource.collationUnicode3)) .fetchSet(sd.PUBLISHER) override fun findAllPublishersByLibrary(libraryId: String, filterOnLibraryIds: Collection?): Set = @@ -364,7 +364,7 @@ class ReferentialDao( .where(sd.PUBLISHER.ne("")) .and(s.LIBRARY_ID.eq(libraryId)) .apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } } - .orderBy(lower(sd.PUBLISHER.udfStripAccents())) + .orderBy(sd.PUBLISHER.collate(SqliteUdfDataSource.collationUnicode3)) .fetchSet(sd.PUBLISHER) override fun findAllPublishersByCollection(collectionId: String, filterOnLibraryIds: Collection?): Set = @@ -375,7 +375,7 @@ class ReferentialDao( .where(sd.PUBLISHER.ne("")) .and(cs.COLLECTION_ID.eq(collectionId)) .apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } } - .orderBy(lower(sd.PUBLISHER.udfStripAccents())) + .orderBy(sd.PUBLISHER.collate(SqliteUdfDataSource.collationUnicode3)) .fetchSet(sd.PUBLISHER) override fun findAllAgeRatings(filterOnLibraryIds: Collection?): Set = diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesCollectionDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesCollectionDao.kt index 8edf554c6..ea1a21fa6 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesCollectionDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesCollectionDao.kt @@ -2,6 +2,7 @@ package org.gotson.komga.infrastructure.jooq import org.gotson.komga.domain.model.SeriesCollection import org.gotson.komga.domain.persistence.SeriesCollectionRepository +import org.gotson.komga.infrastructure.datasource.SqliteUdfDataSource import org.gotson.komga.infrastructure.search.LuceneEntity import org.gotson.komga.infrastructure.search.LuceneHelper import org.gotson.komga.jooq.Tables @@ -9,7 +10,6 @@ import org.gotson.komga.jooq.tables.records.CollectionRecord import org.jooq.DSLContext import org.jooq.Record import org.jooq.ResultQuery -import org.jooq.impl.DSL import org.springframework.data.domain.Page import org.springframework.data.domain.PageImpl import org.springframework.data.domain.PageRequest @@ -31,7 +31,7 @@ class SeriesCollectionDao( private val s = Tables.SERIES private val sorts = mapOf( - "name" to DSL.lower(c.NAME.udfStripAccents()), + "name" to c.NAME.collate(SqliteUdfDataSource.collationUnicode3), ) override fun findByIdOrNull(collectionId: String): SeriesCollection? = diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDtoDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDtoDao.kt index 8858ab07c..7280454a4 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDtoDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDtoDao.kt @@ -4,6 +4,7 @@ import mu.KotlinLogging import org.gotson.komga.domain.model.ReadStatus import org.gotson.komga.domain.model.SeriesSearch import org.gotson.komga.domain.model.SeriesSearchWithReadProgress +import org.gotson.komga.infrastructure.datasource.SqliteUdfDataSource import org.gotson.komga.infrastructure.search.LuceneEntity import org.gotson.komga.infrastructure.search.LuceneHelper import org.gotson.komga.infrastructure.web.toFilePath @@ -74,14 +75,14 @@ class SeriesDtoDao( ) private val sorts = mapOf( - "metadata.titleSort" to lower(d.TITLE_SORT), + "metadata.titleSort" to d.TITLE_SORT.noCase(), "createdDate" to s.CREATED_DATE, "created" to s.CREATED_DATE, "lastModifiedDate" to s.LAST_MODIFIED_DATE, "lastModified" to s.LAST_MODIFIED_DATE, "booksMetadata.releaseDate" to bma.RELEASE_DATE, "collection.number" to cs.NUMBER, - "name" to lower(s.NAME.udfStripAccents()), + "name" to s.NAME.collate(SqliteUdfDataSource.collationUnicode3), "booksCount" to s.BOOK_COUNT, ) diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/Utils.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/Utils.kt index 27be99623..b88850117 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/Utils.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/Utils.kt @@ -10,6 +10,8 @@ import java.time.LocalDateTime import java.time.ZoneId import java.time.ZoneOffset +fun Field.noCase() = this.collate("NOCASE") + fun LocalDateTime.toUTC(): LocalDateTime = atZone(ZoneId.systemDefault()).withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime()