Skip to content

Commit 7a6df3c

Browse files
committed
regrex poc with pcre4j - no logical plan yet
Signed-off-by: Jialiang Liang <jiallian@amazon.com>
1 parent f487cab commit 7a6df3c

6 files changed

Lines changed: 224 additions & 0 deletions

File tree

core/build.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ plugins {
3434

3535
repositories {
3636
mavenCentral()
37+
maven { url 'https://jitpack.io' }
3738
}
3839

3940
pitest {
@@ -57,6 +58,13 @@ dependencies {
5758
api group: 'com.google.code.gson', name: 'gson', version: '2.8.9'
5859
api group: 'com.tdunning', name: 't-digest', version: '3.3'
5960
api "net.minidev:json-smart:${versions.json_smart}"
61+
// Using pcre4j for PCRE2 support
62+
api(group: 'org.pcre4j', name: 'regex', version: '0.4.3') {
63+
exclude group: 'net.java.dev.jna', module: 'jna'
64+
}
65+
api(group: 'org.pcre4j', name: 'jna', version: '0.4.3') {
66+
exclude group: 'net.java.dev.jna', module: 'jna'
67+
}
6068
api('org.apache.calcite:calcite-core:1.38.0') {
6169
exclude group: 'net.minidev', module: 'json-smart'
6270
exclude group: 'commons-lang', module: 'commons-lang'

core/src/main/java/org/opensearch/sql/analysis/Analyzer.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
import org.opensearch.sql.ast.tree.Relation;
8181
import org.opensearch.sql.ast.tree.RelationSubquery;
8282
import org.opensearch.sql.ast.tree.Rename;
83+
import org.opensearch.sql.ast.tree.Regex;
8384
import org.opensearch.sql.ast.tree.Reverse;
8485
import org.opensearch.sql.ast.tree.Sort;
8586
import org.opensearch.sql.ast.tree.Sort.SortOption;
@@ -689,6 +690,14 @@ public LogicalPlan visitReverse(Reverse node, AnalysisContext context) {
689690
"REVERSE is supported only when " + CALCITE_ENGINE_ENABLED.getKeyValue() + "=true");
690691
}
691692

693+
@Override
694+
public LogicalPlan visitRegex(Regex node, AnalysisContext context) {
695+
// For now, throw unsupported operation as we're building a PoC
696+
// This will be implemented when we add the execution logic
697+
throw new UnsupportedOperationException(
698+
"REGEX command is not yet fully implemented");
699+
}
700+
692701
@Override
693702
public LogicalPlan visitPaginate(Paginate paginate, AnalysisContext context) {
694703
LogicalPlan child = paginate.getChild().get(0).accept(this, context);

core/src/main/java/org/opensearch/sql/ast/AbstractNodeVisitor.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
import org.opensearch.sql.ast.tree.Relation;
7070
import org.opensearch.sql.ast.tree.RelationSubquery;
7171
import org.opensearch.sql.ast.tree.Rename;
72+
import org.opensearch.sql.ast.tree.Regex;
7273
import org.opensearch.sql.ast.tree.Reverse;
7374
import org.opensearch.sql.ast.tree.Sort;
7475
import org.opensearch.sql.ast.tree.SubqueryAlias;
@@ -249,6 +250,10 @@ public T visitReverse(Reverse node, C context) {
249250
return visitChildren(node, context);
250251
}
251252

253+
public T visitRegex(Regex node, C context) {
254+
return visitChildren(node, context);
255+
}
256+
252257
public T visitLambdaFunction(LambdaFunction node, C context) {
253258
return visitChildren(node, context);
254259
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.ast.tree;
7+
8+
import com.google.common.collect.ImmutableList;
9+
import java.util.List;
10+
import lombok.EqualsAndHashCode;
11+
import lombok.Getter;
12+
import lombok.Setter;
13+
import lombok.ToString;
14+
import org.opensearch.sql.ast.AbstractNodeVisitor;
15+
import org.opensearch.sql.ast.expression.Literal;
16+
import org.opensearch.sql.ast.expression.UnresolvedExpression;
17+
18+
/** AST node represent Regex filtering operation. */
19+
@Getter
20+
@ToString
21+
@EqualsAndHashCode(callSuper = false)
22+
public class Regex extends UnresolvedPlan {
23+
/** Field to match against. */
24+
private final UnresolvedExpression field;
25+
26+
/** Whether this is a negated match (!=). */
27+
private final boolean negated;
28+
29+
/** Pattern. */
30+
private final Literal pattern;
31+
32+
/** Child Plan. */
33+
@Setter
34+
private UnresolvedPlan child;
35+
36+
public Regex(UnresolvedExpression field, String operator, Literal pattern) {
37+
// Require explicit field - no default to _source for PoC
38+
this.field = field;
39+
this.negated = "!=".equals(operator);
40+
this.pattern = pattern;
41+
}
42+
43+
@Override
44+
public Regex attach(UnresolvedPlan child) {
45+
this.child = child;
46+
return this;
47+
}
48+
49+
@Override
50+
public List<UnresolvedPlan> getChild() {
51+
return this.child == null ? ImmutableList.of() : ImmutableList.of(this.child);
52+
}
53+
54+
@Override
55+
public <T, C> T accept(AbstractNodeVisitor<T, C> nodeVisitor, C context) {
56+
return nodeVisitor.visitRegex(this, context);
57+
}
58+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.expression.operator.predicate;
7+
8+
import java.util.concurrent.ConcurrentHashMap;
9+
import lombok.EqualsAndHashCode;
10+
import lombok.Getter;
11+
import lombok.ToString;
12+
import org.pcre4j.Pcre4j;
13+
import org.pcre4j.jna.Pcre2;
14+
import org.pcre4j.regex.Pattern;
15+
import org.pcre4j.regex.Matcher;
16+
import org.opensearch.sql.data.model.ExprBooleanValue;
17+
import org.opensearch.sql.data.model.ExprValue;
18+
import org.opensearch.sql.data.model.ExprValueUtils;
19+
import org.opensearch.sql.data.type.ExprCoreType;
20+
import org.opensearch.sql.data.type.ExprType;
21+
import org.opensearch.sql.expression.Expression;
22+
import org.opensearch.sql.expression.ExpressionNodeVisitor;
23+
import org.opensearch.sql.expression.env.Environment;
24+
import org.opensearch.sql.expression.function.FunctionName;
25+
26+
/**
27+
* Expression for PCRE-compatible regex matching using JPCRE2.
28+
* Supports full PCRE features including:
29+
* - Named groups (?<name>...)
30+
* - Lookahead/lookbehind (including variable-length)
31+
* - Backreferences
32+
* - Recursion (?R) and named recursion (?&name)
33+
* - Conditionals (?(condition)yes|no)
34+
* - Inline flags (?i), (?m), (?s), etc.
35+
*/
36+
@ToString
37+
@EqualsAndHashCode
38+
public class RegexMatch implements Expression {
39+
@Getter
40+
private final Expression field;
41+
42+
@Getter
43+
private final Expression pattern;
44+
45+
@Getter
46+
private final boolean negated;
47+
48+
// Pattern cache to avoid recompiling the same patterns
49+
private static final ConcurrentHashMap<String, Pattern> patternCache =
50+
new ConcurrentHashMap<>();
51+
52+
// Maximum cache size to prevent memory issues
53+
private static final int MAX_CACHE_SIZE = 1000;
54+
55+
// Initialize PCRE4J with JNA backend (done once)
56+
static {
57+
Pcre4j.setup(new Pcre2());
58+
}
59+
60+
public RegexMatch(Expression field, Expression pattern, boolean negated) {
61+
this.field = field;
62+
this.pattern = pattern;
63+
this.negated = negated;
64+
}
65+
66+
@Override
67+
public ExprValue valueOf(Environment<Expression, ExprValue> valueEnv) {
68+
ExprValue fieldValue = field.valueOf(valueEnv);
69+
ExprValue patternValue = pattern.valueOf(valueEnv);
70+
71+
// Handle null/missing values
72+
if (fieldValue.isNull() || fieldValue.isMissing() ||
73+
patternValue.isNull() || patternValue.isMissing()) {
74+
return ExprValueUtils.booleanValue(false);
75+
}
76+
77+
String text = fieldValue.stringValue();
78+
String regex = patternValue.stringValue();
79+
80+
try {
81+
// Get compiled pattern from cache or compile new one
82+
Pattern compiledPattern = getCompiledPattern(regex);
83+
84+
// Create matcher and check for match
85+
Matcher matcher = compiledPattern.matcher(text);
86+
boolean matches = matcher.find(); // Use find() for partial match like SPL
87+
88+
// Apply negation if needed
89+
return ExprValueUtils.booleanValue(negated ? !matches : matches);
90+
91+
} catch (Exception e) {
92+
// Log error and return false on pattern compilation/matching errors
93+
// In production, you'd want proper logging here
94+
System.err.println("Regex error: " + e.getMessage());
95+
return ExprValueUtils.booleanValue(false);
96+
}
97+
}
98+
99+
/**
100+
* Get compiled pattern from cache or compile and cache it.
101+
*/
102+
private Pattern getCompiledPattern(String regex) {
103+
// Check cache size and clear if needed (simple LRU-like behavior)
104+
if (patternCache.size() > MAX_CACHE_SIZE) {
105+
patternCache.clear();
106+
}
107+
108+
return patternCache.computeIfAbsent(regex, r -> {
109+
// Compile with PCRE2 defaults
110+
// pcre4j compiles the pattern with full PCRE2 support
111+
return Pattern.compile(r);
112+
});
113+
}
114+
115+
@Override
116+
public ExprType type() {
117+
return ExprCoreType.BOOLEAN;
118+
}
119+
120+
@Override
121+
public <T, C> T accept(ExpressionNodeVisitor<T, C> visitor, C context) {
122+
// This will be implemented when we add the visitor pattern for expressions
123+
return visitor.visitNode(this, context);
124+
}
125+
}

ppl/src/main/java/org/opensearch/sql/ppl/parser/AstBuilder.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.StatsByClauseContext;
9797
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParserBaseVisitor;
9898
import org.opensearch.sql.ppl.utils.ArgumentFactory;
99+
import org.opensearch.sql.ast.tree.Regex;
99100

100101
/** Class of building the AST. Refines the visit path and build the AST nodes */
101102
public class AstBuilder extends OpenSearchPPLParserBaseVisitor<UnresolvedPlan> {
@@ -688,6 +689,24 @@ public UnresolvedPlan visitAppendcolCommand(OpenSearchPPLParser.AppendcolCommand
688689
return new AppendCol(override, subsearch.get());
689690
}
690691

692+
@Override
693+
public UnresolvedPlan visitRegexCommand(OpenSearchPPLParser.RegexCommandContext ctx) {
694+
UnresolvedExpression field = null;
695+
String operator = null;
696+
Literal pattern = (Literal) internalVisitExpression(ctx.regexExpr().pattern);
697+
698+
if (ctx.regexExpr().field != null) {
699+
field = internalVisitExpression(ctx.regexExpr().field);
700+
}
701+
if (ctx.regexExpr().EQUAL() != null) {
702+
operator = "=";
703+
} else if (ctx.regexExpr().NOT_EQUAL() != null) {
704+
operator = "!=";
705+
}
706+
707+
return new Regex(field, operator, pattern);
708+
}
709+
691710
/** Get original text in query. */
692711
private String getTextInQuery(ParserRuleContext ctx) {
693712
Token start = ctx.getStart();

0 commit comments

Comments
 (0)