From 00ba4132621419132c3a78d97a73fd28537e5112 Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Thu, 12 Feb 2026 16:22:21 +0800 Subject: [PATCH] fix string matching to be accent insensitive --- .../jooq/SearchOperatorUtils.kt | 21 +++++----- .../jooq/main/SeriesSearchTest.kt | 42 +++++++++---------- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SearchOperatorUtils.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SearchOperatorUtils.kt index 77d5eb23..93d12933 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SearchOperatorUtils.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SearchOperatorUtils.kt @@ -1,6 +1,7 @@ package org.gotson.komga.infrastructure.jooq import org.gotson.komga.domain.model.SearchOperator +import org.gotson.komga.language.stripAccents import org.jooq.Field import java.time.LocalDate import java.time.ZoneOffset @@ -10,8 +11,8 @@ fun SearchOperator.Equality.toCondition( field: Field, ignoreCase: Boolean = false, ) = when (this) { - is SearchOperator.Is -> if (ignoreCase) field.equalIgnoreCase(this.value) else field.eq(this.value) - is SearchOperator.IsNot -> if (ignoreCase) field.notEqualIgnoreCase(this.value) else field.ne(this.value) + is SearchOperator.Is -> if (ignoreCase) field.unicode1().equal(this.value) else field.equal(this.value) + is SearchOperator.IsNot -> if (ignoreCase) field.unicode1().notEqual(this.value) else field.notEqual(this.value) } fun SearchOperator.Equality.toCondition(field: Field) = @@ -30,14 +31,14 @@ fun SearchOperator.Equality.toCondition( fun SearchOperator.StringOp.toCondition(field: Field) = when (this) { - is SearchOperator.BeginsWith -> field.startsWithIgnoreCase(value) - is SearchOperator.DoesNotBeginWith -> field.startsWithIgnoreCase(value).not() - is SearchOperator.EndsWith -> field.endsWithIgnoreCase(value) - is SearchOperator.DoesNotEndWith -> field.endsWithIgnoreCase(value).not() - is SearchOperator.Contains -> field.containsIgnoreCase(value) - is SearchOperator.DoesNotContain -> field.notContainsIgnoreCase(value) - is SearchOperator.Is<*> -> field.equalIgnoreCase(value as String) - is SearchOperator.IsNot<*> -> field.notEqualIgnoreCase(value as String) + is SearchOperator.BeginsWith -> field.udfStripAccents().startsWithIgnoreCase(value.stripAccents()) + is SearchOperator.DoesNotBeginWith -> field.udfStripAccents().startsWithIgnoreCase(value.stripAccents()).not() + is SearchOperator.EndsWith -> field.udfStripAccents().endsWithIgnoreCase(value.stripAccents()) + is SearchOperator.DoesNotEndWith -> field.udfStripAccents().endsWithIgnoreCase(value.stripAccents()).not() + is SearchOperator.Contains -> field.udfStripAccents().containsIgnoreCase(value.stripAccents()) + is SearchOperator.DoesNotContain -> field.udfStripAccents().notContainsIgnoreCase(value.stripAccents()) + is SearchOperator.Is<*> -> field.unicode1().equal(value as String) + is SearchOperator.IsNot<*> -> field.unicode1().notEqual(value as String) } fun SearchOperator.Date.toCondition(field: Field) = diff --git a/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/main/SeriesSearchTest.kt b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/main/SeriesSearchTest.kt index f69bdd82..173887a8 100644 --- a/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/main/SeriesSearchTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/main/SeriesSearchTest.kt @@ -649,7 +649,7 @@ class SeriesSearchTest( } run { - val search = SeriesSearch(SearchCondition.Publisher(SearchOperator.Is("marvelOUS"))) + val search = SeriesSearch(SearchCondition.Publisher(SearchOperator.Is("mÂrvelOUS"))) val found = seriesDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content val foundDto = seriesDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content @@ -658,7 +658,7 @@ class SeriesSearchTest( } run { - val search = SeriesSearch(SearchCondition.Publisher(SearchOperator.IsNot("marvelOUS"))) + val search = SeriesSearch(SearchCondition.Publisher(SearchOperator.IsNot("mÂrvelOUS"))) val found = seriesDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content val foundDto = seriesDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content @@ -1201,12 +1201,12 @@ class SeriesSearchTest( makeSeries("2", library1.id).let { series -> seriesLifecycle.createSeries(series) seriesMetadataRepository.findById(series.id).let { - seriesMetadataRepository.update(it.copy(title = "Series 2")) + seriesMetadataRepository.update(it.copy(title = "Series two")) } } run { - val search = SeriesSearch(SearchCondition.Title(SearchOperator.Is("seRIES 1"))) + val search = SeriesSearch(SearchCondition.Title(SearchOperator.Is("séRIES 1"))) val found = seriesDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content val foundDto = seriesDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content @@ -1215,7 +1215,7 @@ class SeriesSearchTest( } run { - val search = SeriesSearch(SearchCondition.Title(SearchOperator.IsNot("series 1"))) + val search = SeriesSearch(SearchCondition.Title(SearchOperator.IsNot("séRIES 1"))) val found = seriesDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content val foundDto = seriesDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content @@ -1224,7 +1224,7 @@ class SeriesSearchTest( } run { - val search = SeriesSearch(SearchCondition.Title(SearchOperator.Contains("series"))) + val search = SeriesSearch(SearchCondition.Title(SearchOperator.Contains("séRIES"))) val found = seriesDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content val foundDto = seriesDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content @@ -1233,16 +1233,16 @@ class SeriesSearchTest( } run { - val search = SeriesSearch(SearchCondition.Title(SearchOperator.DoesNotContain("1"))) + val search = SeriesSearch(SearchCondition.Title(SearchOperator.DoesNotContain("TWÔ"))) val found = seriesDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content val foundDto = seriesDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content - assertThat(found.map { it.name }).containsExactlyInAnyOrder("2") - assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("2") + assertThat(found.map { it.name }).containsExactlyInAnyOrder("1") + assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1") } run { - val search = SeriesSearch(SearchCondition.Title(SearchOperator.BeginsWith("series"))) + val search = SeriesSearch(SearchCondition.Title(SearchOperator.BeginsWith("séRIES"))) val found = seriesDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content val foundDto = seriesDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content @@ -1251,7 +1251,7 @@ class SeriesSearchTest( } run { - val search = SeriesSearch(SearchCondition.Title(SearchOperator.DoesNotBeginWith("series"))) + val search = SeriesSearch(SearchCondition.Title(SearchOperator.DoesNotBeginWith("séRIES"))) val found = seriesDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content val foundDto = seriesDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content @@ -1260,22 +1260,22 @@ class SeriesSearchTest( } run { - val search = SeriesSearch(SearchCondition.Title(SearchOperator.EndsWith("1"))) - val found = seriesDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content - val foundDto = seriesDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content - - assertThat(found.map { it.name }).containsExactlyInAnyOrder("1") - assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1") - } - - run { - val search = SeriesSearch(SearchCondition.Title(SearchOperator.DoesNotEndWith("1"))) + val search = SeriesSearch(SearchCondition.Title(SearchOperator.EndsWith("TWÔ"))) val found = seriesDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content val foundDto = seriesDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content assertThat(found.map { it.name }).containsExactlyInAnyOrder("2") assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("2") } + + run { + val search = SeriesSearch(SearchCondition.Title(SearchOperator.DoesNotEndWith("TWÔ"))) + val found = seriesDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content + val foundDto = seriesDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content + + assertThat(found.map { it.name }).containsExactlyInAnyOrder("1") + assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1") + } } @Test