feat(search): add AttributeFilter record with key regex + wildcard pattern translation
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
package com.cameleer.server.core.search;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Structured attribute filter for execution search.
|
||||
* <p>
|
||||
* Value semantics:
|
||||
* <ul>
|
||||
* <li>{@code value == null} or blank -> key-exists check</li>
|
||||
* <li>{@code value} contains {@code *} -> wildcard match (translated to SQL LIKE pattern)</li>
|
||||
* <li>otherwise -> exact match</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Keys must match {@code ^[a-zA-Z0-9._-]+$} — they are later inlined into
|
||||
* ClickHouse SQL via {@code JSONExtractString}, which does not accept a
|
||||
* parameter placeholder for the JSON path. Values are always parameter-bound.
|
||||
*/
|
||||
public record AttributeFilter(String key, String value) {
|
||||
|
||||
private static final Pattern KEY_PATTERN = Pattern.compile("^[a-zA-Z0-9._-]+$");
|
||||
|
||||
public AttributeFilter {
|
||||
if (key == null || !KEY_PATTERN.matcher(key).matches()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid attribute key: must match " + KEY_PATTERN.pattern() + ", got: " + key);
|
||||
}
|
||||
if (value != null && value.isBlank()) {
|
||||
value = null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isKeyOnly() {
|
||||
return value == null;
|
||||
}
|
||||
|
||||
public boolean isWildcard() {
|
||||
return value != null && value.indexOf('*') >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a SQL LIKE pattern for wildcard matches with {@code %} / {@code _} / {@code \}
|
||||
* in the source value escaped, or {@code null} for exact / key-only filters.
|
||||
*/
|
||||
public String toLikePattern() {
|
||||
if (!isWildcard()) return null;
|
||||
StringBuilder sb = new StringBuilder(value.length() + 4);
|
||||
for (int i = 0; i < value.length(); i++) {
|
||||
char c = value.charAt(i);
|
||||
switch (c) {
|
||||
case '\\' -> sb.append("\\\\");
|
||||
case '%' -> sb.append("\\%");
|
||||
case '_' -> sb.append("\\_");
|
||||
case '*' -> sb.append('%');
|
||||
default -> sb.append(c);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.cameleer.server.core.search;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class AttributeFilterTest {
|
||||
|
||||
@Test
|
||||
void keyOnly_blankValue_normalizesToNull() {
|
||||
AttributeFilter f = new AttributeFilter("order", "");
|
||||
assertThat(f.value()).isNull();
|
||||
assertThat(f.isKeyOnly()).isTrue();
|
||||
assertThat(f.isWildcard()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void keyOnly_nullValue_isKeyOnly() {
|
||||
AttributeFilter f = new AttributeFilter("order", null);
|
||||
assertThat(f.isKeyOnly()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void exactValue_isNotWildcard() {
|
||||
AttributeFilter f = new AttributeFilter("order", "47");
|
||||
assertThat(f.isKeyOnly()).isFalse();
|
||||
assertThat(f.isWildcard()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void starInValue_isWildcard() {
|
||||
AttributeFilter f = new AttributeFilter("order", "47*");
|
||||
assertThat(f.isWildcard()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidKey_throws() {
|
||||
assertThatThrownBy(() -> new AttributeFilter("bad key", "x"))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("attribute key");
|
||||
}
|
||||
|
||||
@Test
|
||||
void blankKey_throws() {
|
||||
assertThatThrownBy(() -> new AttributeFilter(" ", null))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void wildcardPattern_escapesLikeMetaCharacters() {
|
||||
AttributeFilter f = new AttributeFilter("order", "a_b%c\\d*");
|
||||
assertThat(f.toLikePattern()).isEqualTo("a\\_b\\%c\\\\d%");
|
||||
}
|
||||
|
||||
@Test
|
||||
void exactValue_toLikePattern_returnsNull() {
|
||||
AttributeFilter f = new AttributeFilter("order", "47");
|
||||
assertThat(f.toLikePattern()).isNull();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user