Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,7 @@ predicate[ParserRuleContext value]
| NOT? IN '(' expression (',' expression)* ')' #inList
| NOT? IN '(' query ')' #inSubquery
| NOT? LIKE pattern=valueExpression (ESCAPE escape=valueExpression)? #like
| NOT? ILIKE pattern=valueExpression (ESCAPE escape=valueExpression)? #ilike
| IS NOT? NULL #nullPredicate
| IS NOT? DISTINCT FROM right=valueExpression #distinctFrom
;
Expand Down Expand Up @@ -1193,6 +1194,7 @@ LEAVE: 'LEAVE';
LEFT: 'LEFT';
LEVEL: 'LEVEL';
LIKE: 'LIKE';
ILIKE: 'ILIKE';
LIMIT: 'LIMIT';
LISTAGG: 'LISTAGG';
LOCAL: 'LOCAL';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ public void test()
"LEFT",
"LEVEL",
"LIKE",
"ILIKE",
"LIMIT",
"LISTAGG",
"LOCAL",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1064,22 +1064,23 @@ protected Type visitArithmeticBinary(ArithmeticBinaryExpression node, Context co
@Override
protected Type visitLikePredicate(LikePredicate node, Context context)
{
String operatorName = node.isCaseInsensitive() ? "ILIKE" : "LIKE";
Type valueType = process(node.getValue(), context);
if (!(valueType instanceof CharType) && !(valueType instanceof VarcharType)) {
coerceType(context, node.getValue(), VARCHAR, "Left side of LIKE expression");
coerceType(context, node.getValue(), VARCHAR, "Left side of " + operatorName + " expression");
}

Type patternType = process(node.getPattern(), context);
if (!(patternType instanceof VarcharType)) {
// TODO can pattern be of char type?
coerceType(context, node.getPattern(), VARCHAR, "Pattern for LIKE expression");
coerceType(context, node.getPattern(), VARCHAR, "Pattern for " + operatorName + " expression");
}
if (node.getEscape().isPresent()) {
Expression escape = node.getEscape().get();
Type escapeType = process(escape, context);
if (!(escapeType instanceof VarcharType)) {
// TODO can escape be of char type?
coerceType(context, escape, VARCHAR, "Escape for LIKE expression");
coerceType(context, escape, VARCHAR, "Escape for " + operatorName + " expression");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@
import static io.trino.sql.tree.JsonQuery.QuotesBehavior.KEEP;
import static io.trino.sql.tree.JsonQuery.QuotesBehavior.OMIT;
import static io.trino.type.JsonType.JSON;
import static io.trino.type.LikeFunctions.ILIKE_FUNCTION_NAME;
import static io.trino.type.LikeFunctions.ILIKE_PATTERN_FUNCTION_NAME;
import static io.trino.type.LikeFunctions.LIKE_FUNCTION_NAME;
import static io.trino.type.LikeFunctions.LIKE_PATTERN_FUNCTION_NAME;
import static io.trino.type.LikePatternType.LIKE_PATTERN;
Expand Down Expand Up @@ -918,23 +920,27 @@ private io.trino.sql.ir.Expression translate(LikePredicate node)
io.trino.sql.ir.Expression pattern = translateExpression(node.getPattern());
Optional<io.trino.sql.ir.Expression> escape = node.getEscape().map(this::translateExpression);

boolean caseInsensitive = node.isCaseInsensitive();
String patternFunctionName = caseInsensitive ? ILIKE_PATTERN_FUNCTION_NAME : LIKE_PATTERN_FUNCTION_NAME;
String likeFunctionName = caseInsensitive ? ILIKE_FUNCTION_NAME : LIKE_FUNCTION_NAME;

Call patternCall;
if (escape.isPresent()) {
patternCall = BuiltinFunctionCallBuilder.resolve(plannerContext.getMetadata())
.setName(LIKE_PATTERN_FUNCTION_NAME)
.setName(patternFunctionName)
.addArgument(VARCHAR, new io.trino.sql.ir.Cast(pattern, VARCHAR))
.addArgument(VARCHAR, new io.trino.sql.ir.Cast(escape.get(), VARCHAR))
.build();
}
else {
patternCall = BuiltinFunctionCallBuilder.resolve(plannerContext.getMetadata())
.setName(LIKE_PATTERN_FUNCTION_NAME)
.setName(patternFunctionName)
.addArgument(VARCHAR, new io.trino.sql.ir.Cast(pattern, VARCHAR))
.build();
}

Call call = BuiltinFunctionCallBuilder.resolve(plannerContext.getMetadata())
.setName(LIKE_FUNCTION_NAME)
.setName(likeFunctionName)
.addArgument(value.type(), value)
.addArgument(LIKE_PATTERN, patternCall)
.build();
Expand Down
44 changes: 42 additions & 2 deletions core/trino-main/src/main/java/io/trino/type/LikeFunctions.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public final class LikeFunctions
{
public static final String LIKE_FUNCTION_NAME = "$like";
public static final String LIKE_PATTERN_FUNCTION_NAME = "$like_pattern";
public static final String ILIKE_FUNCTION_NAME = "$ilike";
public static final String ILIKE_PATTERN_FUNCTION_NAME = "$ilike_pattern";

private LikeFunctions() {}

Expand All @@ -58,15 +60,53 @@ public static boolean likeVarchar(@SqlType("varchar(x)") Slice value, @SqlType(L
@SqlType(LikePatternType.NAME)
public static LikePattern likePattern(@SqlType("varchar") Slice pattern)
{
return LikePattern.compile(pattern.toStringUtf8(), Optional.empty(), false);
return LikePattern.compile(pattern.toStringUtf8(), Optional.empty(), false, true);
}

@ScalarFunction(value = LIKE_PATTERN_FUNCTION_NAME, hidden = true)
@SqlType(LikePatternType.NAME)
public static LikePattern likePattern(@SqlType("varchar") Slice pattern, @SqlType("varchar") Slice escape)
{
try {
return LikePattern.compile(pattern.toStringUtf8(), getEscapeCharacter(Optional.of(escape)), false);
return LikePattern.compile(pattern.toStringUtf8(), getEscapeCharacter(Optional.of(escape)), false, true);
}
catch (RuntimeException e) {
throw new TrinoException(INVALID_FUNCTION_ARGUMENT, e);
}
}

@ScalarFunction(value = ILIKE_FUNCTION_NAME, hidden = true)
@LiteralParameters("x")
@SqlType(StandardTypes.BOOLEAN)
public static boolean ilikeChar(@LiteralParameter("x") Long x, @SqlType("char(x)") Slice value, @SqlType(LikePatternType.NAME) LikePattern pattern)
{
return ilikeVarchar(padSpaces(value, x.intValue()), pattern);
}

@ScalarFunction(value = ILIKE_FUNCTION_NAME, hidden = true)
@LiteralParameters("x")
@SqlType(StandardTypes.BOOLEAN)
public static boolean ilikeVarchar(@SqlType("varchar(x)") Slice value, @SqlType(LikePatternType.NAME) LikePattern pattern)
{
// Convert to lowercase for case-insensitive matching
String lowerString = value.toStringUtf8().toLowerCase(java.util.Locale.ROOT);
byte[] lowerBytes = lowerString.getBytes(java.nio.charset.StandardCharsets.UTF_8);
return pattern.getMatcher().match(lowerBytes, 0, lowerBytes.length);
}

@ScalarFunction(value = ILIKE_PATTERN_FUNCTION_NAME, hidden = true)
@SqlType(LikePatternType.NAME)
public static LikePattern ilikePattern(@SqlType("varchar") Slice pattern)
{
return LikePattern.compile(pattern.toStringUtf8(), Optional.empty(), true, true);
}

@ScalarFunction(value = ILIKE_PATTERN_FUNCTION_NAME, hidden = true)
@SqlType(LikePatternType.NAME)
public static LikePattern ilikePattern(@SqlType("varchar") Slice pattern, @SqlType("varchar") Slice escape)
{
try {
return LikePattern.compile(pattern.toStringUtf8(), getEscapeCharacter(Optional.of(escape)), true, true);
}
catch (RuntimeException e) {
throw new TrinoException(INVALID_FUNCTION_ARGUMENT, e);
Expand Down
27 changes: 22 additions & 5 deletions core/trino-main/src/main/java/io/trino/type/LikePattern.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import io.trino.likematcher.LikeMatcher;

import java.util.Locale;
import java.util.Objects;
import java.util.Optional;

Expand All @@ -29,22 +30,31 @@ public class LikePattern
{
private final String pattern;
private final Optional<Character> escape;
private final boolean caseInsensitive;
private final LikeMatcher matcher;

public static LikePattern compile(String pattern, Optional<Character> escape)
{
return new LikePattern(pattern, escape, LikeMatcher.compile(pattern, escape));
return compile(pattern, escape, false, true);
}

public static LikePattern compile(String pattern, Optional<Character> escape, boolean optimize)
{
return new LikePattern(pattern, escape, LikeMatcher.compile(pattern, escape, optimize));
return compile(pattern, escape, false, optimize);
}

private LikePattern(String pattern, Optional<Character> escape, LikeMatcher matcher)
public static LikePattern compile(String pattern, Optional<Character> escape, boolean caseInsensitive, boolean optimize)
{
String effectivePattern = caseInsensitive ? pattern.toLowerCase(Locale.ROOT) : pattern;
Optional<Character> effectiveEscape = escape.map(ch -> caseInsensitive ? Character.toLowerCase(ch) : ch);
return new LikePattern(pattern, escape, caseInsensitive, LikeMatcher.compile(effectivePattern, effectiveEscape, optimize));
}

private LikePattern(String pattern, Optional<Character> escape, boolean caseInsensitive, LikeMatcher matcher)
{
this.pattern = requireNonNull(pattern, "pattern is null");
this.escape = requireNonNull(escape, "escape is null");
this.caseInsensitive = caseInsensitive;
this.matcher = requireNonNull(matcher, "likeMatcher is null");
}

Expand All @@ -58,6 +68,11 @@ public Optional<Character> getEscape()
return escape;
}

public boolean isCaseInsensitive()
{
return caseInsensitive;
}

public LikeMatcher getMatcher()
{
return matcher;
Expand All @@ -73,13 +88,15 @@ public boolean equals(Object o)
return false;
}
LikePattern that = (LikePattern) o;
return Objects.equals(pattern, that.pattern) && Objects.equals(escape, that.escape);
return Objects.equals(pattern, that.pattern) &&
Objects.equals(escape, that.escape) &&
caseInsensitive == that.caseInsensitive;
}

@Override
public int hashCode()
{
return Objects.hash(pattern, escape);
return Objects.hash(pattern, escape, caseInsensitive);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
import java.util.Optional;

import static io.airlift.slice.Slices.utf8Slice;
import static io.trino.type.LikeFunctions.ilikeChar;
import static io.trino.type.LikeFunctions.ilikePattern;
import static io.trino.type.LikeFunctions.ilikeVarchar;
import static io.trino.type.LikeFunctions.isLikePattern;
import static io.trino.type.LikeFunctions.likeChar;
import static io.trino.type.LikeFunctions.likePattern;
Expand Down Expand Up @@ -356,4 +359,117 @@ SELECT value FROM (
"""))
.matches("VALUES 'a%b', 'c%'");
}

@Test
public void testIlikeBasic()
{
LikePattern matcher = ilikePattern(utf8Slice("F%B__"));
assertThat(ilikeVarchar(utf8Slice("foobar"), matcher)).isTrue();
assertThat(ilikeVarchar(utf8Slice("FOOBAR"), matcher)).isTrue();
assertThat(ilikeVarchar(utf8Slice("FooBar"), matcher)).isTrue();
assertThat(ilikeVarchar(offsetHeapSlice("foobar"), matcher)).isTrue();

// Basic case-insensitive tests using query() to avoid constant folding issues
assertThat(assertions.query("SELECT 'foob' ILIKE 'f%b__'"))
.matches("VALUES false");
assertThat(assertions.query("SELECT 'FOOBAR' ILIKE 'f%b__'"))
.matches("VALUES true");
assertThat(assertions.query("SELECT 'foobar' ILIKE 'F%B__'"))
.matches("VALUES true");
assertThat(assertions.query("SELECT 'FOOB' ILIKE 'f%b'"))
.matches("VALUES true");

// Test with mixed case
assertThat(assertions.query("SELECT 'foobar' ILIKE 'FoO%'"))
.matches("VALUES true");
assertThat(assertions.query("SELECT 'FOOBAR' ILIKE 'foo%'"))
.matches("VALUES true");
}

@Test
public void testIlikeChar()
{
LikePattern matcher = ilikePattern(utf8Slice("F%B__"));
assertThat(ilikeChar(6L, utf8Slice("foobar"), matcher)).isTrue();
assertThat(ilikeChar(6L, utf8Slice("FOOBAR"), matcher)).isTrue();
assertThat(ilikeChar(6L, offsetHeapSlice("FooBar"), matcher)).isTrue();
assertThat(ilikeChar(6L, utf8Slice("foob"), matcher)).isTrue();
assertThat(ilikeChar(6L, offsetHeapSlice("FOOB"), matcher)).isTrue();
assertThat(ilikeChar(7L, utf8Slice("foob"), matcher)).isFalse();

// Test with char type using query() to avoid constant folding issues
assertThat(assertions.query("SELECT CAST('foo' AS char(6)) ILIKE 'FOO%'"))
.matches("VALUES true");
assertThat(assertions.query("SELECT CAST('FOO' AS char(6)) ILIKE 'foo%'"))
.matches("VALUES true");
assertThat(assertions.query("SELECT CAST('foob' AS char(6)) ILIKE 'F%B__'"))
.matches("VALUES true");
}

@Test
public void testIlikeUtf8()
{
// Test with UTF-8 characters - case folding
LikePattern matcher = ilikePattern(utf8Slice("%ÑAME%"));
assertThat(ilikeVarchar(utf8Slice("my ñame is"), matcher)).isTrue();
assertThat(ilikeVarchar(utf8Slice("my ÑAME is"), matcher)).isTrue();

assertThat(assertions.query("SELECT 'foo名bar' ILIKE '%名%'"))
.matches("VALUES true");
}

@Test
public void testIlikeWithEscape()
{
LikePattern matcher = ilikePattern(utf8Slice("X%X_ABCXX"), utf8Slice("X"));
assertThat(ilikeVarchar(utf8Slice("%_ABCx"), matcher)).isTrue();
assertThat(ilikeVarchar(utf8Slice("%_abcX"), matcher)).isTrue();

assertThat(assertions.query("SELECT 'f%bar' ILIKE 'F#%B__' ESCAPE '#'"))
.matches("VALUES true");
assertThat(assertions.query("SELECT 'F%BAR' ILIKE 'f#%b__' ESCAPE '#'"))
.matches("VALUES true");
}

@Test
public void testIlikeWithDynamicPattern()
{
assertThat(assertions.query(
"""
SELECT value FROM (
VALUES
('a', 'A'),
('B', 'a'),
('C', '%')) t(value, pattern)
WHERE value ILIKE pattern
"""))
.matches("VALUES 'a', 'C'");

assertThat(assertions.query(
"""
SELECT value FROM (
VALUES
('A%b', 'aX%B', 'X'),
('a0B', 'AX%b', 'X'),
('b_', 'aY_', 'Y'),
('C%', 'cZ%', 'Z')) t(value, pattern, esc)
WHERE value ILIKE pattern ESCAPE esc
"""))
.matches("VALUES 'A%b', 'C%'");
}

@Test
public void testIlikeDifferentFromLike()
{
// Verify ILIKE and LIKE behave differently
assertThat(assertions.query("SELECT 'FOO' LIKE 'foo'"))
.matches("VALUES false");
assertThat(assertions.query("SELECT 'FOO' ILIKE 'foo'"))
.matches("VALUES true");

assertThat(assertions.query("SELECT 'foo' LIKE 'FOO'"))
.matches("VALUES false");
assertThat(assertions.query("SELECT 'foo' ILIKE 'FOO'"))
.matches("VALUES true");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,7 @@ protected String visitLikePredicate(LikePredicate node, Void context)

builder.append('(')
.append(process(node.getValue(), context))
.append(" LIKE ")
.append(node.isCaseInsensitive() ? " ILIKE " : " LIKE ")
.append(process(node.getPattern(), context));

node.getEscape().ifPresent(escape -> builder.append(" ESCAPE ")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2313,7 +2313,25 @@ public Node visitLike(SqlBaseParser.LikeContext context)
getLocation(context),
(Expression) visit(context.value),
(Expression) visit(context.pattern),
visitIfPresent(context.escape, Expression.class));
visitIfPresent(context.escape, Expression.class),
false);

if (context.NOT() != null) {
result = new NotExpression(getLocation(context), result);
}

return result;
}

@Override
public Node visitIlike(SqlBaseParser.IlikeContext context)
{
Expression result = new LikePredicate(
getLocation(context),
(Expression) visit(context.value),
(Expression) visit(context.pattern),
visitIfPresent(context.escape, Expression.class),
true);

if (context.NOT() != null) {
result = new NotExpression(getLocation(context), result);
Expand Down
Loading
Loading