From 4536af9fcb3b05a96f0237518b3d14122c87c13b Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 13 Jun 2025 12:06:53 +0200 Subject: [PATCH] Implement 'like' condition for string values Squash commits of branch feature/like_query_for_string: - Implement 'like' condition for string values - Log change - Enhance tests - Fix JavaDoc - Add more tests --- gradle/changelog/like.yaml | 2 ++ .../main/java/sonia/scm/store/Operator.java | 1 + .../java/sonia/scm/store/QueryableStore.java | 12 +++++++ .../sonia/scm/store/sqlite/SQLCondition.java | 5 +-- .../sqlite/SQLiteQueryableStoreTest.java | 32 +++++++++++++++++++ 5 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 gradle/changelog/like.yaml diff --git a/gradle/changelog/like.yaml b/gradle/changelog/like.yaml new file mode 100644 index 0000000000..e28174f0a5 --- /dev/null +++ b/gradle/changelog/like.yaml @@ -0,0 +1,2 @@ +- type: added + description: "Like" condition for string fields in queryable stores diff --git a/scm-core/src/main/java/sonia/scm/store/Operator.java b/scm-core/src/main/java/sonia/scm/store/Operator.java index d54c252ac1..9b0bfbd942 100644 --- a/scm-core/src/main/java/sonia/scm/store/Operator.java +++ b/scm-core/src/main/java/sonia/scm/store/Operator.java @@ -24,6 +24,7 @@ public enum Operator { LESS_OR_EQUAL, GREATER_OR_EQUAL, CONTAINS, + LIKE, IN, NULL, KEY, diff --git a/scm-core/src/main/java/sonia/scm/store/QueryableStore.java b/scm-core/src/main/java/sonia/scm/store/QueryableStore.java index 745ecba426..6b943e7296 100644 --- a/scm-core/src/main/java/sonia/scm/store/QueryableStore.java +++ b/scm-core/src/main/java/sonia/scm/store/QueryableStore.java @@ -313,6 +313,18 @@ public interface QueryableStore extends AutoCloseable { return new LeafCondition<>(this, Operator.CONTAINS, value); } + /** + * Creates a condition that checks if the field matches the given pattern. The pattern can contain asterisks (*) as + * wildcards, which will match any sequence of characters. + *
For example, Heart*Gold will match "Heart Of Gold", "HeartGold", or "Heart of Gold" but not + * "Heart of Gold and Silver". For now, escaping of asterisks is not supported. + * + * @param value The pattern to check for. + * @return The condition to use in a query. + */ + public Condition like(String value) { + return new LeafCondition<>(this, Operator.LIKE, value); + } /** * Creates a condition that checks if the field is equal to any of the given values. diff --git a/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLCondition.java b/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLCondition.java index 42f2830f98..5b30abf793 100644 --- a/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLCondition.java +++ b/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLCondition.java @@ -73,6 +73,7 @@ class SQLCondition implements SQLNodeWithValue { case GREATER -> ">"; case GREATER_OR_EQUAL -> ">="; case CONTAINS -> "LIKE '%' ||"; + case LIKE -> "LIKE "; case NULL -> "IS NULL"; case IN -> "IN"; case KEY -> "key ="; @@ -106,14 +107,14 @@ class SQLCondition implements SQLNodeWithValue { switch (operator) { case NULL: return new SQLValue(null); - case IN: if (value instanceof Object[] valueArray) { return new SQLValue(valueArray); } else { throw new IllegalArgumentException("Value for IN operator must be an array."); } - + case LIKE: + return new SQLValue(value.toString().replace('*', '%')); default: return new SQLValue(computeParameter(leafCondition)); } diff --git a/scm-persistence/src/test/java/sonia/scm/store/sqlite/SQLiteQueryableStoreTest.java b/scm-persistence/src/test/java/sonia/scm/store/sqlite/SQLiteQueryableStoreTest.java index 21c516683b..90d336566e 100644 --- a/scm-persistence/src/test/java/sonia/scm/store/sqlite/SQLiteQueryableStoreTest.java +++ b/scm-persistence/src/test/java/sonia/scm/store/sqlite/SQLiteQueryableStoreTest.java @@ -20,6 +20,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import sonia.scm.group.Group; import sonia.scm.repository.Repository; import sonia.scm.store.Conditions; @@ -66,6 +68,36 @@ class SQLiteQueryableStoreTest { @Nested class QueryClassTypes { + @ParameterizedTest + @ValueSource(strings = {"*Of*", "Heart*Gold", "H*", "*d", "*Heart Of Gold*", "Heart Of Gold", "Heart Of *Gold"}) + void shouldWorkWithLikes(String searchString) { + SQLiteQueryableMutableStore store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class); + store.put(new Spaceship("Space Shuttle", Range.SOLAR_SYSTEM)); + store.put(new Spaceship("Heart Of Gold", Range.INTER_GALACTIC)); + + List all = store + .query(SPACESHIP_NAME.like(searchString)) + .findAll(); + + assertThat(all) + .extracting("name") + .containsExactly("Heart Of Gold"); + } + + @ParameterizedTest + @ValueSource(strings = {"Of", "*of*", "heart of gold"}) + void shouldNotFindNotMatchingValuesWithLike() { + SQLiteQueryableMutableStore store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class); + store.put(new Spaceship("Space Shuttle", Range.SOLAR_SYSTEM)); + store.put(new Spaceship("Heart Of Gold", Range.INTER_GALACTIC)); + + List all = store + .query(SPACESHIP_NAME.like("Of")) + .findAll(); + + assertThat(all).isEmpty(); + } + @Test void shouldWorkWithEnums() { SQLiteQueryableMutableStore store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class);