-
Notifications
You must be signed in to change notification settings - Fork 25.7k
EQL: Deal with internally created IN in a different way for EQL #132167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
286cb26
c6f8d73
942f6b9
9bc5c8a
6f35722
d75bebe
4dbf1b3
999fad2
03c348d
44cfa85
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| pr: 132167 | ||
| summary: Deal with internally created IN in a different way for EQL | ||
| area: EQL | ||
| type: bug | ||
| issues: | ||
| - 118621 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -76,47 +76,66 @@ private static Map<String, String[]> getReplacementPatterns() { | |
| public static void main(String[] args) throws IOException { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changes in this class are just for testing purposes, has nothing to do with the actual fix. |
||
| main = true; | ||
| try (RestClient client = RestClient.builder(new HttpHost("localhost", 9200)).build()) { | ||
| loadDatasetIntoEs(client, DataLoader::createParser); | ||
| loadDatasetIntoEsWithIndexCreator(client, DataLoader::createParser, (restClient, indexName, indexMapping) -> { | ||
| // don't use ESRestTestCase methods here or, if you do, test running the main method before making the change | ||
| StringBuilder jsonBody = new StringBuilder("{"); | ||
| jsonBody.append("\"settings\":{\"number_of_shards\":1},"); | ||
| jsonBody.append("\"mappings\":"); | ||
| jsonBody.append(indexMapping); | ||
| jsonBody.append("}"); | ||
|
|
||
| Request request = new Request("PUT", "/" + indexName); | ||
| request.setJsonEntity(jsonBody.toString()); | ||
| restClient.performRequest(request); | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| public static void loadDatasetIntoEs(RestClient client, CheckedBiFunction<XContent, InputStream, XContentParser, IOException> p) | ||
| throws IOException { | ||
| loadDatasetIntoEsWithIndexCreator(client, p, (restClient, indexName, indexMapping) -> { | ||
| ESRestTestCase.createIndex(restClient, indexName, Settings.builder().put("number_of_shards", 1).build(), indexMapping, null); | ||
| }); | ||
| } | ||
|
|
||
| private static void loadDatasetIntoEsWithIndexCreator(RestClient client, CheckedBiFunction<XContent, InputStream, XContentParser, IOException> p, IndexCreator indexCreator) | ||
| throws IOException { | ||
|
|
||
| // | ||
| // Main Index | ||
| // | ||
| load(client, TEST_INDEX, null, DataLoader::timestampToUnixMillis, p); | ||
| load(client, TEST_INDEX, null, DataLoader::timestampToUnixMillis, p, indexCreator); | ||
| // | ||
| // Aux Index | ||
| // | ||
| load(client, TEST_EXTRA_INDEX, null, null, p); | ||
| load(client, TEST_EXTRA_INDEX, null, null, p, indexCreator); | ||
| // | ||
| // Date_Nanos index | ||
| // | ||
| // The data for this index is loaded from the same endgame-140.data sample, only having the mapping for @timestamp changed: the | ||
| // chosen Windows filetime timestamps (2017+) can coincidentally also be readily used as nano-resolution unix timestamps (1973+). | ||
| // There are mixed values with and without nanos precision so that the filtering is properly tested for both cases. | ||
| load(client, TEST_NANOS_INDEX, TEST_INDEX, DataLoader::timestampToUnixNanos, p); | ||
| load(client, TEST_SAMPLE, null, null, p); | ||
| load(client, TEST_NANOS_INDEX, TEST_INDEX, DataLoader::timestampToUnixNanos, p, indexCreator); | ||
| load(client, TEST_SAMPLE, null, null, p, indexCreator); | ||
| // | ||
| // missing_events index | ||
| // | ||
| load(client, TEST_MISSING_EVENTS_INDEX, null, null, p); | ||
| load(client, TEST_SAMPLE_MULTI, null, null, p); | ||
| load(client, TEST_MISSING_EVENTS_INDEX, null, null, p, indexCreator); | ||
| load(client, TEST_SAMPLE_MULTI, null, null, p, indexCreator); | ||
| // | ||
| // index with a runtime field ("broken", type long) that causes shard failures. | ||
| // the rest of the mapping is the same as TEST_INDEX | ||
| // | ||
| load(client, TEST_SHARD_FAILURES_INDEX, null, DataLoader::timestampToUnixMillis, p); | ||
| load(client, TEST_SHARD_FAILURES_INDEX, null, DataLoader::timestampToUnixMillis, p, indexCreator); | ||
| } | ||
|
|
||
| private static void load( | ||
| RestClient client, | ||
| String indexNames, | ||
| String dataName, | ||
| Consumer<Map<String, Object>> datasetTransform, | ||
| CheckedBiFunction<XContent, InputStream, XContentParser, IOException> p | ||
| CheckedBiFunction<XContent, InputStream, XContentParser, IOException> p, | ||
| IndexCreator indexCreator | ||
| ) throws IOException { | ||
| String[] splitNames = indexNames.split(","); | ||
| for (String indexName : splitNames) { | ||
|
|
@@ -130,15 +149,11 @@ private static void load( | |
| if (data == null) { | ||
| throw new IllegalArgumentException("Cannot find resource " + name); | ||
| } | ||
| createTestIndex(client, indexName, readMapping(mapping)); | ||
| indexCreator.createIndex(client, indexName, readMapping(mapping)); | ||
| loadData(client, indexName, datasetTransform, data, p); | ||
| } | ||
| } | ||
|
|
||
| private static void createTestIndex(RestClient client, String indexName, String mapping) throws IOException { | ||
| ESRestTestCase.createIndex(client, indexName, Settings.builder().put("number_of_shards", 1).build(), mapping, null); | ||
| } | ||
|
|
||
| /** | ||
| * Reads the mapping file, ignoring comments and replacing placeholders for random types. | ||
| */ | ||
|
|
@@ -236,4 +251,8 @@ private static XContentParser createParser(XContent xContent, InputStream data) | |
| NamedXContentRegistry contentRegistry = new NamedXContentRegistry(ClusterModule.getNamedXWriteables()); | ||
| return xContent.createParser(contentRegistry, LoggingDeprecationHandler.INSTANCE, data); | ||
| } | ||
|
|
||
| private interface IndexCreator { | ||
| void createIndex(RestClient client, String indexName, String mapping) throws IOException; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -177,6 +177,10 @@ protected TypeResolution resolveType() { | |
| return super.resolveType(); | ||
| } | ||
|
|
||
| public TypeResolution validateInTypes() { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've added this one because I didn't want to change the |
||
| return resolveType(); | ||
| } | ||
|
|
||
| @Override | ||
| public int hashCode() { | ||
| return Objects.hash(value, list); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1203,8 +1203,8 @@ private static boolean notEqualsIsRemovableFromConjunction(NotEquals notEquals, | |
| * 2. a == 1 OR a IN (2) becomes a IN (1, 2) | ||
| * 3. a IN (1) OR a IN (2) becomes a IN (1, 2) | ||
| * | ||
| * This rule does NOT check for type compatibility as that phase has been | ||
| * already be verified in the analyzer. | ||
| * By default (see {@link #shouldValidateIn()}), this rule does NOT check for type compatibility as that phase has | ||
| * already been verified in the analyzer, but this behavior can be changed by subclasses. | ||
| */ | ||
| public static class CombineDisjunctionsToIn extends OptimizerExpressionRule<Or> { | ||
| public CombineDisjunctionsToIn() { | ||
|
|
@@ -1214,18 +1214,24 @@ public CombineDisjunctionsToIn() { | |
| @Override | ||
| protected Expression rule(Or or) { | ||
| Expression e = or; | ||
| // look only at equals and In | ||
| // look only at Equals and In | ||
| List<Expression> exps = splitOr(e); | ||
|
|
||
| Map<Expression, Set<Expression>> found = new LinkedHashMap<>(); | ||
| Map<Expression, List<Expression>> originalOrs = new LinkedHashMap<>(); | ||
| ZoneId zoneId = null; | ||
| List<Expression> ors = new LinkedList<>(); | ||
|
|
||
| for (Expression exp : exps) { | ||
| if (exp instanceof Equals eq) { | ||
| // consider only equals against foldables | ||
| // consider only Equals against foldables | ||
| if (eq.right().foldable()) { | ||
| found.computeIfAbsent(eq.left(), k -> new LinkedHashSet<>()).add(eq.right()); | ||
| if (shouldValidateIn()) { | ||
| // in case there is an optimized In being built and its validation fails, rebuild the original ORs | ||
| // so, keep around the original Expressions | ||
| originalOrs.computeIfAbsent(eq.left(), k -> new ArrayList<>()).add(eq); | ||
| } | ||
| } else { | ||
| ors.add(exp); | ||
| } | ||
|
|
@@ -1234,6 +1240,11 @@ protected Expression rule(Or or) { | |
| } | ||
| } else if (exp instanceof In in) { | ||
| found.computeIfAbsent(in.value(), k -> new LinkedHashSet<>()).addAll(in.list()); | ||
| if (shouldValidateIn()) { | ||
| // in case there is an optimized In being built and its validation fails, rebuild the original ORs | ||
| // so, keep around the original Expressions | ||
| originalOrs.computeIfAbsent(in.value(), k -> new ArrayList<>()).add(in); | ||
| } | ||
| if (zoneId == null) { | ||
| zoneId = in.zoneId(); | ||
| } | ||
|
|
@@ -1243,10 +1254,32 @@ protected Expression rule(Or or) { | |
| } | ||
|
|
||
| if (found.isEmpty() == false) { | ||
| // combine equals alongside the existing ors | ||
| // combine Equals alongside the existing ORs | ||
| final ZoneId finalZoneId = zoneId; | ||
| found.forEach( | ||
| (k, v) -> { ors.add(v.size() == 1 ? createEquals(k, v, finalZoneId) : createIn(k, new ArrayList<>(v), finalZoneId)); } | ||
| (k, v) -> { | ||
| if (v.size() == 1) { | ||
| ors.add(createEquals(k, v.iterator().next(), finalZoneId)); | ||
| } else { | ||
| In in = createIn(k, new ArrayList<>(v), finalZoneId); | ||
| // IN has its own particularities when it comes to type resolution and not all implementations | ||
| // double check the validity of an internally created IN (like the one created here). EQL is one where the IN | ||
| // implementation is like this mechanism here has been specifically created for it | ||
| if (shouldValidateIn()) { | ||
| Expression.TypeResolution resolution = in.validateInTypes(); | ||
|
||
| if (resolution.unresolved()) { | ||
| // if the internally created In is not valid, fall back to the original ORs | ||
| assert originalOrs.containsKey(k); | ||
| assert originalOrs.get(k).isEmpty() == false; | ||
| ors.add(combineOr(originalOrs.get(k))); | ||
| } else { | ||
| ors.add(in); | ||
| } | ||
| } else { | ||
| ors.add(in); | ||
| } | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| Expression combineOr = combineOr(ors); | ||
|
|
@@ -1261,13 +1294,17 @@ protected Expression rule(Or or) { | |
| return e; | ||
| } | ||
|
|
||
| protected Equals createEquals(Expression k, Set<Expression> v, ZoneId finalZoneId) { | ||
| return new Equals(k.source(), k, v.iterator().next(), finalZoneId); | ||
| } | ||
|
|
||
| protected In createIn(Expression key, List<Expression> values, ZoneId zoneId) { | ||
| return new In(key.source(), key, values, zoneId); | ||
| } | ||
|
|
||
| protected boolean shouldValidateIn() { | ||
| return false; | ||
| } | ||
|
|
||
| private Equals createEquals(Expression key, Expression value, ZoneId finalZoneId) { | ||
| return new Equals(key.source(), key, value, finalZoneId); | ||
| } | ||
| } | ||
|
|
||
| public static class PushDownAndCombineFilters extends OptimizerRule<Filter> { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just for testing purposes, has nothing to do with the actual fix.