diff --git a/docs/changelog/134098.yaml b/docs/changelog/134098.yaml new file mode 100644 index 0000000000000..9654475c5e551 --- /dev/null +++ b/docs/changelog/134098.yaml @@ -0,0 +1,5 @@ +pr: 134098 +summary: LOOKUP JOIN with expressions +area: ES|QL +type: enhancement +issues: [] diff --git a/docs/reference/query-languages/esql/_snippets/commands/types/lookup-join.md b/docs/reference/query-languages/esql/_snippets/commands/types/lookup-join.md index 3e54f0ad66277..f4be29cd9954d 100644 --- a/docs/reference/query-languages/esql/_snippets/commands/types/lookup-join.md +++ b/docs/reference/query-languages/esql/_snippets/commands/types/lookup-join.md @@ -5,17 +5,17 @@ | field from the left index | field from the lookup index | | --- | --- | | boolean | boolean | -| byte | half_float, float, double, scaled_float, byte, short, integer, long | +| byte | byte, short, integer, long, half_float, float, double, scaled_float | | date | date | | date_nanos | date_nanos | | double | half_float, float, double, scaled_float, byte, short, integer, long | | float | half_float, float, double, scaled_float, byte, short, integer, long | | half_float | half_float, float, double, scaled_float, byte, short, integer, long | -| integer | half_float, float, double, scaled_float, byte, short, integer, long | +| integer | byte, short, integer, long, half_float, float, double, scaled_float | | ip | ip | | keyword | keyword | -| long | half_float, float, double, scaled_float, byte, short, integer, long | +| long | byte, short, integer, long, half_float, float, double, scaled_float | | scaled_float | half_float, float, double, scaled_float, byte, short, integer, long | -| short | half_float, float, double, scaled_float, byte, short, integer, long | +| short | byte, short, integer, long, half_float, float, double, scaled_float | | text | keyword | diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java index cb18fc50d4f61..b8cb648f00779 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersions.java +++ b/server/src/main/java/org/elasticsearch/TransportVersions.java @@ -327,6 +327,7 @@ static TransportVersion def(int id) { public static final TransportVersion TIMESERIES_DEFAULT_LIMIT = def(9_160_0_00); public static final TransportVersion INFERENCE_API_OPENAI_HEADERS = def(9_161_0_00); public static final TransportVersion NEW_SEMANTIC_QUERY_INTERCEPTORS = def(9_162_0_00); + public static final TransportVersion ESQL_LOOKUP_JOIN_ON_EXPRESSION = def(9_163_0_00); /* * STOP! READ THIS FIRST! No, really, diff --git a/test/external-modules/esql-heap-attack/src/javaRestTest/java/org/elasticsearch/xpack/esql/heap_attack/HeapAttackIT.java b/test/external-modules/esql-heap-attack/src/javaRestTest/java/org/elasticsearch/xpack/esql/heap_attack/HeapAttackIT.java index d106f7bf4d740..cfb1395816e8e 100644 --- a/test/external-modules/esql-heap-attack/src/javaRestTest/java/org/elasticsearch/xpack/esql/heap_attack/HeapAttackIT.java +++ b/test/external-modules/esql-heap-attack/src/javaRestTest/java/org/elasticsearch/xpack/esql/heap_attack/HeapAttackIT.java @@ -566,6 +566,7 @@ private Response query(String query, String filterPath) throws IOException { .setRequestConfig(RequestConfig.custom().setSocketTimeout(Math.toIntExact(TimeValue.timeValueMinutes(6).millis())).build()) .setWarningsHandler(WarningsHandler.PERMISSIVE) ); + logger.info("Running query:" + query); return runQuery(() -> client().performRequest(request)); } @@ -735,7 +736,7 @@ private Map fetchMvLongs() throws IOException { public void testLookupExplosion() throws IOException { int sensorDataCount = 400; int lookupEntries = 10000; - Map map = lookupExplosion(sensorDataCount, lookupEntries, 1, lookupEntries); + Map map = lookupExplosion(sensorDataCount, lookupEntries, 1, lookupEntries, false); assertMap(map, matchesMap().extraOk().entry("values", List.of(List.of(sensorDataCount * lookupEntries)))); } @@ -743,20 +744,40 @@ public void testLookupExplosionManyFields() throws IOException { int sensorDataCount = 400; int lookupEntries = 1000; int joinFieldsCount = 990; - Map map = lookupExplosion(sensorDataCount, lookupEntries, joinFieldsCount, lookupEntries); + Map map = lookupExplosion(sensorDataCount, lookupEntries, joinFieldsCount, lookupEntries, false); + assertMap(map, matchesMap().extraOk().entry("values", List.of(List.of(sensorDataCount * lookupEntries)))); + } + + public void testLookupExplosionExpression() throws IOException { + int sensorDataCount = 400; + int lookupEntries = 10000; + Map map = lookupExplosion(sensorDataCount, lookupEntries, 1, lookupEntries, true); + assertMap(map, matchesMap().extraOk().entry("values", List.of(List.of(sensorDataCount * lookupEntries)))); + } + + public void testLookupExplosionManyFieldsExpression() throws IOException { + int sensorDataCount = 400; + int lookupEntries = 1000; + int joinFieldsCount = 399;// only join on 399 columns due to max expression size of 400 + Map map = lookupExplosion(sensorDataCount, lookupEntries, joinFieldsCount, lookupEntries, true); assertMap(map, matchesMap().extraOk().entry("values", List.of(List.of(sensorDataCount * lookupEntries)))); } public void testLookupExplosionManyMatchesManyFields() throws IOException { // 1500, 10000 is enough locally, but some CI machines need more. int lookupEntries = 10000; - assertCircuitBreaks(attempt -> lookupExplosion(attempt * 1500, lookupEntries, 30, lookupEntries)); + assertCircuitBreaks(attempt -> lookupExplosion(attempt * 1500, lookupEntries, 30, lookupEntries, false)); } public void testLookupExplosionManyMatches() throws IOException { // 1500, 10000 is enough locally, but some CI machines need more. int lookupEntries = 10000; - assertCircuitBreaks(attempt -> lookupExplosion(attempt * 1500, lookupEntries, 1, lookupEntries)); + assertCircuitBreaks(attempt -> lookupExplosion(attempt * 1500, lookupEntries, 1, lookupEntries, false)); + } + + public void testLookupExplosionManyMatchesExpression() throws IOException { + int lookupEntries = 10000; + assertCircuitBreaks(attempt -> lookupExplosion(attempt * 1500, lookupEntries, 1, lookupEntries, true)); } public void testLookupExplosionManyMatchesFiltered() throws IOException { @@ -768,9 +789,21 @@ public void testLookupExplosionManyMatchesFiltered() throws IOException { int reductionFactor = 1000; // reduce the number of matches by this factor // lookupEntries % reductionFactor must be 0 to ensure the number of rows returned matches the expected value assertTrue(0 == lookupEntries % reductionFactor); - Map map = lookupExplosion(sensorDataCount, lookupEntries, 1, lookupEntries / reductionFactor); + Map map = lookupExplosion(sensorDataCount, lookupEntries, 1, lookupEntries / reductionFactor, false); assertMap(map, matchesMap().extraOk().entry("values", List.of(List.of(sensorDataCount * lookupEntries / reductionFactor)))); + } + public void testLookupExplosionManyMatchesFilteredExpression() throws IOException { + // This test will only work with the expanding join optimization + // that pushes the filter to the right side of the lookup. + // Without the optimization, it will fail with circuit_breaking_exception + int sensorDataCount = 10000; + int lookupEntries = 10000; + int reductionFactor = 1000; // reduce the number of matches by this factor + // lookupEntries % reductionFactor must be 0 to ensure the number of rows returned matches the expected value + assertTrue(0 == lookupEntries % reductionFactor); + Map map = lookupExplosion(sensorDataCount, lookupEntries, 1, lookupEntries / reductionFactor, true); + assertMap(map, matchesMap().extraOk().entry("values", List.of(List.of(sensorDataCount * lookupEntries / reductionFactor)))); } public void testLookupExplosionNoFetch() throws IOException { @@ -797,17 +830,33 @@ public void testLookupExplosionBigStringManyMatches() throws IOException { assertCircuitBreaks(attempt -> lookupExplosionBigString(attempt * 500, 1)); } - private Map lookupExplosion(int sensorDataCount, int lookupEntries, int joinFieldsCount, int lookupEntriesToKeep) - throws IOException { + private Map lookupExplosion( + int sensorDataCount, + int lookupEntries, + int joinFieldsCount, + int lookupEntriesToKeep, + boolean expressionBasedJoin + ) throws IOException { try { - lookupExplosionData(sensorDataCount, lookupEntries, joinFieldsCount); + lookupExplosionData(sensorDataCount, lookupEntries, joinFieldsCount, expressionBasedJoin); StringBuilder query = startQuery(); query.append("FROM sensor_data | LOOKUP JOIN sensor_lookup ON "); - for (int i = 0; i < joinFieldsCount; i++) { - if (i != 0) { - query.append(","); + if (expressionBasedJoin) { + for (int i = 0; i < joinFieldsCount; i++) { + if (i != 0) { + query.append(" AND "); + } + query.append("id_left").append(i); + query.append("=="); + query.append("id_right").append(i); + } + } else { + for (int i = 0; i < joinFieldsCount; i++) { + if (i != 0) { + query.append(","); + } + query.append("id").append(i); } - query.append("id").append(i); } if (lookupEntries != lookupEntriesToKeep) { // add a filter to reduce the number of matches @@ -826,7 +875,7 @@ private Map lookupExplosion(int sensorDataCount, int lookupEntri private Map lookupExplosionNoFetch(int sensorDataCount, int lookupEntries) throws IOException { try { - lookupExplosionData(sensorDataCount, lookupEntries, 1); + lookupExplosionData(sensorDataCount, lookupEntries, 1, false); StringBuilder query = startQuery(); query.append("FROM sensor_data | LOOKUP JOIN sensor_lookup ON id0 | STATS COUNT(*)\"}"); return responseAsMap(query(query.toString(), null)); @@ -836,14 +885,15 @@ private Map lookupExplosionNoFetch(int sensorDataCount, int look } } - private void lookupExplosionData(int sensorDataCount, int lookupEntries, int joinFieldCount) throws IOException { - initSensorData(sensorDataCount, 1, joinFieldCount); - initSensorLookup(lookupEntries, 1, i -> "73.9857 40.7484", joinFieldCount); + private void lookupExplosionData(int sensorDataCount, int lookupEntries, int joinFieldCount, boolean expressionBasedJoin) + throws IOException { + initSensorData(sensorDataCount, 1, joinFieldCount, expressionBasedJoin); + initSensorLookup(lookupEntries, 1, i -> "73.9857 40.7484", joinFieldCount, expressionBasedJoin); } private Map lookupExplosionBigString(int sensorDataCount, int lookupEntries) throws IOException { try { - initSensorData(sensorDataCount, 1, 1); + initSensorData(sensorDataCount, 1, 1, false); initSensorLookupString(lookupEntries, 1, i -> { int target = Math.toIntExact(ByteSizeValue.ofMb(1).getBytes()); StringBuilder str = new StringBuilder(Math.toIntExact(ByteSizeValue.ofMb(2).getBytes())); @@ -876,7 +926,7 @@ public void testEnrichExplosionManyMatches() throws IOException { private Map enrichExplosion(int sensorDataCount, int lookupEntries) throws IOException { try { - initSensorData(sensorDataCount, 1, 1); + initSensorData(sensorDataCount, 1, 1, false); initSensorEnrich(lookupEntries, 1, i -> "73.9857 40.7484"); try { StringBuilder query = startQuery(); @@ -1050,7 +1100,7 @@ private void initMvLongsIndex(int docs, int fields, int fieldValues) throws IOEx initIndex("mv_longs", bulk.toString()); } - private void initSensorData(int docCount, int sensorCount, int joinFieldCount) throws IOException { + private void initSensorData(int docCount, int sensorCount, int joinFieldCount, boolean expressionBasedJoin) throws IOException { logger.info("loading sensor data"); // We cannot go over 1000 fields, due to failed on parsing mappings on index creation // [sensor_data] java.lang.IllegalArgumentException: Limit of total fields [1000] has been exceeded @@ -1061,8 +1111,9 @@ private void initSensorData(int docCount, int sensorCount, int joinFieldCount) t "properties": { "@timestamp": { "type": "date" }, """); + String suffix = expressionBasedJoin ? "_left" : ""; for (int i = 0; i < joinFieldCount; i++) { - createIndexBuilder.append("\"id").append(i).append("\": { \"type\": \"long\" },"); + createIndexBuilder.append("\"id").append(suffix).append(i).append("\": { \"type\": \"long\" },"); } createIndexBuilder.append(""" "value": { "type": "double" } @@ -1083,7 +1134,7 @@ private void initSensorData(int docCount, int sensorCount, int joinFieldCount) t {"create":{}} {"timestamp":"%s",""", DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.formatMillis(i * 10L + firstDate))); for (int j = 0; j < joinFieldCount; j++) { - data.append(String.format(Locale.ROOT, "\"id%d\":%d, ", j, i % sensorCount)); + data.append(String.format(Locale.ROOT, "\"id%s%d\":%d, ", suffix, j, i % sensorCount)); } data.append(String.format(Locale.ROOT, "\"value\": %f}\n", i * 1.1)); if (i % docsPerBulk == docsPerBulk - 1) { @@ -1094,8 +1145,13 @@ private void initSensorData(int docCount, int sensorCount, int joinFieldCount) t initIndex("sensor_data", data.toString()); } - private void initSensorLookup(int lookupEntries, int sensorCount, IntFunction location, int joinFieldsCount) - throws IOException { + private void initSensorLookup( + int lookupEntries, + int sensorCount, + IntFunction location, + int joinFieldsCount, + boolean expressionBasedJoin + ) throws IOException { logger.info("loading sensor lookup"); // cannot go over 1000 fields, due to failed on parsing mappings on index creation // [sensor_data] java.lang.IllegalArgumentException: Limit of total fields [1000] has been exceeded @@ -1105,8 +1161,9 @@ private void initSensorLookup(int lookupEntries, int sensorCount, IntFunction location) throws IOException { - initSensorLookup(lookupEntries, sensorCount, location, 1); + initSensorLookup(lookupEntries, sensorCount, location, 1, false); logger.info("loading sensor enrich"); Request create = new Request("PUT", "/_enrich/policy/sensor"); diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/lookup/QueryList.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/lookup/QueryList.java index 4d93ab63aaa5d..f300a400e60af 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/lookup/QueryList.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/lookup/QueryList.java @@ -125,7 +125,7 @@ public final Query getQuery(int position) { * Returns the query at the given position. */ @Nullable - abstract Query doGetQuery(int position, int firstValueIndex, int valueCount); + public abstract Query doGetQuery(int position, int firstValueIndex, int valueCount); private Query wrapSingleValueQuery(Query query) { assert onlySingleValueParams != null : "Requested to wrap single value query without single value params"; @@ -155,17 +155,11 @@ private Query wrapSingleValueQuery(Query query) { } /** - * Returns a list of term queries for the given field and the input block - * using only the {@link ElementType} of the {@link Block} to determine the - * query. + * Returns a function that reads values from the given block. The function + * takes the offset of the value to read and returns the value as an {@link Object}. */ - public static QueryList rawTermQueryList( - MappedFieldType field, - SearchExecutionContext searchExecutionContext, - AliasFilter aliasFilter, - Block block - ) { - IntFunction blockToJavaObject = switch (block.elementType()) { + public static IntFunction createBlockValueReader(Block block) { + return switch (block.elementType()) { case BOOLEAN -> { BooleanBlock booleanBlock = (BooleanBlock) block; yield booleanBlock::getBoolean; @@ -196,7 +190,20 @@ public static QueryList rawTermQueryList( case AGGREGATE_METRIC_DOUBLE -> throw new IllegalArgumentException("can't read values from [aggregate metric double] block"); case UNKNOWN -> throw new IllegalArgumentException("can't read values from [" + block + "]"); }; - return new TermQueryList(field, searchExecutionContext, aliasFilter, block, null, blockToJavaObject); + } + + /** + * Returns a list of term queries for the given field and the input block + * using only the {@link ElementType} of the {@link Block} to determine the + * query. + */ + public static QueryList rawTermQueryList( + MappedFieldType field, + SearchExecutionContext searchExecutionContext, + AliasFilter aliasFilter, + Block block + ) { + return new TermQueryList(field, searchExecutionContext, aliasFilter, block, null, createBlockValueReader(block)); } /** @@ -297,7 +304,7 @@ public TermQueryList onlySingleValues(Warnings warnings, String multiValueWarnin } @Override - Query doGetQuery(int position, int firstValueIndex, int valueCount) { + public Query doGetQuery(int position, int firstValueIndex, int valueCount) { return switch (valueCount) { case 0 -> null; case 1 -> field.termQuery(blockValueReader.apply(firstValueIndex), searchExecutionContext); @@ -360,7 +367,7 @@ public DateNanosQueryList onlySingleValues(Warnings warnings, String multiValueW } @Override - Query doGetQuery(int position, int firstValueIndex, int valueCount) { + public Query doGetQuery(int position, int firstValueIndex, int valueCount) { return switch (valueCount) { case 0 -> null; case 1 -> dateFieldType.equalityQuery(blockValueReader.apply(firstValueIndex), searchExecutionContext); @@ -412,7 +419,7 @@ public GeoShapeQueryList onlySingleValues(Warnings warnings, String multiValueWa } @Override - Query doGetQuery(int position, int firstValueIndex, int valueCount) { + public Query doGetQuery(int position, int firstValueIndex, int valueCount) { return switch (valueCount) { case 0 -> null; case 1 -> shapeQuery.apply(firstValueIndex); @@ -453,5 +460,5 @@ private IntFunction shapeQuery() { } } - protected record OnlySingleValueParams(Warnings warnings, String multiValueWarningMessage) {} + public record OnlySingleValueParams(Warnings warnings, String multiValueWarningMessage) {} } diff --git a/x-pack/plugin/esql/compute/test/src/main/java/org/elasticsearch/compute/test/OperatorTestCase.java b/x-pack/plugin/esql/compute/test/src/main/java/org/elasticsearch/compute/test/OperatorTestCase.java index d3a2e173ed3da..c7708699f1498 100644 --- a/x-pack/plugin/esql/compute/test/src/main/java/org/elasticsearch/compute/test/OperatorTestCase.java +++ b/x-pack/plugin/esql/compute/test/src/main/java/org/elasticsearch/compute/test/OperatorTestCase.java @@ -97,7 +97,7 @@ protected ByteSizeValue enoughMemoryForSimple() { * asserting both that this throws a {@link CircuitBreakingException} and releases * all pages. */ - public final void testSimpleCircuitBreaking() { + public void testSimpleCircuitBreaking() { /* * Build the input before building `simple` to handle the rare * cases where `simple` need some state from the input - mostly diff --git a/x-pack/plugin/esql/qa/security/src/javaRestTest/java/org/elasticsearch/xpack/esql/EsqlSecurityIT.java b/x-pack/plugin/esql/qa/security/src/javaRestTest/java/org/elasticsearch/xpack/esql/EsqlSecurityIT.java index 73cd7b77968fe..d5f9ed74c7a92 100644 --- a/x-pack/plugin/esql/qa/security/src/javaRestTest/java/org/elasticsearch/xpack/esql/EsqlSecurityIT.java +++ b/x-pack/plugin/esql/qa/security/src/javaRestTest/java/org/elasticsearch/xpack/esql/EsqlSecurityIT.java @@ -592,15 +592,24 @@ record Listen(long timestamp, String songId, double duration) { } public void testLookupJoinIndexAllowed() throws Exception { + testLookupJoinIndexAllowedHelper(false); + assumeTrue( + "requires LOOKUP JOIN ON boolean expression capability", + EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled() + ); + testLookupJoinIndexAllowedHelper(true); + } + + private void testLookupJoinIndexAllowedHelper(boolean useExpressionJoin) throws Exception { assumeTrue( "Requires LOOKUP JOIN capability", hasCapabilities(adminClient(), List.of(EsqlCapabilities.Cap.JOIN_LOOKUP_V12.capabilityName())) ); - Response resp = runESQLCommand( - "metadata1_read2", - "ROW x = 40.0 | EVAL value = x | LOOKUP JOIN lookup-user2 ON value | KEEP x, org" - ); + String query = useExpressionJoin + ? "ROW x = 40.0 | EVAL value_left = x | LOOKUP JOIN lookup-user2 ON value_left == value | KEEP x, org" + : "ROW x = 40.0 | EVAL value = x | LOOKUP JOIN lookup-user2 ON value | KEEP x, org"; + Response resp = runESQLCommand("metadata1_read2", query); assertOK(resp); Map respMap = entityAsMap(resp); assertThat( @@ -610,25 +619,19 @@ public void testLookupJoinIndexAllowed() throws Exception { assertThat(respMap.get("values"), equalTo(List.of(List.of(40.0, "sales")))); // user is not allowed to use the alias (but is allowed to use the index) - expectThrows( - ResponseException.class, - () -> runESQLCommand( - "metadata1_read2", - "ROW x = 40.0 | EVAL value = x | LOOKUP JOIN lookup-second-alias ON value | KEEP x, org" - ) - ); + String aliasQuery = useExpressionJoin + ? "ROW x = 40.0 | EVAL value_left = x | LOOKUP JOIN lookup-second-alias ON value_left == value | KEEP x, org" + : "ROW x = 40.0 | EVAL value = x | LOOKUP JOIN lookup-second-alias ON value | KEEP x, org"; + expectThrows(ResponseException.class, () -> runESQLCommand("metadata1_read2", aliasQuery)); // user is not allowed to use the index (but is allowed to use the alias) - expectThrows( - ResponseException.class, - () -> runESQLCommand("metadata1_alias_read2", "ROW x = 40.0 | EVAL value = x | LOOKUP JOIN lookup-user2 ON value | KEEP x, org") - ); + String indexQuery = useExpressionJoin + ? "ROW x = 40.0 | EVAL value_left = x | LOOKUP JOIN lookup-user2 ON value_left == value | KEEP x, org" + : "ROW x = 40.0 | EVAL value = x | LOOKUP JOIN lookup-user2 ON value | KEEP x, org"; + expectThrows(ResponseException.class, () -> runESQLCommand("metadata1_alias_read2", indexQuery)); // user has permission on the alias, and can read the key - resp = runESQLCommand( - "metadata1_alias_read2", - "ROW x = 40.0 | EVAL value = x | LOOKUP JOIN lookup-second-alias ON value | KEEP x, org" - ); + resp = runESQLCommand("metadata1_alias_read2", aliasQuery); assertOK(resp); respMap = entityAsMap(resp); assertThat( @@ -638,10 +641,10 @@ public void testLookupJoinIndexAllowed() throws Exception { assertThat(respMap.get("values"), equalTo(List.of(List.of(40.0, "sales")))); // user has permission on the alias, but can't read the key (doc level security at role level) - resp = runESQLCommand( - "metadata1_alias_read2", - "ROW x = 32.0 | EVAL value = x | LOOKUP JOIN lookup-second-alias ON value | KEEP x, org" - ); + String aliasQuery2 = useExpressionJoin + ? "ROW x = 32.0 | EVAL value_left = x | LOOKUP JOIN lookup-second-alias ON value_left == value | KEEP x, org" + : "ROW x = 32.0 | EVAL value = x | LOOKUP JOIN lookup-second-alias ON value | KEEP x, org"; + resp = runESQLCommand("metadata1_alias_read2", aliasQuery2); assertOK(resp); respMap = entityAsMap(resp); assertThat( @@ -656,7 +659,10 @@ public void testLookupJoinIndexAllowed() throws Exception { assertThat(row.get(1), is(nullValue())); // user has permission on the alias, the alias has a filter that doesn't allow to see the value - resp = runESQLCommand("alias_user1", "ROW x = 12.0 | EVAL value = x | LOOKUP JOIN lookup-first-alias ON value | KEEP x, org"); + String aliasQuery3 = useExpressionJoin + ? "ROW x = 12.0 | EVAL value_left = x | LOOKUP JOIN lookup-first-alias ON value_left == value | KEEP x, org" + : "ROW x = 12.0 | EVAL value = x | LOOKUP JOIN lookup-first-alias ON value | KEEP x, org"; + resp = runESQLCommand("alias_user1", aliasQuery3); assertOK(resp); respMap = entityAsMap(resp); assertThat( @@ -671,7 +677,10 @@ public void testLookupJoinIndexAllowed() throws Exception { assertThat(row.get(1), is(nullValue())); // user has permission on the alias, the alias has a filter that allows to see the value - resp = runESQLCommand("alias_user1", "ROW x = 31.0 | EVAL value = x | LOOKUP JOIN lookup-first-alias ON value | KEEP x, org"); + String aliasQuery4 = useExpressionJoin + ? "ROW x = 31.0 | EVAL value_left = x | LOOKUP JOIN lookup-first-alias ON value_left == value | KEEP x, org" + : "ROW x = 31.0 | EVAL value = x | LOOKUP JOIN lookup-first-alias ON value | KEEP x, org"; + resp = runESQLCommand("alias_user1", aliasQuery4); assertOK(resp); respMap = entityAsMap(resp); assertThat( @@ -683,12 +692,23 @@ public void testLookupJoinIndexAllowed() throws Exception { @SuppressWarnings("unchecked") public void testLookupJoinDocLevelSecurity() throws Exception { + testLookupJoinDocLevelSecurityHelper(false); + assumeTrue( + "requires LOOKUP JOIN ON boolean expression capability", + EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled() + ); + testLookupJoinDocLevelSecurityHelper(true); + } + + private void testLookupJoinDocLevelSecurityHelper(boolean useExpressionJoin) throws Exception { assumeTrue( "Requires LOOKUP JOIN capability", hasCapabilities(adminClient(), List.of(EsqlCapabilities.Cap.JOIN_LOOKUP_V12.capabilityName())) ); - - Response resp = runESQLCommand("dls_user", "ROW x = 40.0 | EVAL value = x | LOOKUP JOIN lookup-user2 ON value | KEEP x, org"); + String query = useExpressionJoin + ? "ROW x = 40.0 | EVAL value_left = x | LOOKUP JOIN lookup-user2 ON value_left == value | KEEP x, org" + : "ROW x = 40.0 | EVAL value = x | LOOKUP JOIN lookup-user2 ON value | KEEP x, org"; + Response resp = runESQLCommand("dls_user", query); assertOK(resp); Map respMap = entityAsMap(resp); assertThat( @@ -698,7 +718,10 @@ public void testLookupJoinDocLevelSecurity() throws Exception { assertThat(respMap.get("values"), equalTo(List.of(Arrays.asList(40.0, null)))); - resp = runESQLCommand("dls_user", "ROW x = 32.0 | EVAL value = x | LOOKUP JOIN lookup-user2 ON value | KEEP x, org"); + query = useExpressionJoin + ? "ROW x = 32.0 | EVAL value_left = x | LOOKUP JOIN lookup-user2 ON value_left == value | KEEP x, org" + : "ROW x = 32.0 | EVAL value = x | LOOKUP JOIN lookup-user2 ON value | KEEP x, org"; + resp = runESQLCommand("dls_user", query); assertOK(resp); respMap = entityAsMap(resp); assertThat( @@ -709,7 +732,10 @@ public void testLookupJoinDocLevelSecurity() throws Exception { // same, but with a user that has two dls roles that allow him more visibility - resp = runESQLCommand("dls_user2", "ROW x = 40.0 | EVAL value = x | LOOKUP JOIN lookup-user2 ON value | KEEP x, org"); + query = useExpressionJoin + ? "ROW x = 40.0 | EVAL value_left = x | LOOKUP JOIN lookup-user2 ON value_left == value | KEEP x, org" + : "ROW x = 40.0 | EVAL value = x | LOOKUP JOIN lookup-user2 ON value | KEEP x, org"; + resp = runESQLCommand("dls_user2", query); assertOK(resp); respMap = entityAsMap(resp); assertThat( @@ -719,7 +745,10 @@ public void testLookupJoinDocLevelSecurity() throws Exception { assertThat(respMap.get("values"), equalTo(List.of(Arrays.asList(40.0, "sales")))); - resp = runESQLCommand("dls_user2", "ROW x = 32.0 | EVAL value = x | LOOKUP JOIN lookup-user2 ON value | KEEP x, org"); + query = useExpressionJoin + ? "ROW x = 32.0 | EVAL value_left = x | LOOKUP JOIN lookup-user2 ON value_left == value | KEEP x, org" + : "ROW x = 32.0 | EVAL value = x | LOOKUP JOIN lookup-user2 ON value | KEEP x, org"; + resp = runESQLCommand("dls_user2", query); assertOK(resp); respMap = entityAsMap(resp); assertThat( @@ -732,12 +761,24 @@ public void testLookupJoinDocLevelSecurity() throws Exception { @SuppressWarnings("unchecked") public void testLookupJoinFieldLevelSecurity() throws Exception { + testLookupJoinFieldLevelSecurityHelper(false); + assumeTrue( + "requires LOOKUP JOIN ON boolean expression capability", + EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled() + ); + testLookupJoinFieldLevelSecurityHelper(true); + } + + private void testLookupJoinFieldLevelSecurityHelper(boolean useExpressionJoin) throws Exception { assumeTrue( "Requires LOOKUP JOIN capability", hasCapabilities(adminClient(), List.of(EsqlCapabilities.Cap.JOIN_LOOKUP_V12.capabilityName())) ); - Response resp = runESQLCommand("fls_user2", "ROW x = 40.0 | EVAL value = x | LOOKUP JOIN lookup-user2 ON value"); + String query = useExpressionJoin + ? "ROW x = 40.0 | EVAL value_left = x | LOOKUP JOIN lookup-user2 ON value_left == value | KEEP x, value, org" + : "ROW x = 40.0 | EVAL value = x | LOOKUP JOIN lookup-user2 ON value | KEEP x, value, org"; + Response resp = runESQLCommand("fls_user2", query); assertOK(resp); Map respMap = entityAsMap(resp); assertThat( @@ -751,7 +792,10 @@ public void testLookupJoinFieldLevelSecurity() throws Exception { ) ); - resp = runESQLCommand("fls_user3", "ROW x = 40.0 | EVAL value = x | LOOKUP JOIN lookup-user2 ON value"); + String query2 = useExpressionJoin + ? "ROW x = 40.0 | EVAL value_left = x | LOOKUP JOIN lookup-user2 ON value_left == value | KEEP x, value, org, other" + : "ROW x = 40.0 | EVAL value = x | LOOKUP JOIN lookup-user2 ON value | KEEP x, value, org, other"; + resp = runESQLCommand("fls_user3", query2); assertOK(resp); respMap = entityAsMap(resp); assertThat( @@ -767,7 +811,7 @@ public void testLookupJoinFieldLevelSecurity() throws Exception { ); - resp = runESQLCommand("fls_user4", "ROW x = 40.0 | EVAL value = x | LOOKUP JOIN lookup-user2 ON value"); + resp = runESQLCommand("fls_user4", query); assertOK(resp); respMap = entityAsMap(resp); assertThat( @@ -781,21 +825,39 @@ public void testLookupJoinFieldLevelSecurity() throws Exception { ) ); - ResponseException error = expectThrows( - ResponseException.class, - () -> runESQLCommand("fls_user4_1", "ROW x = 40.0 | EVAL value = x | LOOKUP JOIN lookup-user2 ON value") - ); - assertThat(error.getMessage(), containsString("Unknown column [value] in right side of join")); + ResponseException error = expectThrows(ResponseException.class, () -> runESQLCommand("fls_user4_1", query)); assertThat(error.getResponse().getStatusLine().getStatusCode(), equalTo(HttpStatus.SC_BAD_REQUEST)); + if (useExpressionJoin) { + assertThat( + error.getMessage(), + containsString( + "Join condition must be between one attribute on the left side and one attribute on the right side of the join" + ) + ); + } else { + assertThat(error.getMessage(), containsString("Unknown column [value] in right side of join")); + } } public void testLookupJoinFieldLevelSecurityOnAlias() throws Exception { + testLookupJoinFieldLevelSecurityOnAliasHelper(false); + assumeTrue( + "requires LOOKUP JOIN ON boolean expression capability", + EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled() + ); + testLookupJoinFieldLevelSecurityOnAliasHelper(true); + } + + private void testLookupJoinFieldLevelSecurityOnAliasHelper(boolean useExpressionJoin) throws Exception { assumeTrue( "Requires LOOKUP JOIN capability", hasCapabilities(adminClient(), List.of(EsqlCapabilities.Cap.JOIN_LOOKUP_V12.capabilityName())) ); - Response resp = runESQLCommand("fls_user2_alias", "ROW x = 40.0 | EVAL value = x | LOOKUP JOIN lookup-second-alias ON value"); + String query = useExpressionJoin + ? "ROW x = 40.0 | EVAL value_left = x | LOOKUP JOIN lookup-second-alias ON value_left == value | KEEP x, value, org" + : "ROW x = 40.0 | EVAL value = x | LOOKUP JOIN lookup-second-alias ON value | KEEP x, value, org"; + Response resp = runESQLCommand("fls_user2_alias", query); assertOK(resp); Map respMap = entityAsMap(resp); assertThat( @@ -809,7 +871,10 @@ public void testLookupJoinFieldLevelSecurityOnAlias() throws Exception { ) ); - resp = runESQLCommand("fls_user3_alias", "ROW x = 40.0 | EVAL value = x | LOOKUP JOIN lookup-second-alias ON value"); + String query2 = useExpressionJoin + ? "ROW x = 40.0 | EVAL value_left = x | LOOKUP JOIN lookup-second-alias ON value_left == value | KEEP x, value, org, other" + : "ROW x = 40.0 | EVAL value = x | LOOKUP JOIN lookup-second-alias ON value | KEEP x, value, org, other"; + resp = runESQLCommand("fls_user3_alias", query2); assertOK(resp); respMap = entityAsMap(resp); assertThat( @@ -825,7 +890,7 @@ public void testLookupJoinFieldLevelSecurityOnAlias() throws Exception { ); - resp = runESQLCommand("fls_user4_alias", "ROW x = 40.0 | EVAL value = x | LOOKUP JOIN lookup-second-alias ON value"); + resp = runESQLCommand("fls_user4_alias", query); assertOK(resp); respMap = entityAsMap(resp); assertThat( @@ -839,48 +904,60 @@ public void testLookupJoinFieldLevelSecurityOnAlias() throws Exception { ) ); - ResponseException error = expectThrows( - ResponseException.class, - () -> runESQLCommand("fls_user4_1_alias", "ROW x = 40.0 | EVAL value = x | LOOKUP JOIN lookup-second-alias ON value") - ); - assertThat(error.getMessage(), containsString("Unknown column [value] in right side of join")); + ResponseException error = expectThrows(ResponseException.class, () -> runESQLCommand("fls_user4_1_alias", query)); assertThat(error.getResponse().getStatusLine().getStatusCode(), equalTo(HttpStatus.SC_BAD_REQUEST)); + if (useExpressionJoin) { + assertThat( + error.getMessage(), + containsString( + "Join condition must be between one attribute on the left side and one attribute on the right side of the join" + ) + ); + } else { + assertThat(error.getMessage(), containsString("Unknown column [value] in right side of join")); + } } public void testLookupJoinIndexForbidden() throws Exception { + testLookupJoinIndexForbiddenHelper(false); + assumeTrue( + "requires LOOKUP JOIN ON boolean expression capability", + EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled() + ); + testLookupJoinIndexForbiddenHelper(true); + } + + private void testLookupJoinIndexForbiddenHelper(boolean useExpressionJoin) throws Exception { assumeTrue( "Requires LOOKUP JOIN capability", hasCapabilities(adminClient(), List.of(EsqlCapabilities.Cap.JOIN_LOOKUP_V12.capabilityName())) ); - var resp = expectThrows( - ResponseException.class, - () -> runESQLCommand("metadata1_read2", "FROM lookup-user2 | EVAL value = 10.0 | LOOKUP JOIN lookup-user1 ON value | KEEP x") - ); + String query1 = useExpressionJoin + ? "FROM lookup-user2 | EVAL value_left = 10.0 | LOOKUP JOIN lookup-user1 ON value_left == value | KEEP x" + : "FROM lookup-user2 | EVAL value = 10.0 | LOOKUP JOIN lookup-user1 ON value | KEEP x"; + var resp = expectThrows(ResponseException.class, () -> runESQLCommand("metadata1_read2", query1)); assertThat(resp.getMessage(), containsString("Unknown index [lookup-user1]")); assertThat(resp.getResponse().getStatusLine().getStatusCode(), equalTo(HttpStatus.SC_BAD_REQUEST)); - resp = expectThrows( - ResponseException.class, - () -> runESQLCommand( - "metadata1_read2", - "FROM lookup-user2 | EVAL value = 10.0 | LOOKUP JOIN lookup-first-alias ON value | KEEP x" - ) - ); + String query2 = useExpressionJoin + ? "FROM lookup-user2 | EVAL value_left = 10.0 | LOOKUP JOIN lookup-first-alias ON value_left == value | KEEP x" + : "FROM lookup-user2 | EVAL value = 10.0 | LOOKUP JOIN lookup-first-alias ON value | KEEP x"; + resp = expectThrows(ResponseException.class, () -> runESQLCommand("metadata1_read2", query2)); assertThat(resp.getMessage(), containsString("Unknown index [lookup-first-alias]")); assertThat(resp.getResponse().getStatusLine().getStatusCode(), equalTo(HttpStatus.SC_BAD_REQUEST)); - resp = expectThrows( - ResponseException.class, - () -> runESQLCommand("metadata1_read2", "ROW x = 10.0 | EVAL value = x | LOOKUP JOIN lookup-user1 ON value | KEEP x") - ); + String query3 = useExpressionJoin + ? "ROW x = 10.0 | EVAL value_left = x | LOOKUP JOIN lookup-user1 ON value_left == value | KEEP x" + : "ROW x = 10.0 | EVAL value = x | LOOKUP JOIN lookup-user1 ON value | KEEP x"; + resp = expectThrows(ResponseException.class, () -> runESQLCommand("metadata1_read2", query3)); assertThat(resp.getMessage(), containsString("Unknown index [lookup-user1]")); assertThat(resp.getResponse().getStatusLine().getStatusCode(), equalTo(HttpStatus.SC_BAD_REQUEST)); - resp = expectThrows( - ResponseException.class, - () -> runESQLCommand("alias_user1", "ROW x = 10.0 | EVAL value = x | LOOKUP JOIN lookup-user1 ON value | KEEP x") - ); + String query4 = useExpressionJoin + ? "ROW x = 10.0 | EVAL value_left = x | LOOKUP JOIN lookup-user1 ON value_left == value | KEEP x" + : "ROW x = 10.0 | EVAL value = x | LOOKUP JOIN lookup-user1 ON value | KEEP x"; + resp = expectThrows(ResponseException.class, () -> runESQLCommand("alias_user1", query4)); assertThat(resp.getMessage(), containsString("Unknown index [lookup-user1]")); assertThat(resp.getResponse().getStatusLine().getStatusCode(), equalTo(HttpStatus.SC_BAD_REQUEST)); } diff --git a/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java b/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java index 50ea2509d5ceb..e37fdcf69549c 100644 --- a/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java +++ b/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java @@ -119,6 +119,7 @@ public MultiClusterSpecIT( "LookupJoinOnTwoFieldsMultipleTimes", // Lookup join after LIMIT is not supported in CCS yet "LookupJoinAfterLimitAndRemoteEnrich", + "LookupJoinExpressionAfterLimitAndRemoteEnrich", // Lookup join after FORK is not support in CCS yet "ForkBeforeLookupJoin" ); diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join-expression.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join-expression.csv-spec new file mode 100644 index 0000000000000..33146a8a465b8 --- /dev/null +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join-expression.csv-spec @@ -0,0 +1,737 @@ +// +// CSV spec for LOOKUP JOIN command with expression join +// + +lookupWithExpressionEquals +required_capability: join_lookup_v12 +required_capability: lookup_join_on_boolean_expression + +FROM employees +| WHERE emp_no == 10001 +| LOOKUP JOIN languages_lookup ON languages == language_code +| KEEP emp_no, languages, language_code, language_name +| SORT emp_no, languages, language_code, language_name +; + +emp_no:integer | languages:integer | language_code:integer | language_name:keyword +10001 | 2 | 2 | French +; + +lookupWithExpressionNotEquals +required_capability: join_lookup_v12 +required_capability: lookup_join_on_boolean_expression + +FROM employees +| WHERE emp_no == 10001 +| LOOKUP JOIN languages_lookup ON languages != language_code +| KEEP emp_no, languages, language_code, language_name +| SORT emp_no, languages, language_code, language_name +; + +emp_no:integer | languages:integer | language_code:integer | language_name:keyword +10001 | 2 | 1 | English +10001 | 2 | 3 | Spanish +10001 | 2 | 4 | German +; + +lookupWithExpressionGreater +required_capability: join_lookup_v12 +required_capability: lookup_join_on_boolean_expression + +FROM employees +| WHERE emp_no == 10001 +| LOOKUP JOIN languages_lookup ON languages > language_code +| KEEP emp_no, languages, language_code, language_name +| SORT emp_no, languages, language_code, language_name +; + +emp_no:integer | languages:integer | language_code:integer | language_name:keyword +10001 | 2 | 1 | English +; + +lookupWithExpressionGreaterOrEquals +required_capability: join_lookup_v12 +required_capability: lookup_join_on_boolean_expression + +FROM employees +| WHERE emp_no == 10001 +| LOOKUP JOIN languages_lookup ON languages >= language_code +| KEEP emp_no, languages, language_code, language_name +| SORT emp_no, languages, language_code, language_name +; + +emp_no:integer | languages:integer | language_code:integer | language_name:keyword +10001 | 2 | 1 | English +10001 | 2 | 2 | French +; + +lookupWithExpressionLess +required_capability: join_lookup_v12 +required_capability: lookup_join_on_boolean_expression + +FROM employees +| WHERE emp_no == 10001 +| LOOKUP JOIN languages_lookup ON languages < language_code +| KEEP emp_no, languages, language_code, language_name +| SORT emp_no, languages, language_code, language_name +; + +emp_no:integer | languages:integer | language_code:integer | language_name:keyword +10001 | 2 | 3 | Spanish +10001 | 2 | 4 | German +; + +lookupWithExpressionLessOrEquals +required_capability: join_lookup_v12 +required_capability: lookup_join_on_boolean_expression + +FROM employees +| WHERE emp_no == 10001 +| LOOKUP JOIN languages_lookup ON languages <= language_code +| KEEP emp_no, languages, language_code, language_name +| SORT emp_no, languages, language_code, language_name +; + +emp_no:integer | languages:integer | language_code:integer | language_name:keyword +10001 | 2 | 2 | French +10001 | 2 | 3 | Spanish +10001 | 2 | 4 | German +; + +lookupJoinOnTwoFieldsWithEval +required_capability: join_lookup_v12 +required_capability: lookup_join_on_boolean_expression + +FROM multi_column_joinable +| eval id_int = id_int + 5 +| RENAME id_int AS id_left, is_active_bool AS is_active_left +| LOOKUP JOIN multi_column_joinable_lookup ON id_left == id_int AND is_active_left == is_active_bool +| KEEP id_left, name_str, extra1, other1, other2 +| SORT id_left, name_str, extra1, other1, other2 +; + +warning:Line 2:17: evaluation of [id_int + 5] failed, treating result as null. Only first 20 failures recorded. +warning:Line 2:17: java.lang.IllegalArgumentException: single-value function encountered multi-value +warning:Line 4:3: evaluation of [LOOKUP JOIN multi_column_joinable_lookup ON id_left == id_int AND is_active_left == is_active_bool] failed, treating result as null. Only first 20 failures recorded. +warning:Line 4:3: java.lang.IllegalArgumentException: LOOKUP JOIN encountered multi-value + +id_left:integer | name_str:keyword | extra1:keyword | other1:keyword | other2:integer +6 | null | foo | iota | 9000 +7 | Grace | bar | kappa | 10000 +8 | Hank | baz | lambda | 11000 +9 | null | qux | null | null +10 | null | quux | null | null +11 | null | corge | null | null +12 | null | grault | null | null +13 | null | garply | null | null +14 | null | waldo | null | null +15 | null | fred | null | null +17 | null | xyzzy | null | null +18 | null | thud | null | null +19 | null | foo2 | null | null +20 | null | bar2 | null | null +null | null | plugh | null | null +null | null | xyz | null | null +null | null | zyx | null | null +; + +lookupWithTwoExpressionsAndNoMatch +required_capability: join_lookup_v12 +required_capability: lookup_join_on_boolean_expression + +FROM employees +| WHERE emp_no == 10001 +| EVAL expected_lang = "French" +| LOOKUP JOIN languages_lookup ON languages > language_code AND expected_lang == language_name +| KEEP emp_no, languages, language_code, language_name +| SORT emp_no, languages, language_code, language_name +; + +emp_no:integer | languages:integer | language_code:integer | language_name:keyword +10001 | 2 | null | null +; + +lookupWithTwoExpressionsSelfJoin +required_capability: join_lookup_v12 +required_capability: lookup_join_on_boolean_expression + +FROM languages_lookup +| RENAME language_code AS left_code, language_name AS left_name +| LOOKUP JOIN languages_lookup ON left_code > language_code AND left_name != language_name +| WHERE left_code == 2 +| KEEP left_code, left_name, language_code, language_name +| SORT left_code, left_name, language_code, language_name +; + +left_code:integer | left_name:keyword | language_code:integer | language_name:keyword +2 | French | 1 | English +; + +lookupMultiSelfJoinColTwoExprAnd +required_capability: join_lookup_v12 +required_capability: lookup_join_on_boolean_expression + +FROM multi_column_joinable_lookup +| RENAME id_int AS left_id, name_str AS left_name +| LOOKUP JOIN multi_column_joinable_lookup ON left_id == id_int AND left_name == name_str +| KEEP left_id, left_name, id_int, name_str +| SORT left_id, left_name, id_int, name_str +; + +warning:Line 3:3: evaluation of [LOOKUP JOIN multi_column_joinable_lookup ON left_id == id_int AND left_name == name_str] failed, treating result as null. Only first 20 failures recorded. +warning:Line 3:3: java.lang.IllegalArgumentException: LOOKUP JOIN encountered multi-value + +left_id:integer | left_name:keyword | id_int:integer | name_str:keyword +1 | Alice | 1 | Alice +1 | Alice | 1 | Alice +1 | Alice | 1 | Alice +1 | Alice | 1 | Alice +[1, 19, 20] | Sophia | null | null +2 | Bob | 2 | Bob +3 | Charlie | 3 | Charlie +3 | Charlie | 3 | Charlie +3 | Charlie | 3 | Charlie +3 | Charlie | 3 | Charlie +4 | David | 4 | David +5 | Eve | 5 | Eve +5 | Eve | 5 | Eve +5 | Eve | 5 | Eve +5 | Eve | 5 | Eve +6 | null | null | null +7 | Grace | 7 | Grace +8 | Hank | 8 | Hank +12 | Liam | 12 | Liam +13 | Mia | 13 | Mia +14 | Nina | 14 | Nina +16 | Paul | 16 | Paul +[17, 18] | Olivia | null | null +null | Kate | null | null +; + +lookupJoinOnThreeFields +required_capability: join_lookup_v12 +required_capability: lookup_join_on_boolean_expression + +FROM multi_column_joinable +| RENAME id_int as id_left, name_str as name_left, is_active_bool as is_active_left +| LOOKUP JOIN multi_column_joinable_lookup ON id_left == id_int AND name_left == name_str AND is_active_left == is_active_bool +| KEEP id_left, name_left, extra1, other1, other2 +| SORT id_left, name_left, extra1, other1, other2 +; + +warning:Line 3:3: evaluation of [LOOKUP JOIN multi_column_joinable_lookup ON id_left == id_int AND name_left == name_str AND is_active_left == is_active_bool] failed, treating result as null. Only first 20 failures recorded. +warning:Line 3:3: java.lang.IllegalArgumentException: LOOKUP JOIN encountered multi-value + +id_left:integer| name_left:keyword| extra1:keyword | other1:keyword | other2:integer +1 | Alice | foo | alpha | 1000 +1 | Alice | foo | beta | 2000 +[1, 19, 21] | Sophia | zyx | null | null +2 | Bob | bar | gamma | 3000 +3 | Charlie | baz | delta | 4000 +4 | David | qux | zeta | 6000 +5 | Eve | quux | eta | 7000 +5 | Eve | quux | theta | 8000 +6 | null | corge | null | null +7 | Grace | grault | kappa | 10000 +8 | Hank | garply | lambda | 11000 +9 | Ivy | waldo | null | null +10 | John | fred | null | null +12 | Liam | xyzzy | nu | 13000 +13 | Mia | thud | xi | 14000 +14 | Nina | foo2 | omicron | 15000 +15 | Oscar | bar2 | null | null +[17, 18] | Olivia | xyz | null | null +null | Kate | plugh | null | null +; + +lookupJoinOnFourFields +required_capability: join_lookup_v12 +required_capability: lookup_join_on_boolean_expression + +FROM multi_column_joinable +| RENAME id_int AS id_left, name_str AS name_left, is_active_bool AS is_active_left, ip_addr AS ip_addr_left +| LOOKUP JOIN multi_column_joinable_lookup ON id_left == id_int AND name_left == name_str AND is_active_left == is_active_bool AND ip_addr_left == ip_addr +| KEEP id_left, name_left, extra1, other1, other2 +| SORT id_left, name_left, extra1, other1, other2 +; + +warning:Line 3:3: evaluation of [LOOKUP JOIN multi_column_joinable_lookup ON id_left == id_int AND name_left == name_str AND is_active_left == is_active_bool AND ip_addr_left == ip_addr] failed, treating result as null. Only first 20 failures recorded. +warning:Line 3:3: java.lang.IllegalArgumentException: LOOKUP JOIN encountered multi-value + +id_left:integer| name_left:keyword | extra1:keyword | other1:keyword | other2:integer +1 | Alice | foo | alpha | 1000 +[1, 19, 21] | Sophia | zyx | null | null +2 | Bob | bar | null | null +3 | Charlie | baz | delta | 4000 +4 | David | qux | zeta | 6000 +5 | Eve | quux | eta | 7000 +5 | Eve | quux | theta | 8000 +6 | null | corge | null | null +7 | Grace | grault | null | null +8 | Hank | garply | lambda | 11000 +9 | Ivy | waldo | null | null +10 | John | fred | null | null +12 | Liam | xyzzy | nu | 13000 +13 | Mia | thud | xi | 14000 +14 | Nina | foo2 | omicron | 15000 +15 | Oscar | bar2 | null | null +[17, 18] | Olivia | xyz | null | null +null | Kate | plugh | null | null +; + + +lookupMultiColTwoExprAndNoMatch +required_capability: join_lookup_v12 +required_capability: lookup_join_on_boolean_expression + +FROM multi_column_joinable_lookup +| RENAME id_int AS left_id, name_str AS left_name +| EVAL left_name = "nomatch" +| LOOKUP JOIN multi_column_joinable_lookup ON left_id == id_int AND left_name == name_str +| KEEP left_id, left_name, id_int, name_str +| SORT left_id, left_name, id_int, name_str +; + +warning:Line 4:3: evaluation of [LOOKUP JOIN multi_column_joinable_lookup ON left_id == id_int AND left_name == name_str] failed, treating result as null. Only first 20 failures recorded. +warning:Line 4:3: java.lang.IllegalArgumentException: LOOKUP JOIN encountered multi-value + +left_id:integer | left_name:keyword | id_int:integer | name_str:keyword +[1, 19, 20] | nomatch | null | null +1 | nomatch | null | null +1 | nomatch | null | null +2 | nomatch | null | null +3 | nomatch | null | null +3 | nomatch | null | null +4 | nomatch | null | null +5 | nomatch | null | null +5 | nomatch | null | null +6 | nomatch | null | null +7 | nomatch | null | null +8 | nomatch | null | null +12 | nomatch | null | null +13 | nomatch | null | null +14 | nomatch | null | null +16 | nomatch | null | null +[17, 18] | nomatch | null | null +null | nomatch | null | null +; + +lookupMultiColMixedEqNeq +required_capability: join_lookup_v12 +required_capability: lookup_join_on_boolean_expression + +FROM multi_column_joinable +| RENAME name_str AS left_name, is_active_bool AS is_active_left, id_int AS left_id +| LOOKUP JOIN multi_column_joinable_lookup ON is_active_left == is_active_bool AND left_id != id_int +| KEEP left_id, left_name, id_int, name_str +| SORT left_id, left_name, id_int, name_str +| LIMIT 20 +; + +warning:Line 3:3: evaluation of [LOOKUP JOIN multi_column_joinable_lookup ON is_active_left == is_active_bool AND left_id != id_int] failed, treating result as null. Only first 20 failures recorded. +warning:Line 3:3: java.lang.IllegalArgumentException: LOOKUP JOIN encountered multi-value + +left_id:integer | left_name:keyword | id_int:integer | name_str:keyword +1 | Alice | 3 | Charlie +1 | Alice | 5 | Eve +1 | Alice | 5 | Eve +1 | Alice | 6 | null +1 | Alice | 8 | Hank +1 | Alice | 12 | Liam +1 | Alice | 14 | Nina +1 | Alice | 16 | Paul +[1, 19, 21] | Sophia | null | null +2 | Bob | 3 | Charlie +2 | Bob | 4 | David +2 | Bob | 7 | Grace +2 | Bob | 13 | Mia +3 | Charlie | 1 | Alice +3 | Charlie | 1 | Alice +3 | Charlie | 5 | Eve +3 | Charlie | 5 | Eve +3 | Charlie | 6 | null +3 | Charlie | 8 | Hank +3 | Charlie | 12 | Liam +; + +lookupMultiColMixedGtEq +required_capability: join_lookup_v12 +required_capability: lookup_join_on_boolean_expression + +FROM multi_column_joinable +| RENAME id_int AS left_id, name_str AS left_name, is_active_bool AS is_active_left +| LOOKUP JOIN multi_column_joinable_lookup ON left_id > id_int AND is_active_left == is_active_bool +| KEEP left_id, left_name, id_int, name_str, is_active_left, is_active_bool +| SORT left_id, left_name, id_int, name_str, is_active_left, is_active_left +| LIMIT 20 +; + +warning:Line 3:3: evaluation of [LOOKUP JOIN multi_column_joinable_lookup ON left_id > id_int AND is_active_left == is_active_bool] failed, treating result as null. Only first 20 failures recorded. +warning:Line 3:3: java.lang.IllegalArgumentException: LOOKUP JOIN encountered multi-value + + +left_id:integer | left_name:keyword | id_int:integer | name_str:keyword | is_active_left:boolean | is_active_bool:boolean +1 | Alice | null | null | true | null +[1, 19, 21] | Sophia | null | null | true | null +2 | Bob | null | null | false | null +3 | Charlie | 1 | Alice | true | true +3 | Charlie | 1 | Alice | true | true +4 | David | 2 | Bob | false | false +4 | David | 3 | Charlie | false | false +5 | Eve | 1 | Alice | true | true +5 | Eve | 1 | Alice | true | true +5 | Eve | 3 | Charlie | true | true +6 | null | 1 | Alice | true | true +6 | null | 1 | Alice | true | true +6 | null | 3 | Charlie | true | true +6 | null | 5 | Eve | true | true +6 | null | 5 | Eve | true | true +7 | Grace | 2 | Bob | false | false +7 | Grace | 3 | Charlie | false | false +7 | Grace | 4 | David | false | false +8 | Hank | 1 | Alice | true | true +8 | Hank | 1 | Alice | true | true +; + +lookupMultiColMixedGtEqSwapLeftRight +required_capability: join_lookup_v12 +required_capability: lookup_join_on_boolean_expression + +FROM multi_column_joinable +| RENAME id_int AS left_id, name_str AS left_name, is_active_bool AS is_active_left +| LOOKUP JOIN multi_column_joinable_lookup ON id_int < left_id AND is_active_left == is_active_bool +| KEEP left_id, left_name, id_int, name_str, is_active_left, is_active_bool +| SORT left_id, left_name, id_int, name_str, is_active_left, is_active_left +| LIMIT 20 +; + +warning:Line 3:3: evaluation of [LOOKUP JOIN multi_column_joinable_lookup ON id_int < left_id AND is_active_left == is_active_bool] failed, treating result as null. Only first 20 failures recorded. +warning:Line 3:3: java.lang.IllegalArgumentException: LOOKUP JOIN encountered multi-value + + +left_id:integer | left_name:keyword | id_int:integer | name_str:keyword | is_active_left:boolean | is_active_bool:boolean +1 | Alice | null | null | true | null +[1, 19, 21] | Sophia | null | null | true | null +2 | Bob | null | null | false | null +3 | Charlie | 1 | Alice | true | true +3 | Charlie | 1 | Alice | true | true +4 | David | 2 | Bob | false | false +4 | David | 3 | Charlie | false | false +5 | Eve | 1 | Alice | true | true +5 | Eve | 1 | Alice | true | true +5 | Eve | 3 | Charlie | true | true +6 | null | 1 | Alice | true | true +6 | null | 1 | Alice | true | true +6 | null | 3 | Charlie | true | true +6 | null | 5 | Eve | true | true +6 | null | 5 | Eve | true | true +7 | Grace | 2 | Bob | false | false +7 | Grace | 3 | Charlie | false | false +7 | Grace | 4 | David | false | false +8 | Hank | 1 | Alice | true | true +8 | Hank | 1 | Alice | true | true +; + +lookupMultiColMixedLtNeqEq +required_capability: join_lookup_v12 +required_capability: lookup_join_on_boolean_expression + +FROM multi_column_joinable +| RENAME id_int AS left_id, name_str AS left_name, is_active_bool AS left_is_active +| LOOKUP JOIN multi_column_joinable_lookup ON left_id < id_int AND left_name != name_str AND left_is_active == is_active_bool +| KEEP left_id, left_name, left_is_active, id_int, name_str, is_active_bool +| SORT left_id, left_name, left_is_active, id_int, name_str, is_active_bool +| LIMIT 20 +; + +warning:Line 3:3: evaluation of [LOOKUP JOIN multi_column_joinable_lookup ON left_id < id_int AND left_name != name_str AND left_is_active == is_active_bool] failed, treating result as null. Only first 20 failures recorded. +warning:Line 3:3: java.lang.IllegalArgumentException: LOOKUP JOIN encountered multi-value + +left_id:integer | left_name:keyword | left_is_active:boolean | id_int:integer | name_str:keyword | is_active_bool:boolean +1 | Alice | true | 3 | Charlie | true +1 | Alice | true | 5 | Eve | true +1 | Alice | true | 5 | Eve | true +1 | Alice | true | 8 | Hank | true +1 | Alice | true | 12 | Liam | true +1 | Alice | true | 14 | Nina | true +1 | Alice | true | 16 | Paul | true +[1, 19, 21] | Sophia | true | null | null | null +2 | Bob | false | 3 | Charlie | false +2 | Bob | false | 4 | David | false +2 | Bob | false | 7 | Grace | false +2 | Bob | false | 13 | Mia | false +3 | Charlie | true | 5 | Eve | true +3 | Charlie | true | 5 | Eve | true +3 | Charlie | true | 8 | Hank | true +3 | Charlie | true | 12 | Liam | true +3 | Charlie | true | 14 | Nina | true +3 | Charlie | true | 16 | Paul | true +4 | David | false | 7 | Grace | false +4 | David | false | 13 | Mia | false +; + +lookupMultiColMixedGteNeq +required_capability: join_lookup_v12 +required_capability: lookup_join_on_boolean_expression + +FROM multi_column_joinable +| RENAME id_int AS left_id, name_str AS left_name +| LOOKUP JOIN multi_column_joinable_lookup ON left_id >= id_int AND left_name != name_str +| KEEP left_id, left_name, id_int, name_str +| SORT left_id, left_name, id_int, name_str +| LIMIT 20 +; + +warning:Line 3:3: evaluation of [LOOKUP JOIN multi_column_joinable_lookup ON left_id >= id_int AND left_name != name_str] failed, treating result as null. Only first 20 failures recorded. +warning:Line 3:3: java.lang.IllegalArgumentException: LOOKUP JOIN encountered multi-value + +left_id:integer | left_name:keyword | id_int:integer | name_str:keyword +1 | Alice | null | null +[1, 19, 21] | Sophia | null | null +2 | Bob | 1 | Alice +2 | Bob | 1 | Alice +3 | Charlie | 1 | Alice +3 | Charlie | 1 | Alice +3 | Charlie | 2 | Bob +4 | David | 1 | Alice +4 | David | 1 | Alice +4 | David | 2 | Bob +4 | David | 3 | Charlie +4 | David | 3 | Charlie +5 | Eve | 1 | Alice +5 | Eve | 1 | Alice +5 | Eve | 2 | Bob +5 | Eve | 3 | Charlie +5 | Eve | 3 | Charlie +5 | Eve | 4 | David +6 | null | null | null +7 | Grace | 1 | Alice +; + +lookupMultiColMixedLteEq +required_capability: join_lookup_v12 +required_capability: lookup_join_on_boolean_expression + +FROM multi_column_joinable_lookup +| WHERE id_int == 3 +| RENAME id_int AS left_id, name_str AS left_name +| LOOKUP JOIN multi_column_joinable_lookup ON left_id <= id_int AND left_name == name_str +| KEEP left_id, left_name, id_int, name_str +| SORT left_id, left_name, id_int, name_str +; + + +left_id:integer | left_name:keyword | id_int:integer | name_str:keyword +3 | Charlie | 3 | Charlie +3 | Charlie | 3 | Charlie +3 | Charlie | 3 | Charlie +3 | Charlie | 3 | Charlie +; + +lookupMultiColFourMixed +required_capability: join_lookup_v12 +required_capability: lookup_join_on_boolean_expression + +FROM multi_column_joinable_lookup +| WHERE id_int == 2 +| RENAME id_int AS left_id, name_str AS left_name, is_active_bool AS left_is_active, ip_addr AS left_ip +| LOOKUP JOIN multi_column_joinable_lookup ON left_id < id_int AND left_name != name_str AND left_is_active != is_active_bool AND left_ip != ip_addr +| KEEP left_id, left_name, left_is_active, left_ip, id_int, name_str, is_active_bool, ip_addr +| SORT left_id, left_name, left_is_active, left_ip, id_int, name_str, is_active_bool, ip_addr +; + +warning:Line 4:3: evaluation of [LOOKUP JOIN multi_column_joinable_lookup ON left_id < id_int AND left_name != name_str AND left_is_active != is_active_bool AND left_ip != ip_addr] failed, treating result as null. Only first 20 failures recorded. +warning:Line 4:3: java.lang.IllegalArgumentException: LOOKUP JOIN encountered multi-value + +left_id:integer | left_name:keyword | left_is_active:boolean | left_ip:ip | id_int:integer | name_str:keyword | is_active_bool:boolean | ip_addr:ip +2 | Bob | false | 192.168.1.3 | 5 | Eve | true | 192.168.1.5 +2 | Bob | false | 192.168.1.3 | 5 | Eve | true | 192.168.1.5 +2 | Bob | false | 192.168.1.3 | 8 | Hank | true | 192.168.1.8 +2 | Bob | false | 192.168.1.3 | 12 | Liam | true | 192.168.1.12 +2 | Bob | false | 192.168.1.3 | 14 | Nina | true | 192.168.1.14 +2 | Bob | false | 192.168.1.3 | 16 | Paul | true | 192.168.1.16 +; + + +lookupJoinOnSameLeftFieldTwice +required_capability: join_lookup_v12 +required_capability: lookup_join_on_boolean_expression + +FROM multi_column_joinable +| RENAME id_int AS id_left, name_str AS name_left, is_active_bool AS is_active_left, ip_addr AS ip_addr_left +| LOOKUP JOIN multi_column_joinable_lookup ON id_left == id_int AND name_left == name_str AND id_left < other2 +| KEEP id_left, name_left, extra1, other1, other2 +| SORT id_left, name_left, extra1, other1, other2 +; + +warning:Line 3:3: evaluation of [LOOKUP JOIN multi_column_joinable_lookup ON id_left == id_int AND name_left == name_str AND id_left < other2] failed, treating result as null. Only first 20 failures recorded. +warning:Line 3:3: java.lang.IllegalArgumentException: LOOKUP JOIN encountered multi-value + +id_left:integer | name_left:keyword | extra1:keyword | other1:keyword | other2:integer +1 | Alice | foo | alpha | 1000 +1 | Alice | foo | beta | 2000 +[1, 19, 21] | Sophia | zyx | null | null +2 | Bob | bar | gamma | 3000 +3 | Charlie | baz | delta | 4000 +3 | Charlie | baz | epsilon | 5000 +4 | David | qux | zeta | 6000 +5 | Eve | quux | eta | 7000 +5 | Eve | quux | theta | 8000 +6 | null | corge | null | null +7 | Grace | grault | kappa | 10000 +8 | Hank | garply | lambda | 11000 +9 | Ivy | waldo | null | null +10 | John | fred | null | null +12 | Liam | xyzzy | nu | 13000 +13 | Mia | thud | xi | 14000 +14 | Nina | foo2 | omicron | 15000 +15 | Oscar | bar2 | null | null +[17, 18] | Olivia | xyz | null | null +null | Kate | plugh | null | null +; + +lookupJoinOnSameRightFieldTwice +required_capability: join_lookup_v12 +required_capability: lookup_join_on_boolean_expression + +FROM multi_column_joinable +| RENAME id_int AS id_left, name_str AS name_left, is_active_bool AS is_active_left, ip_addr AS ip_addr_left +| LOOKUP JOIN multi_column_joinable_lookup ON id_left == id_int AND name_left == name_str AND extra2 > id_int +| KEEP id_left, name_left, extra1, other1, other2 +| SORT id_left, name_left, extra1, other1, other2 +; + +warning:Line 3:3: evaluation of [LOOKUP JOIN multi_column_joinable_lookup ON id_left == id_int AND name_left == name_str AND extra2 > id_int] failed, treating result as null. Only first 20 failures recorded. +warning:Line 3:3: java.lang.IllegalArgumentException: LOOKUP JOIN encountered multi-value + + +id_left:integer | name_left:keyword | extra1:keyword | other1:keyword | other2:integer +1 | Alice | foo | alpha | 1000 +1 | Alice | foo | beta | 2000 +[1, 19, 21] | Sophia | zyx | null | null +2 | Bob | bar | gamma | 3000 +3 | Charlie | baz | delta | 4000 +3 | Charlie | baz | epsilon | 5000 +4 | David | qux | zeta | 6000 +5 | Eve | quux | eta | 7000 +5 | Eve | quux | theta | 8000 +6 | null | corge | null | null +7 | Grace | grault | kappa | 10000 +8 | Hank | garply | lambda | 11000 +9 | Ivy | waldo | null | null +10 | John | fred | null | null +12 | Liam | xyzzy | nu | 13000 +13 | Mia | thud | xi | 14000 +14 | Nina | foo2 | omicron | 15000 +15 | Oscar | bar2 | null | null +[17, 18] | Olivia | xyz | null | null +null | Kate | plugh | null | null +; + +lookupJoinOnSameRightFieldTwiceEval +required_capability: join_lookup_v12 +required_capability: lookup_join_on_boolean_expression + +FROM multi_column_joinable +| RENAME id_int AS id_left, name_str AS name_left, is_active_bool AS is_active_left, ip_addr AS ip_addr_left +| EVAL left_id2 = ((extra2 / 200)::integer)*2 +| LOOKUP JOIN multi_column_joinable_lookup ON id_left == id_int AND name_left == name_str AND left_id2 != id_int +| KEEP id_left, name_left, extra1, other1, other2, left_id2 +| SORT id_left, name_left, extra1, other1, other2, left_id2 +; + +warning:Line 4:3: evaluation of [LOOKUP JOIN multi_column_joinable_lookup ON id_left == id_int AND name_left == name_str AND left_id2 != id_int] failed, treating result as null. Only first 20 failures recorded. +warning:Line 4:3: java.lang.IllegalArgumentException: LOOKUP JOIN encountered multi-value + +id_left:integer | name_left:keyword | extra1:keyword | other1:keyword | other2:integer | left_id2:integer +1 | Alice | foo | alpha | 1000 | 0 +1 | Alice | foo | beta | 2000 | 0 +[1, 19, 21] | Sophia | zyx | null | null | 210 +2 | Bob | bar | null | null | 2 +3 | Charlie | baz | delta | 4000 | 2 +3 | Charlie | baz | epsilon | 5000 | 2 +4 | David | qux | null | null | 4 +5 | Eve | quux | eta | 7000 | 4 +5 | Eve | quux | theta | 8000 | 4 +6 | null | corge | null | null | 6 +7 | Grace | grault | kappa | 10000 | 6 +8 | Hank | garply | null | null | 8 +9 | Ivy | waldo | null | null | 8 +10 | John | fred | null | null | 10 +12 | Liam | xyzzy | null | null | 12 +13 | Mia | thud | xi | 14000 | 12 +14 | Nina | foo2 | null | null | 14 +15 | Oscar | bar2 | null | null | 14 +[17, 18] | Olivia | xyz | null | null | 170 +null | Kate | plugh | null | null | 10 +; + +twoLookupJoinsInSameQuery +required_capability: join_lookup_v12 +required_capability: lookup_join_on_boolean_expression + +FROM multi_column_joinable +| WHERE id_int == 1 +| RENAME id_int AS id_left, name_str AS name_left +| LOOKUP JOIN multi_column_joinable_lookup ON id_left == id_int AND name_left == name_str +| RENAME other1 AS other1_from_first_join, id_int AS id_from_first_join, name_str AS name_from_first_join +| LOOKUP JOIN multi_column_joinable_lookup ON id_left == id_int AND other1_from_first_join != other1 +| KEEP id_left, name_left, other1_from_first_join, other1 +| SORT id_left, name_left, other1_from_first_join, other1 +; + +warning:Line 2:9: evaluation of [id_int == 1] failed, treating result as null. Only first 20 failures recorded. +warning:Line 2:9: java.lang.IllegalArgumentException: single-value function encountered multi-value +warning:Line 6:3: evaluation of [LOOKUP JOIN multi_column_joinable_lookup ON id_left == id_int AND other1_from_first_join != other1] failed, treating result as null. Only first 20 failures recorded. +warning:Line 6:3: java.lang.IllegalArgumentException: LOOKUP JOIN encountered multi-value + +id_left:integer | name_left:keyword | other1_from_first_join:keyword | other1:keyword +1 | Alice | alpha | beta +1 | Alice | beta | alpha +; + +lookupOnExpressionOnTheCoordinator +required_capability: join_lookup_v12 +required_capability: lookup_join_on_boolean_expression + +FROM employees +| SORT emp_no +| LIMIT 3 +| EVAL language_code_left = languages +| LOOKUP JOIN languages_lookup ON language_code_left == language_code +| KEEP emp_no, language_code_left, language_name +; + +emp_no:integer | language_code_left:integer | language_name:keyword +10001 | 2 | French +10002 | 5 | null +10003 | 4 | German +; + +lookupJoinExpressionWithPushableFilterOnRight +required_capability: join_lookup_v12 +required_capability: lookup_join_on_boolean_expression + +FROM multi_column_joinable +| RENAME id_int AS id_left, is_active_bool AS is_active_left +| LOOKUP JOIN multi_column_joinable_lookup ON id_int == id_left and is_active_left == is_active_bool +| WHERE other2 > 5000 +| KEEP id_int, name_str, extra1, other1, other2 +| SORT id_int, name_str, extra1, other1, other2 +| LIMIT 20 +; + +warning:Line 3:3: evaluation of [LOOKUP JOIN multi_column_joinable_lookup ON id_int == id_left and is_active_left == is_active_bool] failed, treating result as null. Only first 20 failures recorded. +warning:Line 3:3: java.lang.IllegalArgumentException: LOOKUP JOIN encountered multi-value + +id_int:integer | name_str:keyword | extra1:keyword | other1:keyword | other2:integer +4 | David | qux | zeta | 6000 +5 | Eve | quux | eta | 7000 +5 | Eve | quux | theta | 8000 +6 | null | corge | iota | 9000 +7 | Grace | grault | kappa | 10000 +8 | Hank | garply | lambda | 11000 +12 | Liam | xyzzy | nu | 13000 +13 | Mia | thud | xi | 14000 +14 | Nina | foo2 | omicron | 15000 +; diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/LookupFromIndexIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/LookupFromIndexIT.java index 4056572d38b17..27dce0b9bcbae 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/LookupFromIndexIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/LookupFromIndexIT.java @@ -63,6 +63,8 @@ import org.elasticsearch.xpack.esql.core.type.EsField; import org.elasticsearch.xpack.esql.enrich.LookupFromIndexOperator; import org.elasticsearch.xpack.esql.enrich.MatchConfig; +import org.elasticsearch.xpack.esql.expression.predicate.Predicates; +import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.Equals; import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.GreaterThan; import org.elasticsearch.xpack.esql.plan.logical.EsRelation; import org.elasticsearch.xpack.esql.plan.logical.Filter; @@ -225,6 +227,7 @@ public void populate(int docCount, List expected, Predicate fil Map lookupDoc = new HashMap<>(); for (int f = 0; f < numFields; f++) { lookupDoc.put("key" + f, lookupData[f][i]); + lookupDoc.put("rkey" + f, lookupData[f][i]); } lookupDoc.put("l", i); docs.add(client().prepareIndex("lookup").setSource(lookupDoc)); @@ -263,14 +266,17 @@ private void runLookup(List keyTypes, PopulateIndices populateIndices, .put(IndexSettings.MODE.getKey(), "lookup") .put(IndexMetadata.INDEX_NUMBER_OF_SHARDS_SETTING.getKey(), 1); - String[] lookupMappers = new String[keyTypes.size() * 2 + 2]; - int lookupMappersCounter = 0; - for (; lookupMappersCounter < keyTypes.size(); lookupMappersCounter++) { - lookupMappers[2 * lookupMappersCounter] = "key" + lookupMappersCounter; - lookupMappers[2 * lookupMappersCounter + 1] = "type=" + keyTypes.get(lookupMappersCounter).esType(); + String[] lookupMappers = new String[keyTypes.size() * 4 + 2]; + for (int i = 0; i < keyTypes.size(); i++) { + lookupMappers[2 * i] = "key" + i; + lookupMappers[2 * i + 1] = "type=" + keyTypes.get(i).esType(); + } + for (int i = 0; i < keyTypes.size(); i++) { + lookupMappers[2 * (keyTypes.size() + i)] = "rkey" + i; + lookupMappers[2 * (keyTypes.size() + i) + 1] = "type=" + keyTypes.get(i).esType(); } - lookupMappers[2 * lookupMappersCounter] = "l"; - lookupMappers[2 * lookupMappersCounter + 1] = "type=long"; + lookupMappers[keyTypes.size() * 4] = "l"; + lookupMappers[keyTypes.size() * 4 + 1] = "type=long"; client().admin().indices().prepareCreate("lookup").setSettings(lookupSettings).setMapping(lookupMappers).get(); client().admin().cluster().prepareHealth(TEST_REQUEST_TIMEOUT).setWaitForGreenStatus().get(); @@ -371,9 +377,27 @@ private void runLookup(List keyTypes, PopulateIndices populateIndices, TEST_REQUEST_TIMEOUT ); final String finalNodeWithShard = nodeWithShard; + boolean expressionJoin = EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled() ? randomBoolean() : false; List matchFields = new ArrayList<>(); + List joinOnConditions = new ArrayList<>(); + if (expressionJoin) { + for (int i = 0; i < keyTypes.size(); i++) { + FieldAttribute leftAttr = new FieldAttribute( + Source.EMPTY, + "key" + i, + new EsField("key" + i, keyTypes.get(0), Collections.emptyMap(), true, EsField.TimeSeriesFieldType.NONE) + ); + FieldAttribute rightAttr = new FieldAttribute( + Source.EMPTY, + "rkey" + i, + new EsField("rkey" + i, keyTypes.get(i), Collections.emptyMap(), true, EsField.TimeSeriesFieldType.NONE) + ); + joinOnConditions.add(new Equals(Source.EMPTY, leftAttr, rightAttr)); + } + } + // the matchFields are shared for both types of join for (int i = 0; i < keyTypes.size(); i++) { - matchFields.add(new MatchConfig(new FieldAttribute.FieldName("key" + i), i + 1, keyTypes.get(i))); + matchFields.add(new MatchConfig("key" + i, i + 1, keyTypes.get(i))); } LookupFromIndexOperator.Factory lookup = new LookupFromIndexOperator.Factory( matchFields, @@ -385,7 +409,8 @@ private void runLookup(List keyTypes, PopulateIndices populateIndices, "lookup", List.of(new Alias(Source.EMPTY, "l", new ReferenceAttribute(Source.EMPTY, "l", DataType.LONG))), Source.EMPTY, - filters + filters, + Predicates.combineAnd(joinOnConditions) ); DriverContext driverContext = driverContext(); try ( diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/LookupJoinTypesIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/LookupJoinTypesIT.java index c305938c09b0b..4c6951ce8e017 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/LookupJoinTypesIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/LookupJoinTypesIT.java @@ -7,10 +7,14 @@ package org.elasticsearch.xpack.esql.action; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; + +import org.apache.lucene.queryparser.ext.Extensions.Pair; import org.apache.lucene.tests.util.LuceneTestCase; import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder; import org.elasticsearch.action.index.IndexRequestBuilder; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.Nullable; import org.elasticsearch.index.mapper.extras.MapperExtrasPlugin; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESIntegTestCase; @@ -21,6 +25,7 @@ import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.expression.function.DocsV3Support; import org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry; +import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.EsqlBinaryComparison.BinaryComparisonOperation; import org.elasticsearch.xpack.esql.plan.logical.join.Join; import org.elasticsearch.xpack.esql.plugin.EsqlPlugin; import org.elasticsearch.xpack.spatial.SpatialPlugin; @@ -47,14 +52,19 @@ import static org.elasticsearch.xpack.esql.core.type.DataType.AGGREGATE_METRIC_DOUBLE; import static org.elasticsearch.xpack.esql.core.type.DataType.BOOLEAN; import static org.elasticsearch.xpack.esql.core.type.DataType.BYTE; +import static org.elasticsearch.xpack.esql.core.type.DataType.CARTESIAN_POINT; +import static org.elasticsearch.xpack.esql.core.type.DataType.CARTESIAN_SHAPE; import static org.elasticsearch.xpack.esql.core.type.DataType.DATETIME; import static org.elasticsearch.xpack.esql.core.type.DataType.DATE_NANOS; +import static org.elasticsearch.xpack.esql.core.type.DataType.DENSE_VECTOR; import static org.elasticsearch.xpack.esql.core.type.DataType.DOC_DATA_TYPE; import static org.elasticsearch.xpack.esql.core.type.DataType.DOUBLE; import static org.elasticsearch.xpack.esql.core.type.DataType.FLOAT; import static org.elasticsearch.xpack.esql.core.type.DataType.GEOHASH; import static org.elasticsearch.xpack.esql.core.type.DataType.GEOHEX; import static org.elasticsearch.xpack.esql.core.type.DataType.GEOTILE; +import static org.elasticsearch.xpack.esql.core.type.DataType.GEO_POINT; +import static org.elasticsearch.xpack.esql.core.type.DataType.GEO_SHAPE; import static org.elasticsearch.xpack.esql.core.type.DataType.HALF_FLOAT; import static org.elasticsearch.xpack.esql.core.type.DataType.INTEGER; import static org.elasticsearch.xpack.esql.core.type.DataType.IP; @@ -66,6 +76,7 @@ import static org.elasticsearch.xpack.esql.core.type.DataType.TEXT; import static org.elasticsearch.xpack.esql.core.type.DataType.TSID_DATA_TYPE; import static org.elasticsearch.xpack.esql.core.type.DataType.UNDER_CONSTRUCTION; +import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -106,157 +117,232 @@ protected Collection> nodePlugins() { ); } - private static final Map testConfigurations = new HashMap<>(); + @ParametersFactory + public static Iterable parametersFactory() { + List operations = new ArrayList<>(); + operations.add(new Object[] { null }); + if (EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled()) { + for (BinaryComparisonOperation operation : BinaryComparisonOperation.values()) { + operations.add(new Object[] { operation }); + } + } + return operations; + } + + private final BinaryComparisonOperation operationParameterized; + + public LookupJoinTypesIT(BinaryComparisonOperation operation) { + this.operationParameterized = operation; + } + + private static final Map, TestConfigs> testConfigurations = new HashMap<>(); static { - // Initialize the test configurations for string tests - { - TestConfigs configs = testConfigurations.computeIfAbsent("strings", TestConfigs::new); - configs.addPasses(KEYWORD, KEYWORD); - configs.addPasses(TEXT, KEYWORD); - configs.addFailsUnsupported(KEYWORD, TEXT); - } - - // Test integer types - var integerTypes = List.of(BYTE, SHORT, INTEGER, LONG); - { - TestConfigs configs = testConfigurations.computeIfAbsent("integers", TestConfigs::new); - for (DataType mainType : integerTypes) { - for (DataType lookupType : integerTypes) { - configs.addPasses(mainType, lookupType); + List operations = new ArrayList<>(List.of(BinaryComparisonOperation.values())); + operations.add(null); // null means field-based join + for (BinaryComparisonOperation operation : operations) { + + // Initialize the test configurations for string tests + { + TestConfigs configs = testConfigurations.computeIfAbsent( + new Pair<>("strings", operation), + x -> new TestConfigs(x.cur(), x.cud()) + ); + configs.addPasses(KEYWORD, KEYWORD, operation); + configs.addPasses(TEXT, KEYWORD, operation); + configs.addFailsUnsupported(KEYWORD, TEXT, operation); + } + + // Test integer types + var integerTypes = List.of(BYTE, SHORT, INTEGER, LONG); + { + TestConfigs configs = testConfigurations.computeIfAbsent( + new Pair<>("integers", operation), + x -> new TestConfigs(x.cur(), x.cud()) + ); + for (DataType mainType : integerTypes) { + for (DataType lookupType : integerTypes) { + configs.addPasses(mainType, lookupType, operation); + } } } - } - // Test float and double - var floatTypes = List.of(HALF_FLOAT, FLOAT, DOUBLE, SCALED_FLOAT); - { - TestConfigs configs = testConfigurations.computeIfAbsent("floats", TestConfigs::new); - for (DataType mainType : floatTypes) { - for (DataType lookupType : floatTypes) { - configs.addPasses(mainType, lookupType); + // Test float and double + var floatTypes = List.of(HALF_FLOAT, FLOAT, DOUBLE, SCALED_FLOAT); + { + + TestConfigs configs = testConfigurations.computeIfAbsent( + new Pair<>("floats", operation), + x -> new TestConfigs(x.cur(), x.cud()) + ); + for (DataType mainType : floatTypes) { + for (DataType lookupType : floatTypes) { + configs.addPasses(mainType, lookupType, operation); + } } } - } - // Tests for mixed-numerical types - { - TestConfigs configs = testConfigurations.computeIfAbsent("mixed-numerical", TestConfigs::new); - for (DataType mainType : integerTypes) { - for (DataType lookupType : floatTypes) { - configs.addPasses(mainType, lookupType); - configs.addPasses(lookupType, mainType); + // Tests for mixed-numerical types + { + + TestConfigs configs = testConfigurations.computeIfAbsent( + new Pair<>("mixed-numerical", operation), + x -> new TestConfigs(x.cur(), x.cud()) + ); + for (DataType mainType : integerTypes) { + for (DataType lookupType : floatTypes) { + configs.addPasses(mainType, lookupType, operation); + configs.addPasses(lookupType, mainType, operation); + } } } - } - // Tests for mixed-date/time types - var dateTypes = List.of(DATETIME, DATE_NANOS); - { - TestConfigs configs = testConfigurations.computeIfAbsent("mixed-temporal", TestConfigs::new); - for (DataType mainType : dateTypes) { - for (DataType lookupType : dateTypes) { - if (mainType != lookupType) { - configs.addFails(mainType, lookupType); + // Tests for mixed-date/time types + var dateTypes = List.of(DATETIME, DATE_NANOS); + { + TestConfigs configs = testConfigurations.computeIfAbsent( + new Pair<>("mixed-temporal", operation), + x -> new TestConfigs(x.cur(), x.cud()) + ); + for (DataType mainType : dateTypes) { + for (DataType lookupType : dateTypes) { + if (mainType != lookupType) { + configs.addFails(mainType, lookupType); + } } } } - } - // Union types; non-exhaustive and can be extended - { - TestConfigs configs = testConfigurations.computeIfAbsent("union-types", TestConfigs::new); - configs.addUnionTypePasses(SHORT, INTEGER, INTEGER); - configs.addUnionTypePasses(BYTE, DOUBLE, LONG); - configs.addUnionTypePasses(DATETIME, DATE_NANOS, DATE_NANOS); - configs.addUnionTypePasses(DATE_NANOS, DATETIME, DATETIME); - configs.addUnionTypePasses(SCALED_FLOAT, HALF_FLOAT, DOUBLE); - configs.addUnionTypePasses(TEXT, KEYWORD, KEYWORD); - } + // Union types; non-exhaustive and can be extended + { + TestConfigs configs = testConfigurations.computeIfAbsent( + new Pair<>("union-types", operation), + x -> new TestConfigs(x.cur(), x.cud()) + ); + configs.addUnionTypePasses(SHORT, INTEGER, INTEGER); + configs.addUnionTypePasses(BYTE, DOUBLE, LONG); + configs.addUnionTypePasses(DATETIME, DATE_NANOS, DATE_NANOS); + configs.addUnionTypePasses(DATE_NANOS, DATETIME, DATETIME); + configs.addUnionTypePasses(SCALED_FLOAT, HALF_FLOAT, DOUBLE); + configs.addUnionTypePasses(TEXT, KEYWORD, KEYWORD); - // Tests for all unsupported types - DataType[] unsupported = Join.UNSUPPORTED_TYPES; - { - Collection existing = testConfigurations.values(); - TestConfigs configs = testConfigurations.computeIfAbsent("unsupported", TestConfigs::new); - for (DataType type : unsupported) { - if (type == NULL - || type == DOC_DATA_TYPE - || type == TSID_DATA_TYPE - || type == AGGREGATE_METRIC_DOUBLE - || type == GEOHASH - || type == GEOTILE - || type == GEOHEX - || type.esType() == null - || type.isCounter() - || DataType.isRepresentable(type) == false) { - // Skip unmappable types, or types not supported in ES|QL in general - continue; - } - if (existingIndex(existing, type, type)) { - // Skip existing configurations - continue; + } + + // Tests for all unsupported types + DataType[] unsupported = Join.UNSUPPORTED_TYPES; + boolean isLessOrGreater = operation == BinaryComparisonOperation.GT + || operation == BinaryComparisonOperation.GTE + || operation == BinaryComparisonOperation.LT + || operation == BinaryComparisonOperation.LTE; + { + + Collection existing = testConfigurations.values(); + TestConfigs configs = testConfigurations.computeIfAbsent( + new Pair<>("unsupported", operation), + x -> new TestConfigs(x.cur(), x.cud()) + ); + for (DataType type : unsupported) { + if (type == NULL + || type == DOC_DATA_TYPE + || type == TSID_DATA_TYPE + || type == AGGREGATE_METRIC_DOUBLE + || type == GEOHASH + || type == GEOTILE + || type == GEOHEX + || type.esType() == null + || type.isCounter() + || DataType.isRepresentable(type) == false) { + // Skip unmappable types, or types not supported in ES|QL in general + continue; + } + if (existingIndex(existing, type, type, operation)) { + // Skip existing configurations + continue; + } + if (operation != null && type == DENSE_VECTOR + || isLessOrGreater + && (type == GEO_POINT || type == GEO_SHAPE || type == CARTESIAN_POINT || type == CARTESIAN_SHAPE)) { + configs.addUnsupportedComparisonFails(type, type, operation); + } else { + configs.addFailsUnsupported(type, type, operation); + } } - configs.addFailsUnsupported(type, type); } - } - // Tests for all types where left and right are the same type - DataType[] supported = { - BOOLEAN, - LONG, - INTEGER, - DOUBLE, - SHORT, - BYTE, - FLOAT, - HALF_FLOAT, - DATETIME, - DATE_NANOS, - IP, - KEYWORD, - SCALED_FLOAT }; - { - Collection existing = testConfigurations.values(); - TestConfigs configs = testConfigurations.computeIfAbsent("same", TestConfigs::new); - for (DataType type : supported) { - assertThat("Claiming supported for unsupported type: " + type, List.of(unsupported).contains(type), is(false)); - if (existingIndex(existing, type, type) == false) { - // Only add the configuration if it doesn't already exist - configs.addPasses(type, type); + // Tests for all types where left and right are the same type + DataType[] supported = { + BOOLEAN, + LONG, + INTEGER, + DOUBLE, + SHORT, + BYTE, + FLOAT, + HALF_FLOAT, + DATETIME, + DATE_NANOS, + IP, + KEYWORD, + SCALED_FLOAT }; + { + Collection existing = testConfigurations.values(); + TestConfigs configs = testConfigurations.computeIfAbsent( + new Pair<>("same", operation), + x -> new TestConfigs(x.cur(), x.cud()) + ); + for (DataType type : supported) { + assertThat("Claiming supported for unsupported type: " + type, List.of(unsupported).contains(type), is(false)); + if (existingIndex(existing, type, type, operation) == false) { + // Only add the configuration if it doesn't already exist + if (type == BOOLEAN && isLessOrGreater) { + // Boolean does not support inequality operations + configs.addUnsupportedComparisonFails(type, type, operation); + } else { + configs.addPasses(type, type, operation); + } + } } } - } - // Assert that unsupported types are not in the supported list - for (DataType type : unsupported) { - assertThat("Claiming supported for unsupported type: " + type, List.of(supported).contains(type), is(false)); - } + // Assert that unsupported types are not in the supported list + for (DataType type : unsupported) { + assertThat("Claiming supported for unsupported type: " + type, List.of(supported).contains(type), is(false)); + } - // Assert that unsupported+supported covers all types: - List missing = new ArrayList<>(); - for (DataType type : DataType.values()) { - boolean isUnsupported = List.of(unsupported).contains(type); - boolean isSupported = List.of(supported).contains(type); - if (isUnsupported == false && isSupported == false) { - missing.add(type); + // Assert that unsupported+supported covers all types: + List missing = new ArrayList<>(); + for (DataType type : DataType.values()) { + boolean isUnsupported = List.of(unsupported).contains(type); + boolean isSupported = List.of(supported).contains(type); + if (isUnsupported == false && isSupported == false) { + missing.add(type); + } } - } - assertThat(missing + " are not in the supported or unsupported list", missing.size(), is(0)); + assertThat(missing + " are not in the supported or unsupported list", missing.size(), is(0)); - // Tests for all other type combinations - { - Collection existing = testConfigurations.values(); - TestConfigs configs = testConfigurations.computeIfAbsent("others", TestConfigs::new); - for (DataType mainType : supported) { - for (DataType lookupType : supported) { - if (existingIndex(existing, mainType, lookupType) == false) { - // Only add the configuration if it doesn't already exist - configs.addFails(mainType, lookupType); + // Tests for all other type combinations + { + Collection existing = testConfigurations.values(); + + TestConfigs configs = testConfigurations.computeIfAbsent( + new Pair<>("others", operation), + x -> new TestConfigs(x.cur(), x.cud()) + ); + for (DataType mainType : supported) { + for (DataType lookupType : supported) { + if (existingIndex(existing, mainType, lookupType, operation) == false) { + if (operation == null) { + // Only add the configuration if it doesn't already exist + configs.addFails(mainType, lookupType); + } else if (isLessOrGreater) { + configs.addIncompatibleDifferentTypesLessGreater(mainType, lookupType, operation); + } else { + configs.addMismatchedComparisonFailsEqualNotEqual(mainType, lookupType, operation); + } + } } } } } - // Make sure we have never added two configurations with the same lookup index name. // This prevents accidentally adding the same test config to two different groups. Set knownTypes = new HashSet<>(); @@ -270,8 +356,17 @@ protected Collection> nodePlugins() { } } - private static boolean existingIndex(Collection existing, DataType mainType, DataType lookupType) { - String indexName = LOOKUP_INDEX_PREFIX + mainType.esType() + "_" + lookupType.esType(); + static String stringForOperation(BinaryComparisonOperation operation) { + return operation == null ? "_field" : "_" + operation.name().toLowerCase(Locale.ROOT); + } + + private static boolean existingIndex( + Collection existing, + DataType mainType, + DataType lookupType, + BinaryComparisonOperation operation + ) { + String indexName = LOOKUP_INDEX_PREFIX + mainType.esType() + "_" + lookupType.esType() + stringForOperation(operation); return existing.stream().anyMatch(c -> c.exists(indexName)); } @@ -279,7 +374,7 @@ private static boolean existingIndex(Collection existing, DataType public void testOutputSupportedTypes() throws Exception { Set signatures = new LinkedHashSet<>(); for (TestConfigs configs : testConfigurations.values()) { - if (configs.group.equals("unsupported") || configs.group.equals("union-types")) { + if (configs.group.equals("unsupported") || configs.group.equals("union-types") || configs.operation != null) { continue; } for (TestConfig config : configs.configs.values()) { @@ -300,43 +395,44 @@ public void testOutputSupportedTypes() throws Exception { } public void testLookupJoinStrings() { - testLookupJoinTypes("strings"); + testLookupJoinTypes("strings", operationParameterized); + } public void testLookupJoinIntegers() { - testLookupJoinTypes("integers"); + testLookupJoinTypes("integers", operationParameterized); } public void testLookupJoinFloats() { - testLookupJoinTypes("floats"); + testLookupJoinTypes("floats", operationParameterized); } public void testLookupJoinMixedNumerical() { - testLookupJoinTypes("mixed-numerical"); + testLookupJoinTypes("mixed-numerical", operationParameterized); } public void testLookupJoinMixedTemporal() { - testLookupJoinTypes("mixed-temporal"); + testLookupJoinTypes("mixed-temporal", operationParameterized); } public void testLookupJoinSame() { - testLookupJoinTypes("same"); + testLookupJoinTypes("same", operationParameterized); } public void testLookupJoinUnsupported() { - testLookupJoinTypes("unsupported"); + testLookupJoinTypes("unsupported", operationParameterized); } public void testLookupJoinOthers() { - testLookupJoinTypes("others"); + testLookupJoinTypes("others", operationParameterized); } public void testLookupJoinUnionTypes() { - testLookupJoinTypes("union-types"); + testLookupJoinTypes("union-types", operationParameterized); } - private void testLookupJoinTypes(String group) { - TestConfigs configs = testConfigurations.get(group); + private void testLookupJoinTypes(String group, BinaryComparisonOperation operation) { + TestConfigs configs = testConfigurations.get(new Pair<>(group, operation)); initIndexes(configs); initData(configs); for (TestConfig config : configs.values()) { @@ -373,6 +469,13 @@ private void initData(TestConfigs configs) { indexRandom(true, indexRequests); } + static String suffixLeftFieldName(BinaryComparisonOperation operation) { + if (operation != null) { + return "_left"; + } + return ""; + } + private static String propertyFor(String fieldName, DataType type) { return String.format(Locale.ROOT, "\"%s\": %s", fieldName, sampleDataTextFor(type)); } @@ -446,10 +549,13 @@ private record TestDocument(String indexName, String id, String source) {}; private static class TestConfigs { final String group; final Map configs; + @Nullable + final BinaryComparisonOperation operation; // null means field based join - TestConfigs(String group) { + TestConfigs(String group, BinaryComparisonOperation operation) { this.group = group; this.configs = new LinkedHashMap<>(); + this.operation = operation; } public List indices() { @@ -534,22 +640,24 @@ private void add(TestConfig config) { configs.put(config.lookupIndexName(), config); } - private void addPasses(DataType mainType, DataType lookupType) { - add(new TestConfigPasses(mainType, lookupType)); + private void addPasses(DataType mainType, DataType lookupType, BinaryComparisonOperation operation) { + add(new TestConfigPasses(mainType, lookupType, operation)); } private void addUnionTypePasses(DataType mainType, DataType otherMainType, DataType lookupType) { - add(new TestConfigPassesUnionType(mainType, otherMainType, lookupType)); + add(new TestConfigPassesUnionType(mainType, otherMainType, lookupType, operation)); } private void addFails(DataType mainType, DataType lookupType) { - String fieldName = LOOKUP_INDEX_PREFIX + lookupType.esType(); + String fieldNameLeft = LOOKUP_INDEX_PREFIX + lookupType.esType() + suffixLeftFieldName(operation); + String fieldNameRight = LOOKUP_INDEX_PREFIX + lookupType.esType(); + String errorMessage = String.format( Locale.ROOT, "JOIN left field [%s] of type [%s] is incompatible with right field [%s] of type [%s]", - fieldName, + fieldNameLeft, mainType.widenSmallNumeric(), - fieldName, + fieldNameRight, lookupType.widenSmallNumeric() ); add( @@ -557,12 +665,72 @@ private void addFails(DataType mainType, DataType lookupType) { mainType, lookupType, VerificationException.class, - e -> assertThat(e.getMessage(), containsString(errorMessage)) + e -> assertThat(e.getMessage(), containsString(errorMessage)), + operation ) ); } - private void addFailsUnsupported(DataType mainType, DataType lookupType) { + private void addMismatchedComparisonFailsEqualNotEqual( + DataType mainType, + DataType lookupType, + BinaryComparisonOperation operation + ) { + String fieldNameLeft = LOOKUP_INDEX_PREFIX + lookupType.esType() + suffixLeftFieldName(operation); + String fieldNameRight = LOOKUP_INDEX_PREFIX + lookupType.esType(); + final Consumer assertion = e -> { + String errorMessage1 = String.format( + Locale.ROOT, + "first argument of [%s %s %s] is [", + fieldNameLeft, + operation.symbol(), + fieldNameRight + ); + String errorMessage3 = String.format(Locale.ROOT, " but was [%s]", lookupType.widenSmallNumeric().typeName()); + assertThat(e.getMessage(), containsString(errorMessage1)); + assertThat(e.getMessage(), containsString("] so second argument must also be [")); + assertThat(e.getMessage(), containsString(errorMessage3)); + }; + + add(new TestConfigFails<>(mainType, lookupType, VerificationException.class, assertion, operation)); + } + + private void addIncompatibleDifferentTypesLessGreater(DataType mainType, DataType lookupType, BinaryComparisonOperation operation) { + String fieldNameLeft = LOOKUP_INDEX_PREFIX + lookupType.esType() + suffixLeftFieldName(operation); + String fieldNameRight = LOOKUP_INDEX_PREFIX + lookupType.esType(); + + String errorMessage1 = String.format(Locale.ROOT, "argument of [%s %s %s]", fieldNameLeft, operation.symbol(), fieldNameRight); + + add(new TestConfigFails<>(mainType, lookupType, VerificationException.class, e -> { + assertThat(e.getMessage().toLowerCase(Locale.ROOT), containsString(errorMessage1)); + assertThat( + e.getMessage().toLowerCase(Locale.ROOT), + anyOf(List.of(containsString(mainType.widenSmallNumeric().typeName()), containsString("numeric"))) + ); + assertThat(e.getMessage().toLowerCase(Locale.ROOT), containsString(lookupType.typeName())); + }, operation)); + } + + private void addUnsupportedComparisonFails(DataType mainType, DataType lookupType, BinaryComparisonOperation operation) { + String fieldNameLeft = LOOKUP_INDEX_PREFIX + lookupType.esType() + suffixLeftFieldName(operation); + String fieldNameRight = LOOKUP_INDEX_PREFIX + lookupType.esType(); + + String errorMessage1 = String.format( + Locale.ROOT, + "first argument of [%s %s %s] must be", + fieldNameLeft, + operation.symbol(), + fieldNameRight + ); + String errorMessage2 = String.format(Locale.ROOT, "found value [%s] type [%s]", fieldNameLeft, mainType.typeName()); + + add(new TestConfigFails<>(mainType, lookupType, VerificationException.class, e -> { + assertThat(e.getMessage().toLowerCase(Locale.ROOT), containsString(errorMessage1)); + assertThat(e.getMessage().toLowerCase(Locale.ROOT), containsString(errorMessage2)); + }, operation)); + } + + private void addFailsUnsupported(DataType mainType, DataType lookupType, BinaryComparisonOperation operation) { String fieldName = "lookup_" + lookupType.esType(); String errorMessage = String.format( Locale.ROOT, @@ -575,7 +743,8 @@ private void addFailsUnsupported(DataType mainType, DataType lookupType) { mainType, lookupType, VerificationException.class, - e -> assertThat(e.getMessage(), containsString(errorMessage)) + e -> assertThat(e.getMessage(), containsString(errorMessage)), + operation ) ); } @@ -586,6 +755,8 @@ interface TestConfig { DataType lookupType(); + BinaryComparisonOperation operation(); + default TestMapping mainIndex() { return new TestMapping(MAIN_INDEX, List.of(propertySpecFor(mainFieldName(), mainType())), null); } @@ -615,7 +786,7 @@ default void validateAdditionalMainIndex() { } default String lookupIndexName() { - return LOOKUP_INDEX_PREFIX + mainType().esType() + "_" + lookupType().esType(); + return LOOKUP_INDEX_PREFIX + mainType().esType() + "_" + lookupType().esType() + stringForOperation(operation()); } default TestMapping lookupIndex() { @@ -635,20 +806,39 @@ default String lookupFieldName() { return LOOKUP_INDEX_PREFIX + lookupType().esType(); } - default String testQuery() { - String mainField = mainFieldName(); - String lookupField = lookupFieldName(); - String lookupIndex = lookupIndexName(); - - return String.format( - Locale.ROOT, - "FROM %s | RENAME %s AS %s | LOOKUP JOIN %s ON %s | KEEP other", - MAIN_INDEX, - mainField, - lookupField, - lookupIndex, - lookupField - ); + default String testQuery(BinaryComparisonOperation operation) { + if (operation != null) { + String mainField = mainFieldName(); + String lookupField = lookupFieldName(); + String lookupFieldLeft = lookupFieldName() + suffixLeftFieldName(operation); + String lookupIndex = lookupIndexName(); + + return String.format( + Locale.ROOT, + "FROM %s | EVAL %s = %s | LOOKUP JOIN %s ON %s %s %s | KEEP other", + MAIN_INDEX, + lookupFieldLeft, + mainField, + lookupIndex, + lookupFieldLeft, + operation.symbol(), + lookupField + ); + } else { + String mainField = mainFieldName(); + String lookupField = lookupFieldName(); + String lookupIndex = lookupIndexName(); + + return String.format( + Locale.ROOT, + "FROM %s | RENAME %s AS %s | LOOKUP JOIN %s ON %s | KEEP other", + MAIN_INDEX, + mainField, + lookupField, + lookupIndex, + lookupField + ); + } } void doTest(); @@ -682,15 +872,24 @@ private static void validateIndex(String indexName, String fieldName, Object exp /** * Test case for a pair of types that can successfully be used in {@code LOOKUP JOIN}. */ - private record TestConfigPasses(DataType mainType, DataType lookupType) implements TestConfig { + private record TestConfigPasses(DataType mainType, DataType lookupType, BinaryComparisonOperation operation) implements TestConfig { @Override public void doTest() { - String query = testQuery(); + String query = testQuery(operation); try (var response = EsqlQueryRequestBuilder.newRequestBuilder(client()).query(query).get()) { Iterator results = response.response().column(0).iterator(); assertTrue("Expected at least one result for query: " + query, results.hasNext()); Object indexedResult = response.response().column(0).iterator().next(); - assertThat("Expected valid result: " + query, indexedResult, equalTo("value")); + List valueProducingOperations = new ArrayList<>(); + valueProducingOperations.add(null); + valueProducingOperations.addAll( + List.of(BinaryComparisonOperation.EQ, BinaryComparisonOperation.GTE, BinaryComparisonOperation.LTE) + ); + if (valueProducingOperations.contains(operation)) { + assertThat("Expected valid result: " + query, indexedResult, equalTo("value")); + } else { + assertTrue("Expected valid result: " + query, (indexedResult == null) || (indexedResult.equals("value") == false)); + } } } } @@ -698,15 +897,26 @@ public void doTest() { /** * Test case for a {@code LOOKUP JOIN} where a field with a mapping conflict is cast to the type of the lookup field. */ - private record TestConfigPassesUnionType(DataType mainType, DataType otherMainType, DataType lookupType) implements TestConfig { + private record TestConfigPassesUnionType( + DataType mainType, + DataType otherMainType, + DataType lookupType, + BinaryComparisonOperation operation + ) implements TestConfig { @Override public String lookupIndexName() { // Override so it doesn't clash with other lookup indices from non-union type tests. - return LOOKUP_INDEX_PREFIX + mainType().esType() + "_union_" + otherMainType().esType() + "_" + lookupType().esType(); + return LOOKUP_INDEX_PREFIX + + mainType().esType() + + "_union_" + + otherMainType().esType() + + "_" + + lookupType().esType() + + stringForOperation(operation()); } private String additionalIndexName() { - return mainFieldName() + "_as_" + otherMainType().typeName(); + return mainFieldName() + "_as_" + otherMainType().typeName() + stringForOperation(operation()); } @Override @@ -720,37 +930,67 @@ public void validateAdditionalMainIndex() { } @Override - public String testQuery() { - String mainField = mainFieldName(); - String lookupField = lookupFieldName(); - String lookupIndex = lookupIndexName(); - - return String.format( - Locale.ROOT, - "FROM %s, %s | EVAL %s = %s::%s | LOOKUP JOIN %s ON %s | KEEP other", - MAIN_INDEX, - additionalIndexName(), - lookupField, - mainField, - lookupType.typeName(), - lookupIndex, - lookupField - ); + public String testQuery(BinaryComparisonOperation operation) { + if (operation == null) { + String mainField = mainFieldName(); + String lookupField = lookupFieldName(); + String lookupIndex = lookupIndexName(); + + return String.format( + Locale.ROOT, + "FROM %s, %s | EVAL %s = %s::%s | LOOKUP JOIN %s ON %s | KEEP other", + MAIN_INDEX, + additionalIndexName(), + lookupField, + mainField, + lookupType.typeName(), + lookupIndex, + lookupField + ); + } else { + String mainField = mainFieldName(); + String lookupField = lookupFieldName(); + String lookupIndex = lookupIndexName(); + String lookupFieldLeft = lookupField + suffixLeftFieldName(operation); + + return String.format( + Locale.ROOT, + "FROM %s, %s | EVAL %s = %s::%s | LOOKUP JOIN %s ON %s %s %s | KEEP other", + MAIN_INDEX, + additionalIndexName(), + lookupFieldLeft, + mainField, + lookupType.typeName(), + lookupIndex, + lookupFieldLeft, + operation.symbol(), + lookupField + ); + } } @Override public void doTest() { - String query = testQuery(); + String query = testQuery(operation); try (var response = EsqlQueryRequestBuilder.newRequestBuilder(client()).query(query).get()) { Iterator results = response.response().column(0).iterator(); assertTrue("Expected at least two results for query, but result was empty: " + query, results.hasNext()); Object indexedResult = results.next(); - assertThat("Expected valid result: " + query, indexedResult, equalTo("value")); - - assertTrue("Expected at least two results for query: " + query, results.hasNext()); - indexedResult = results.next(); - assertThat("Expected valid result: " + query, indexedResult, equalTo("value")); + List valueProducingOperations = new ArrayList<>(); + valueProducingOperations.add(null); + valueProducingOperations.addAll( + List.of(BinaryComparisonOperation.EQ, BinaryComparisonOperation.GTE, BinaryComparisonOperation.LTE) + ); + if (valueProducingOperations.contains(operation)) { + assertThat("Expected valid result: " + query, indexedResult, equalTo("value")); + + assertTrue("Expected at least two results for query: " + query, results.hasNext()); + indexedResult = results.next(); + assertThat("Expected valid result: " + query, indexedResult, equalTo("value")); + } else { + assertTrue("Expected valid result: " + query, (indexedResult == null) || (indexedResult.equals("value") == false)); + } } } } @@ -758,12 +998,16 @@ public void doTest() { /** * Test case for a pair of types that generate an error message when used in {@code LOOKUP JOIN}. */ - private record TestConfigFails(DataType mainType, DataType lookupType, Class exception, Consumer assertion) - implements - TestConfig { + private record TestConfigFails( + DataType mainType, + DataType lookupType, + Class exception, + Consumer assertion, + BinaryComparisonOperation operation + ) implements TestConfig { @Override public void doTest() { - String query = testQuery(); + String query = testQuery(operation); E e = expectThrows( exception(), "Expected exception " + exception().getSimpleName() + " but no exception was thrown: " + query, diff --git a/x-pack/plugin/esql/src/main/antlr/parser/Join.g4 b/x-pack/plugin/esql/src/main/antlr/parser/Join.g4 index facbd38fe3908..ca2c3ba8b3a1d 100644 --- a/x-pack/plugin/esql/src/main/antlr/parser/Join.g4 +++ b/x-pack/plugin/esql/src/main/antlr/parser/Join.g4 @@ -18,11 +18,8 @@ joinTarget ; joinCondition - : ON joinPredicate (COMMA joinPredicate)* + : ON booleanExpression (COMMA booleanExpression)* ; -joinPredicate - : valueExpression - ; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java index 4de0c461b0954..01bffd153b4a3 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java @@ -1454,6 +1454,11 @@ public enum Cap { */ URL_DECODE(Build.current().isSnapshot()), + /** + * Allow lookup join on boolean expressions + */ + LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION(Build.current().isSnapshot()), + /** * FORK with remote indices */ diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java index 88ee8dab97aaa..cf63ba5c33c9b 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java @@ -24,6 +24,7 @@ import org.elasticsearch.xpack.esql.core.capabilities.Resolvables; import org.elasticsearch.xpack.esql.core.expression.Alias; import org.elasticsearch.xpack.esql.core.expression.Attribute; +import org.elasticsearch.xpack.esql.core.expression.AttributeSet; import org.elasticsearch.xpack.esql.core.expression.EmptyAttribute; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.Expressions; @@ -90,8 +91,10 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToUnsignedLong; import org.elasticsearch.xpack.esql.expression.function.scalar.nulls.Coalesce; import org.elasticsearch.xpack.esql.expression.function.vector.VectorFunction; +import org.elasticsearch.xpack.esql.expression.predicate.Predicates; import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.DateTimeArithmeticOperation; import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.EsqlArithmeticOperation; +import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.EsqlBinaryComparison; import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.In; import org.elasticsearch.xpack.esql.index.EsIndex; import org.elasticsearch.xpack.esql.index.IndexResolution; @@ -718,10 +721,59 @@ private LogicalPlan resolveLookup(Lookup l, List childrenOutput) { return l; } + private List resolveJoinFiltersAndSwapIfNeeded( + List filters, + AttributeSet leftOutput, + AttributeSet rightOutput + ) { + if (filters.isEmpty()) { + return emptyList(); + } + List childrenOutput = new ArrayList<>(leftOutput); + childrenOutput.addAll(rightOutput); + + List resolvedFilters = new ArrayList<>(filters.size()); + for (Expression filter : filters) { + Expression filterResolved = filter.transformUp(UnresolvedAttribute.class, ua -> maybeResolveAttribute(ua, childrenOutput)); + resolvedFilters.add(resolveAndOrientJoinCondition(filterResolved, leftOutput, rightOutput)); + } + return resolvedFilters; + } + + private Expression resolveAndOrientJoinCondition(Expression condition, AttributeSet leftOutput, AttributeSet rightOutput) { + if (condition instanceof EsqlBinaryComparison comp + && comp.left() instanceof Attribute leftAttr + && comp.right() instanceof Attribute rightAttr) { + + boolean leftIsFromLeft = leftOutput.contains(leftAttr); + boolean rightIsFromRight = rightOutput.contains(rightAttr); + + if (leftIsFromLeft && rightIsFromRight) { + return comp; // Correct orientation + } + + boolean leftIsFromRight = rightOutput.contains(leftAttr); + boolean rightIsFromLeft = leftOutput.contains(rightAttr); + + if (leftIsFromRight && rightIsFromLeft) { + return comp.swapLeftAndRight(); // Swapped orientation + } + + // Invalid orientation (e.g., both from left or both from right) + throw new IllegalArgumentException( + "Join condition must be between one attribute on the left side and " + + "one attribute on the right side of the join, but found: " + + condition.sourceText() + ); + } + return condition; // Not a binary comparison between two attributes, no change needed. + } + private Join resolveLookupJoin(LookupJoin join) { JoinConfig config = join.config(); // for now, support only (LEFT) USING clauses JoinType type = config.type(); + // rewrite the join into an equi-join between the field with the same name between left and right if (type instanceof UsingJoinType using) { List cols = using.columns(); @@ -739,22 +791,46 @@ private Join resolveLookupJoin(LookupJoin join) { name, "Only LEFT join is supported with USING" ); - return join.withConfig(new JoinConfig(type, singletonList(errorAttribute), emptyList(), emptyList())); + return join.withConfig(new JoinConfig(type, singletonList(errorAttribute), emptyList(), null)); + } + List leftKeys = new ArrayList<>(); + List rightKeys = new ArrayList<>(); + List resolvedFilters = new ArrayList<>(); + if (join.config().joinOnConditions() != null) { + resolvedFilters = resolveJoinFiltersAndSwapIfNeeded( + Predicates.splitAnd(join.config().joinOnConditions()), + join.left().outputSet(), + join.right().outputSet() + ); + // build leftKeys and rightKeys using the correct side of the resolvedFilters. + // resolveJoinFiltersAndSwapIfNeeded already put the left and right on the correct side + for (Expression expression : resolvedFilters) { + if (expression instanceof EsqlBinaryComparison binaryComparison + && binaryComparison.left() instanceof Attribute leftAttribute + && binaryComparison.right() instanceof Attribute rightAttribute) { + leftKeys.add(leftAttribute); + rightKeys.add(rightAttribute); + } else { + throw new IllegalArgumentException("Unsupported join filter expression: " + expression); + } + } + } else { + // resolve the using columns against the left and the right side then assemble the new join config + leftKeys = resolveUsingColumns(cols, join.left().output(), "left"); + rightKeys = resolveUsingColumns(cols, join.right().output(), "right"); } - // resolve the using columns against the left and the right side then assemble the new join config - List leftKeys = resolveUsingColumns(cols, join.left().output(), "left"); - List rightKeys = resolveUsingColumns(cols, join.right().output(), "right"); - config = new JoinConfig(coreJoin, leftKeys, leftKeys, rightKeys); - join = new LookupJoin(join.source(), join.left(), join.right(), config, join.isRemote()); + config = new JoinConfig(coreJoin, leftKeys, rightKeys, Predicates.combineAnd(resolvedFilters)); + return new LookupJoin(join.source(), join.left(), join.right(), config, join.isRemote()); } else if (type != JoinTypes.LEFT) { // everything else is unsupported for now // LEFT can only happen by being mapped from a USING above. So we need to exclude this as well because this rule can be run // more than once. UnresolvedAttribute errorAttribute = new UnresolvedAttribute(join.source(), "unsupported", "Unsupported join type"); // add error message - return join.withConfig(new JoinConfig(type, singletonList(errorAttribute), emptyList(), emptyList())); + return join.withConfig(new JoinConfig(type, singletonList(errorAttribute), emptyList(), null)); } + return join; } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/AnalyzerRules.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/AnalyzerRules.java index 4889583d1b8a9..91c4015d40a39 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/AnalyzerRules.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/AnalyzerRules.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.esql.analysis; +import org.elasticsearch.xpack.esql.VerificationException; import org.elasticsearch.xpack.esql.core.expression.Attribute; import org.elasticsearch.xpack.esql.core.expression.UnresolvedAttribute; import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan; @@ -116,7 +117,6 @@ public static List maybeResolveAgainstList( int colDiff = a.sourceLocation().getColumnNumber() - b.sourceLocation().getColumnNumber(); return lineDiff != 0 ? lineDiff : (colDiff != 0 ? colDiff : a.name().compareTo(b.name())); }).map(a -> "line " + a.sourceLocation().toString().substring(1) + " [" + a.name() + "]").toList(); - - throw new IllegalStateException("Reference [" + ua.name() + "] is ambiguous; " + "matches any of " + refs); + throw new VerificationException("Found ambiguous reference to [" + ua.name() + "]; " + "matches any of " + refs); } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/AbstractLookupService.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/AbstractLookupService.java index b52c6472f962a..c834b538c5e86 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/AbstractLookupService.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/AbstractLookupService.java @@ -558,6 +558,10 @@ abstract static class TransportRequest extends AbstractTransportRequest implemen this.source = source; } + public Page getInputPage() { + return inputPage; + } + @Override public final String[] indices() { return new String[] { indexPattern }; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/BinaryComparisonQueryList.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/BinaryComparisonQueryList.java new file mode 100644 index 0000000000000..883589e3b93cf --- /dev/null +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/BinaryComparisonQueryList.java @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.enrich; + +import org.apache.lucene.search.Query; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.operator.Warnings; +import org.elasticsearch.compute.operator.lookup.QueryList; +import org.elasticsearch.index.mapper.MappedFieldType; +import org.elasticsearch.index.query.SearchExecutionContext; +import org.elasticsearch.search.internal.AliasFilter; +import org.elasticsearch.xpack.esql.capabilities.TranslationAware; +import org.elasticsearch.xpack.esql.core.expression.Literal; +import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.EsqlBinaryComparison; +import org.elasticsearch.xpack.esql.optimizer.rules.physical.local.LucenePushdownPredicates; +import org.elasticsearch.xpack.esql.planner.TranslatorHandler; +import org.elasticsearch.xpack.esql.plugin.EsqlFlags; +import org.elasticsearch.xpack.esql.stats.SearchContextStats; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.List; +import java.util.function.IntFunction; + +/** + * A {@link QueryList} that generates a query for a binary comparison. + * This class is used in the context of an expression based lookup join, + * where we need to generate a query for each row of the left dataset. + * The query is then used to fetch the matching rows from the right dataset. + * The query is a binary comparison between a field from the right dataset + * and a single value from the left dataset. e.g right_id > 5 + * The value is extracted from a block at a given position. + * The comparison is then translated to a Lucene query. + * If the comparison cannot be translated, an exception is thrown. + * This class is used in conjunction with {@link ExpressionQueryList} to generate the final query for the lookup join. + */ +public class BinaryComparisonQueryList extends QueryList { + private final EsqlBinaryComparison binaryComparison; + private final IntFunction blockValueReader; + private final SearchExecutionContext searchExecutionContext; + private final LucenePushdownPredicates lucenePushdownPredicates; + + public BinaryComparisonQueryList( + MappedFieldType field, + SearchExecutionContext searchExecutionContext, + Block leftHandSideBlock, + EsqlBinaryComparison binaryComparison, + ClusterService clusterService, + AliasFilter aliasFilter, + Warnings warnings + ) { + super( + field, + searchExecutionContext, + aliasFilter, + leftHandSideBlock, + new OnlySingleValueParams(warnings, "LOOKUP JOIN encountered multi-value") + ); + // swap left and right if the field is on the right + // We get a filter in the form left_expr >= right_expr + // here we will swap it to right_expr <= left_expr + // and later in doGetQuery we will replace left_expr with the value from the leftHandSideBlock + // We do that because binaryComparison expects the field to be on the left and the literal on the right to be translatable + this.binaryComparison = (EsqlBinaryComparison) binaryComparison.swapLeftAndRight(); + this.blockValueReader = QueryList.createBlockValueReader(leftHandSideBlock); + this.searchExecutionContext = searchExecutionContext; + lucenePushdownPredicates = LucenePushdownPredicates.from( + SearchContextStats.from(List.of(searchExecutionContext)), + new EsqlFlags(clusterService.getClusterSettings()) + ); + } + + @Override + public QueryList onlySingleValues(Warnings warnings, String multiValueWarningMessage) { + throw new UnsupportedOperationException(); + } + + @Override + public Query doGetQuery(int position, int firstValueIndex, int valueCount) { + Object value = blockValueReader.apply(firstValueIndex); + // create a new comparison with the value from the block as a literal + EsqlBinaryComparison comparison = binaryComparison.getFunctionType() + .buildNewInstance( + binaryComparison.source(), + binaryComparison.left(), + new Literal(binaryComparison.right().source(), value, binaryComparison.right().dataType()) + ); + try { + if (TranslationAware.Translatable.YES.equals(comparison.translatable(lucenePushdownPredicates))) { + return comparison.asQuery(lucenePushdownPredicates, TranslatorHandler.TRANSLATOR_HANDLER) + .toQueryBuilder() + .toQuery(searchExecutionContext); + } else { + throw new IllegalStateException("Cannot translate join condition: " + binaryComparison); + } + } catch (IOException e) { + throw new UncheckedIOException("Error while building query for join on filter:", e); + } + } +} diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/ExpressionQueryList.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/ExpressionQueryList.java index 47d137bbbfe83..9cd2bfb1b0c35 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/ExpressionQueryList.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/ExpressionQueryList.java @@ -11,13 +11,20 @@ import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.Query; import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.Warnings; import org.elasticsearch.compute.operator.lookup.LookupEnrichQueryGenerator; import org.elasticsearch.compute.operator.lookup.QueryList; +import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.SearchExecutionContext; +import org.elasticsearch.search.internal.AliasFilter; import org.elasticsearch.xpack.esql.capabilities.TranslationAware; +import org.elasticsearch.xpack.esql.core.expression.Attribute; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.expression.predicate.Predicates; +import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.EsqlBinaryComparison; import org.elasticsearch.xpack.esql.optimizer.rules.physical.local.LucenePushdownPredicates; import org.elasticsearch.xpack.esql.plan.physical.EsSourceExec; import org.elasticsearch.xpack.esql.plan.physical.FilterExec; @@ -30,6 +37,7 @@ import java.util.ArrayList; import java.util.List; +import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION; import static org.elasticsearch.xpack.esql.planner.TranslatorHandler.TRANSLATOR_HANDLER; /** @@ -37,24 +45,155 @@ * Each query in the resulting query will be a conjunction of all queries from the input lists at the same position. * In addition, we support an optional pre-join filter that will be applied to all queries if it is pushable. * If the pre-join filter cannot be pushed down to Lucene, it will be ignored. + * This class is used in the context of a lookup join, where we need to generate a query for each row of the left dataset. + * The query is then used to fetch the matching rows from the right dataset. + * The class supports two types of joins: + * 1. Field-based join: The join conditions are based on the equality of fields from the left and right datasets. + * It is used for field-based join when the join is on more than one field or there is a preJoinFilter + * 2. Expression-based join: The join conditions are based on a complex expression that can involve multiple fields and operators. */ public class ExpressionQueryList implements LookupEnrichQueryGenerator { private final List queryLists; private final List preJoinFilters = new ArrayList<>(); private final SearchExecutionContext context; + private final AliasFilter aliasFilter; - public ExpressionQueryList( + private ExpressionQueryList( List queryLists, SearchExecutionContext context, PhysicalPlan rightPreJoinPlan, - ClusterService clusterService + ClusterService clusterService, + AliasFilter aliasFilter + ) { + this.queryLists = new ArrayList<>(queryLists); + this.context = context; + this.aliasFilter = aliasFilter; + buildPreJoinFilter(rightPreJoinPlan, clusterService); + } + + /** + * Creates a new {@link ExpressionQueryList} for a field-based join. + * A field-based join is a join where the join conditions are based on the equality of fields from the left and right datasets. + * For example | LOOKUP JOIN on field1, field2, field3 + * The query lists are generated from the join conditions. + * The pre-join filter is an optional filter that is applied to the right dataset before the join. + */ + public static ExpressionQueryList fieldBasedJoin( + List queryLists, + SearchExecutionContext context, + PhysicalPlan rightPreJoinPlan, + ClusterService clusterService, + AliasFilter aliasFilter ) { if (queryLists.size() < 2 && (rightPreJoinPlan instanceof FilterExec == false)) { throw new IllegalArgumentException("ExpressionQueryList must have at least two QueryLists or a pre-join filter"); } - this.queryLists = queryLists; - this.context = context; - buildPreJoinFilter(rightPreJoinPlan, clusterService); + return new ExpressionQueryList(queryLists, context, rightPreJoinPlan, clusterService, aliasFilter); + } + + /** + * Creates a new {@link ExpressionQueryList} for an expression-based join. + * An expression-based join is a join where the join conditions are based on a complex expression + * that can involve multiple fields and operators. + * Example | LOOKUP JOIN on left_field > right_field AND left_field2 == right_field2 + * The query lists are generated from the join conditions. + * The pre-join filter is an optional filter that is applied to the right dataset before the join. + */ + public static ExpressionQueryList expressionBasedJoin( + SearchExecutionContext context, + PhysicalPlan rightPreJoinPlan, + ClusterService clusterService, + LookupFromIndexService.TransportRequest request, + AliasFilter aliasFilter, + Warnings warnings + ) { + if (LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled() == false) { + throw new UnsupportedOperationException("Lookup Join on Boolean Expression capability is not enabled"); + } + if (request.getJoinOnConditions() == null) { + throw new IllegalStateException("expressionBasedJoin must have join conditions"); + } + ExpressionQueryList expressionQueryList = new ExpressionQueryList( + new ArrayList<>(), + context, + rightPreJoinPlan, + clusterService, + aliasFilter + ); + expressionQueryList.buildJoinOnForExpressionJoin( + request.getJoinOnConditions(), + request.getMatchFields(), + request.getInputPage(), + clusterService, + warnings + ); + return expressionQueryList; + } + + private void buildJoinOnForExpressionJoin( + Expression joinOnConditions, + List matchFields, + Page inputPage, + ClusterService clusterService, + Warnings warnings + ) { + List expressions = Predicates.splitAnd(joinOnConditions); + for (Expression expr : expressions) { + if (expr instanceof EsqlBinaryComparison binaryComparison) { + // the left side comes from the page that was sent to the lookup node + // the right side is the field from the lookup index + // check if the left side is in the matchFields + // if it is its corresponding page is the corresponding number in inputPage + Expression left = binaryComparison.left(); + if (left instanceof Attribute leftAttribute) { + boolean matched = false; + for (int i = 0; i < matchFields.size(); i++) { + if (matchFields.get(i).fieldName().equals(leftAttribute.name())) { + Block block = inputPage.getBlock(i); + Expression right = binaryComparison.right(); + if (right instanceof Attribute rightAttribute) { + MappedFieldType fieldType = context.getFieldType(rightAttribute.name()); + if (fieldType != null) { + queryLists.add( + new BinaryComparisonQueryList( + fieldType, + context, + block, + binaryComparison, + clusterService, + aliasFilter, + warnings + ) + ); + matched = true; + break; + } else { + throw new IllegalStateException( + "Could not find field [" + rightAttribute.name() + "] in the lookup join index" + ); + } + } else { + throw new IllegalStateException( + "Only field from the right dataset are supported on the right of the join on condition but got: " + expr + ); + } + } + } + if (matched == false) { + throw new IllegalStateException( + "Could not find field [" + leftAttribute.name() + "] in the left side of the lookup join" + ); + } + } else { + throw new IllegalStateException( + "Only field from the left dataset are supported on the left of the join on condition but got: " + expr + ); + } + } else { + // we only support binary comparisons in the join on conditions + throw new IllegalStateException("Only binary comparisons are supported in join ON conditions, but got: " + expr); + } + } } private void addToPreJoinFilters(QueryBuilder query) { @@ -93,6 +232,13 @@ private void buildPreJoinFilter(PhysicalPlan rightPreJoinPlan, ClusterService cl } } + /** + * Returns the query at the given position. + * The query is a conjunction of all queries from the input lists at the same position. + * If a pre-join filter exists, it is also added to the query. + * @param position The position of the query to return. + * @return The query at the given position, or null if any of the match fields are null. + */ @Override public Query getQuery(int position) { BooleanQuery.Builder builder = new BooleanQuery.Builder(); @@ -112,6 +258,12 @@ public Query getQuery(int position) { return builder.build(); } + /** + * Returns the number of positions in the query list. + * The number of positions is the same for all query lists. + * @return The number of positions in the query list. + * @throws IllegalArgumentException if the query lists have different position counts. + */ @Override public int getPositionCount() { int positionCount = queryLists.get(0).getPositionCount(); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/LookupFromIndexOperator.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/LookupFromIndexOperator.java index c7cc98152c92c..3fbb54aa3a365 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/LookupFromIndexOperator.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/LookupFromIndexOperator.java @@ -24,6 +24,7 @@ import org.elasticsearch.core.Releasables; import org.elasticsearch.tasks.CancellableTask; import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.NamedExpression; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.plan.physical.PhysicalPlan; @@ -49,7 +50,8 @@ public record Factory( String lookupIndex, List loadFields, Source source, - PhysicalPlan rightPreJoinPlan + PhysicalPlan rightPreJoinPlan, + Expression joinOnConditions ) implements OperatorFactory { @Override public String describe() { @@ -59,11 +61,12 @@ public String describe() { stringBuilder.append(" input_type=") .append(matchField.type()) .append(" match_field=") - .append(matchField.fieldName().string()) + .append(matchField.fieldName()) .append(" inputChannel=") .append(matchField.channel()); } stringBuilder.append(" right_pre_join_plan=").append(rightPreJoinPlan == null ? "null" : rightPreJoinPlan.toString()); + stringBuilder.append(" join_on_expression=").append(joinOnConditions == null ? "null" : joinOnConditions.toString()); stringBuilder.append("]"); return stringBuilder.toString(); } @@ -81,7 +84,8 @@ public Operator get(DriverContext driverContext) { lookupIndex, loadFields, source, - rightPreJoinPlan + rightPreJoinPlan, + joinOnConditions ); } } @@ -96,6 +100,7 @@ public Operator get(DriverContext driverContext) { private long totalRows = 0L; private final List matchFields; private final PhysicalPlan rightPreJoinPlan; + private final Expression joinOnConditions; /** * Total number of pages emitted by this {@link Operator}. */ @@ -120,7 +125,8 @@ public LookupFromIndexOperator( String lookupIndex, List loadFields, Source source, - PhysicalPlan rightPreJoinPlan + PhysicalPlan rightPreJoinPlan, + Expression joinOnConditions ) { super(driverContext, lookupService.getThreadContext(), maxOutstandingRequests); this.matchFields = matchFields; @@ -132,6 +138,7 @@ public LookupFromIndexOperator( this.loadFields = loadFields; this.source = source; this.rightPreJoinPlan = rightPreJoinPlan; + this.joinOnConditions = joinOnConditions; } @Override @@ -159,7 +166,8 @@ protected void performAsync(Page inputPage, ActionListener listener new Page(inputBlockArray), loadFields, source, - rightPreJoinPlan + rightPreJoinPlan, + joinOnConditions ); lookupService.lookupAsync( request, @@ -215,12 +223,13 @@ public String toString() { stringBuilder.append(" input_type=") .append(matchField.type()) .append(" match_field=") - .append(matchField.fieldName().string()) + .append(matchField.fieldName()) .append(" inputChannel=") .append(matchField.channel()); } stringBuilder.append(" right_pre_join_plan=").append(rightPreJoinPlan == null ? "null" : rightPreJoinPlan.toString()); + stringBuilder.append(" join_on_expression=").append(joinOnConditions == null ? "null" : joinOnConditions.toString()); stringBuilder.append("]"); return stringBuilder.toString(); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/LookupFromIndexService.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/LookupFromIndexService.java index eaafe6964aaa5..e912a3128f355 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/LookupFromIndexService.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/LookupFromIndexService.java @@ -34,7 +34,7 @@ import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.esql.EsqlIllegalArgumentException; import org.elasticsearch.xpack.esql.action.EsqlQueryAction; -import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; +import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.NamedExpression; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; @@ -98,7 +98,8 @@ protected TransportRequest transportRequest(LookupFromIndexService.Request reque request.extractFields, request.matchFields, request.source, - request.rightPreJoinPlan + request.rightPreJoinPlan, + request.joinOnConditions ); } @@ -110,25 +111,29 @@ protected LookupEnrichQueryGenerator queryList( Block inputBlock, Warnings warnings ) { - List queryLists = new ArrayList<>(); - for (int i = 0; i < request.matchFields.size(); i++) { - MatchConfig matchField = request.matchFields.get(i); - QueryList q = termQueryList( - context.getFieldType(matchField.fieldName().string()), - context, - aliasFilter, - request.inputPage.getBlock(matchField.channel()), - matchField.type() - ).onlySingleValues(warnings, "LOOKUP JOIN encountered multi-value"); - queryLists.add(q); - } - - PhysicalPlan physicalPlan = request.rightPreJoinPlan; - physicalPlan = localLookupNodePlanning(physicalPlan); - if (queryLists.size() == 1 && physicalPlan instanceof FilterExec == false) { - return queryLists.getFirst(); + PhysicalPlan lookupNodePlan = localLookupNodePlanning(request.rightPreJoinPlan); + if (request.joinOnConditions == null) { + // this is a field based join + List queryLists = new ArrayList<>(); + for (int i = 0; i < request.matchFields.size(); i++) { + MatchConfig matchField = request.matchFields.get(i); + QueryList q = termQueryList( + context.getFieldType(matchField.fieldName()), + context, + aliasFilter, + request.inputPage.getBlock(matchField.channel()), + matchField.type() + ).onlySingleValues(warnings, "LOOKUP JOIN encountered multi-value"); + queryLists.add(q); + } + if (queryLists.size() == 1 && lookupNodePlan instanceof FilterExec == false) { + return queryLists.getFirst(); + } + return ExpressionQueryList.fieldBasedJoin(queryLists, context, lookupNodePlan, clusterService, aliasFilter); + } else { + // this is an expression based join + return ExpressionQueryList.expressionBasedJoin(context, lookupNodePlan, clusterService, request, aliasFilter, warnings); } - return new ExpressionQueryList(queryLists, context, physicalPlan, clusterService); } @@ -163,6 +168,7 @@ protected AbstractLookupService.LookupResponse readLookupResponse(StreamInput in public static class Request extends AbstractLookupService.Request { private final List matchFields; private final PhysicalPlan rightPreJoinPlan; + private final Expression joinOnConditions; Request( String sessionId, @@ -172,11 +178,13 @@ public static class Request extends AbstractLookupService.Request { Page inputPage, List extractFields, Source source, - PhysicalPlan rightPreJoinPlan + PhysicalPlan rightPreJoinPlan, + Expression joinOnConditions ) { super(sessionId, index, indexPattern, matchFields.get(0).type(), inputPage, extractFields, source); this.matchFields = matchFields; this.rightPreJoinPlan = rightPreJoinPlan; + this.joinOnConditions = joinOnConditions; } } @@ -188,6 +196,7 @@ protected static class TransportRequest extends AbstractLookupService.TransportR private final List matchFields; private final PhysicalPlan rightPreJoinPlan; + private final Expression joinOnConditions; // Right now we assume that the page contains the same number of blocks as matchFields and that the blocks are in the same order // The channel information inside the MatchConfig, should say the same thing @@ -200,11 +209,13 @@ protected static class TransportRequest extends AbstractLookupService.TransportR List extractFields, List matchFields, Source source, - PhysicalPlan rightPreJoinPlan + PhysicalPlan rightPreJoinPlan, + Expression joinOnConditions ) { super(sessionId, shardId, indexPattern, inputPage, toRelease, extractFields, source); this.matchFields = matchFields; this.rightPreJoinPlan = rightPreJoinPlan; + this.joinOnConditions = joinOnConditions; } static TransportRequest readFrom(StreamInput in, BlockFactory blockFactory) throws IOException { @@ -238,7 +249,7 @@ static TransportRequest readFrom(StreamInput in, BlockFactory blockFactory) thro String matchField = in.readString(); // For older versions, we only support a single match field. matchFields = new ArrayList<>(1); - matchFields.add(new MatchConfig(new FieldAttribute.FieldName(matchField), 0, inputDataType)); + matchFields.add(new MatchConfig(matchField, 0, inputDataType)); } var source = Source.EMPTY; if (in.getTransportVersion().onOrAfter(TransportVersions.V_8_17_0)) { @@ -254,6 +265,10 @@ static TransportRequest readFrom(StreamInput in, BlockFactory blockFactory) thro if (in.getTransportVersion().supports(ESQL_LOOKUP_JOIN_PRE_JOIN_FILTER)) { rightPreJoinPlan = planIn.readOptionalNamedWriteable(PhysicalPlan.class); } + Expression joinOnConditions = null; + if (in.getTransportVersion().onOrAfter(TransportVersions.ESQL_LOOKUP_JOIN_ON_EXPRESSION)) { + joinOnConditions = planIn.readOptionalNamedWriteable(Expression.class); + } TransportRequest result = new TransportRequest( sessionId, shardId, @@ -263,12 +278,21 @@ static TransportRequest readFrom(StreamInput in, BlockFactory blockFactory) thro extractFields, matchFields, source, - rightPreJoinPlan + rightPreJoinPlan, + joinOnConditions ); result.setParentTask(parentTaskId); return result; } + public Expression getJoinOnConditions() { + return joinOnConditions; + } + + public List getMatchFields() { + return matchFields; + } + @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); @@ -298,7 +322,7 @@ public void writeTo(StreamOutput out) throws IOException { } else { // older versions only support a single match field, we already checked this above when writing the datatype // send the field name of the first and only match field here - out.writeString(matchFields.get(0).fieldName().string()); + out.writeString(matchFields.get(0).fieldName()); } if (out.getTransportVersion().onOrAfter(TransportVersions.V_8_17_0)) { source.writeTo(planOut); @@ -309,12 +333,19 @@ public void writeTo(StreamOutput out) throws IOException { if (out.getTransportVersion().supports(ESQL_LOOKUP_JOIN_PRE_JOIN_FILTER)) { planOut.writeOptionalNamedWriteable(rightPreJoinPlan); } + if (out.getTransportVersion().onOrAfter(TransportVersions.ESQL_LOOKUP_JOIN_ON_EXPRESSION)) { + planOut.writeOptionalNamedWriteable(joinOnConditions); + } else { + if (joinOnConditions != null) { + throw new IllegalArgumentException("LOOKUP JOIN with ON conditions is not supported on remote node"); + } + } } @Override protected String extraDescription() { return " ,match_fields=" - + matchFields.stream().map(x -> x.fieldName().string()).collect(Collectors.joining(", ")) + + matchFields.stream().map(MatchConfig::fieldName).collect(Collectors.joining(", ")) + ", right_pre_join_plan=" + (rightPreJoinPlan == null ? "null" : rightPreJoinPlan.toString()); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/MatchConfig.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/MatchConfig.java index 616c6710eff48..ed3da0d2da49e 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/MatchConfig.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/MatchConfig.java @@ -10,42 +10,57 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; -import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.planner.Layout; import java.io.IOException; import java.util.Objects; +/** + * Configuration for a field used in the join condition of a LOOKUP JOIN or ENRICH operation. + *

+ * This class specifies how to match a field from the input data (the "left" side of the join) + * with a field in the lookup index (the "right" side). The interpretation of its properties + * depends on the type of join. + *

+ * For simple field-based joins (e.g., {@code ... ON field1, field2}), this configuration + * represents the right-side field ({@code right.field}). In this case, {@link #fieldName} is the + * name of the field in the lookup index used to build the query. + *

+ * For expression-based joins (e.g., {@code ... ON left_field > right_field}), this + * configuration represents the left-side field ({@code left_field}). In this case, + * {@link #fieldName} is the name of the field whose value is sent to the lookup node. + *

+ * The {@link #channel} identifies the position of this field's values within the internal + * page sent to the lookup node. + */ public final class MatchConfig implements Writeable { - private final FieldAttribute.FieldName fieldName; + private final String fieldName; private final int channel; private final DataType type; - public MatchConfig(FieldAttribute.FieldName fieldName, int channel, DataType type) { + public MatchConfig(String fieldName, int channel, DataType type) { this.fieldName = fieldName; this.channel = channel; this.type = type; } - public MatchConfig(FieldAttribute match, Layout.ChannelAndType input) { - // TODO: Using exactAttribute was supposed to handle TEXT fields with KEYWORD subfields - but we don't allow these in lookup - // indices, so the call to exactAttribute looks redundant now. - this(match.exactAttribute().fieldName(), input.channel(), input.type()); + public MatchConfig(String fieldName, Layout.ChannelAndType input) { + this(fieldName, input.channel(), input.type()); } public MatchConfig(StreamInput in) throws IOException { - this(new FieldAttribute.FieldName(in.readString()), in.readInt(), DataType.readFrom(in)); + this(in.readString(), in.readInt(), DataType.readFrom(in)); } @Override public void writeTo(StreamOutput out) throws IOException { - out.writeString(fieldName.string()); + out.writeString(fieldName); out.writeInt(channel); type.writeTo(out); } - public FieldAttribute.FieldName fieldName() { + public String fieldName() { return fieldName; } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java index 16a38671db62c..2e06db66a85e5 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java @@ -254,7 +254,7 @@ private static List arithmetics() { return List.of(Add.ENTRY, Div.ENTRY, Mod.ENTRY, Mul.ENTRY, Sub.ENTRY); } - private static List binaryComparisons() { + public static List binaryComparisons() { return List.of(Equals.ENTRY, GreaterThan.ENTRY, GreaterThanOrEqual.ENTRY, LessThan.ENTRY, LessThanOrEqual.ENTRY, NotEquals.ENTRY); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/PruneLeftJoinOnNullMatchingField.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/PruneLeftJoinOnNullMatchingField.java index e46ab04e8fc91..418a292c3e154 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/PruneLeftJoinOnNullMatchingField.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/PruneLeftJoinOnNullMatchingField.java @@ -40,7 +40,7 @@ protected LogicalPlan rule(Join join, LogicalOptimizerContext ctx) { if (join.config().type() == LEFT) { // other types will have different replacement logic AttributeMap attributeMap = RuleUtils.foldableReferences(join, ctx); - for (var attr : AttributeSet.of(join.config().matchFields())) { + for (var attr : AttributeSet.of(join.config().leftFields())) { var resolved = attributeMap.resolve(attr); if (resolved != null && isGuaranteedNull(resolved)) { plan = replaceJoin(join); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParser.interp b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParser.interp index ff04c6a4dd735..251311f78d789 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParser.interp +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParser.interp @@ -381,8 +381,7 @@ comparisonOperator joinCommand joinTarget joinCondition -joinPredicate atn: -[4, 1, 143, 899, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 2, 66, 7, 66, 2, 67, 7, 67, 2, 68, 7, 68, 2, 69, 7, 69, 2, 70, 7, 70, 2, 71, 7, 71, 2, 72, 7, 72, 2, 73, 7, 73, 2, 74, 7, 74, 2, 75, 7, 75, 2, 76, 7, 76, 2, 77, 7, 77, 2, 78, 7, 78, 2, 79, 7, 79, 2, 80, 7, 80, 2, 81, 7, 81, 2, 82, 7, 82, 2, 83, 7, 83, 2, 84, 7, 84, 2, 85, 7, 85, 2, 86, 7, 86, 2, 87, 7, 87, 2, 88, 7, 88, 2, 89, 7, 89, 2, 90, 7, 90, 1, 0, 1, 0, 4, 0, 185, 8, 0, 11, 0, 12, 0, 186, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 3, 0, 195, 8, 0, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 5, 2, 206, 8, 2, 10, 2, 12, 2, 209, 9, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 3, 218, 8, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 3, 4, 246, 8, 4, 1, 5, 1, 5, 1, 5, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 5, 8, 259, 8, 8, 10, 8, 12, 8, 262, 9, 8, 1, 9, 1, 9, 1, 9, 3, 9, 267, 8, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 5, 10, 274, 8, 10, 10, 10, 12, 10, 277, 9, 10, 1, 11, 1, 11, 1, 11, 3, 11, 282, 8, 11, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 5, 14, 293, 8, 14, 10, 14, 12, 14, 296, 9, 14, 1, 14, 3, 14, 299, 8, 14, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 3, 15, 310, 8, 15, 1, 16, 1, 16, 1, 17, 1, 17, 1, 18, 1, 18, 1, 19, 1, 19, 1, 20, 1, 20, 1, 20, 1, 20, 5, 20, 324, 8, 20, 10, 20, 12, 20, 327, 9, 20, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 3, 22, 334, 8, 22, 1, 22, 1, 22, 3, 22, 338, 8, 22, 1, 23, 1, 23, 1, 23, 5, 23, 343, 8, 23, 10, 23, 12, 23, 346, 9, 23, 1, 24, 1, 24, 1, 24, 3, 24, 351, 8, 24, 1, 25, 1, 25, 1, 25, 3, 25, 356, 8, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 3, 25, 365, 8, 25, 1, 26, 1, 26, 1, 26, 5, 26, 370, 8, 26, 10, 26, 12, 26, 373, 9, 26, 1, 27, 1, 27, 1, 27, 3, 27, 378, 8, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 3, 27, 387, 8, 27, 1, 28, 1, 28, 1, 28, 5, 28, 392, 8, 28, 10, 28, 12, 28, 395, 9, 28, 1, 29, 1, 29, 1, 29, 5, 29, 400, 8, 29, 10, 29, 12, 29, 403, 9, 29, 1, 30, 1, 30, 1, 31, 1, 31, 1, 31, 3, 31, 410, 8, 31, 1, 32, 1, 32, 3, 32, 414, 8, 32, 1, 33, 1, 33, 3, 33, 418, 8, 33, 1, 34, 1, 34, 1, 34, 3, 34, 423, 8, 34, 1, 35, 1, 35, 1, 35, 1, 36, 1, 36, 1, 36, 1, 36, 5, 36, 432, 8, 36, 10, 36, 12, 36, 435, 9, 36, 1, 37, 1, 37, 3, 37, 439, 8, 37, 1, 37, 1, 37, 3, 37, 443, 8, 37, 1, 38, 1, 38, 1, 38, 1, 39, 1, 39, 1, 39, 1, 40, 1, 40, 1, 40, 1, 40, 5, 40, 455, 8, 40, 10, 40, 12, 40, 458, 9, 40, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 3, 41, 468, 8, 41, 1, 42, 1, 42, 1, 42, 1, 42, 3, 42, 474, 8, 42, 1, 43, 1, 43, 1, 43, 5, 43, 479, 8, 43, 10, 43, 12, 43, 482, 9, 43, 1, 44, 1, 44, 1, 44, 1, 44, 1, 45, 1, 45, 3, 45, 490, 8, 45, 1, 46, 1, 46, 1, 46, 1, 46, 1, 47, 1, 47, 1, 47, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49, 1, 49, 1, 49, 1, 50, 1, 50, 1, 50, 1, 51, 1, 51, 1, 51, 1, 51, 3, 51, 513, 8, 51, 1, 51, 1, 51, 1, 51, 1, 51, 5, 51, 519, 8, 51, 10, 51, 12, 51, 522, 9, 51, 3, 51, 524, 8, 51, 1, 52, 1, 52, 1, 53, 1, 53, 1, 53, 3, 53, 531, 8, 53, 1, 53, 1, 53, 1, 54, 1, 54, 1, 54, 1, 55, 1, 55, 1, 55, 1, 55, 3, 55, 542, 8, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 3, 55, 549, 8, 55, 1, 56, 1, 56, 1, 56, 1, 57, 4, 57, 555, 8, 57, 11, 57, 12, 57, 556, 1, 58, 1, 58, 1, 58, 1, 58, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 5, 59, 569, 8, 59, 10, 59, 12, 59, 572, 9, 59, 1, 60, 1, 60, 1, 61, 1, 61, 1, 61, 1, 61, 3, 61, 580, 8, 61, 1, 61, 1, 61, 1, 61, 1, 61, 1, 61, 1, 62, 1, 62, 1, 62, 1, 62, 3, 62, 591, 8, 62, 1, 62, 1, 62, 1, 62, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 64, 1, 64, 1, 64, 1, 64, 3, 64, 605, 8, 64, 1, 65, 1, 65, 1, 65, 1, 66, 1, 66, 3, 66, 612, 8, 66, 1, 66, 1, 66, 1, 67, 1, 67, 1, 67, 1, 67, 1, 68, 1, 68, 1, 68, 1, 68, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 3, 69, 631, 8, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 5, 69, 638, 8, 69, 10, 69, 12, 69, 641, 9, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 3, 69, 648, 8, 69, 1, 69, 1, 69, 1, 69, 3, 69, 653, 8, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 5, 69, 661, 8, 69, 10, 69, 12, 69, 664, 9, 69, 1, 70, 1, 70, 3, 70, 668, 8, 70, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 3, 70, 675, 8, 70, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 3, 70, 682, 8, 70, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 5, 70, 689, 8, 70, 10, 70, 12, 70, 692, 9, 70, 1, 70, 1, 70, 1, 70, 1, 70, 3, 70, 698, 8, 70, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 5, 70, 705, 8, 70, 10, 70, 12, 70, 708, 9, 70, 1, 70, 1, 70, 3, 70, 712, 8, 70, 1, 71, 1, 71, 1, 71, 3, 71, 717, 8, 71, 1, 71, 1, 71, 1, 71, 1, 72, 1, 72, 1, 72, 1, 72, 1, 72, 3, 72, 727, 8, 72, 1, 73, 1, 73, 1, 73, 1, 73, 3, 73, 733, 8, 73, 1, 73, 1, 73, 1, 73, 1, 73, 1, 73, 1, 73, 5, 73, 741, 8, 73, 10, 73, 12, 73, 744, 9, 73, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 3, 74, 754, 8, 74, 1, 74, 1, 74, 1, 74, 5, 74, 759, 8, 74, 10, 74, 12, 74, 762, 9, 74, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 5, 75, 770, 8, 75, 10, 75, 12, 75, 773, 9, 75, 1, 75, 1, 75, 3, 75, 777, 8, 75, 3, 75, 779, 8, 75, 1, 75, 1, 75, 1, 76, 1, 76, 1, 76, 3, 76, 786, 8, 76, 1, 77, 1, 77, 1, 77, 1, 77, 5, 77, 792, 8, 77, 10, 77, 12, 77, 795, 9, 77, 3, 77, 797, 8, 77, 1, 77, 1, 77, 1, 78, 1, 78, 1, 78, 1, 78, 1, 79, 1, 79, 3, 79, 807, 8, 79, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 5, 80, 822, 8, 80, 10, 80, 12, 80, 825, 9, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 5, 80, 833, 8, 80, 10, 80, 12, 80, 836, 9, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 5, 80, 844, 8, 80, 10, 80, 12, 80, 847, 9, 80, 1, 80, 1, 80, 3, 80, 851, 8, 80, 1, 81, 1, 81, 1, 82, 1, 82, 3, 82, 857, 8, 82, 1, 83, 3, 83, 860, 8, 83, 1, 83, 1, 83, 1, 84, 3, 84, 865, 8, 84, 1, 84, 1, 84, 1, 85, 1, 85, 1, 86, 1, 86, 1, 87, 1, 87, 1, 87, 1, 87, 1, 87, 1, 88, 1, 88, 1, 88, 3, 88, 881, 8, 88, 1, 88, 1, 88, 1, 88, 3, 88, 886, 8, 88, 1, 89, 1, 89, 1, 89, 1, 89, 5, 89, 892, 8, 89, 10, 89, 12, 89, 895, 9, 89, 1, 90, 1, 90, 1, 90, 0, 5, 4, 118, 138, 146, 148, 91, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164, 166, 168, 170, 172, 174, 176, 178, 180, 0, 10, 2, 0, 50, 50, 105, 105, 1, 0, 99, 100, 2, 0, 54, 54, 61, 61, 2, 0, 64, 64, 67, 67, 2, 0, 39, 39, 50, 50, 1, 0, 85, 86, 1, 0, 87, 89, 2, 0, 63, 63, 76, 76, 2, 0, 78, 78, 80, 84, 2, 0, 23, 23, 25, 26, 936, 0, 194, 1, 0, 0, 0, 2, 196, 1, 0, 0, 0, 4, 199, 1, 0, 0, 0, 6, 217, 1, 0, 0, 0, 8, 245, 1, 0, 0, 0, 10, 247, 1, 0, 0, 0, 12, 250, 1, 0, 0, 0, 14, 252, 1, 0, 0, 0, 16, 255, 1, 0, 0, 0, 18, 266, 1, 0, 0, 0, 20, 270, 1, 0, 0, 0, 22, 278, 1, 0, 0, 0, 24, 283, 1, 0, 0, 0, 26, 286, 1, 0, 0, 0, 28, 289, 1, 0, 0, 0, 30, 309, 1, 0, 0, 0, 32, 311, 1, 0, 0, 0, 34, 313, 1, 0, 0, 0, 36, 315, 1, 0, 0, 0, 38, 317, 1, 0, 0, 0, 40, 319, 1, 0, 0, 0, 42, 328, 1, 0, 0, 0, 44, 331, 1, 0, 0, 0, 46, 339, 1, 0, 0, 0, 48, 347, 1, 0, 0, 0, 50, 364, 1, 0, 0, 0, 52, 366, 1, 0, 0, 0, 54, 386, 1, 0, 0, 0, 56, 388, 1, 0, 0, 0, 58, 396, 1, 0, 0, 0, 60, 404, 1, 0, 0, 0, 62, 409, 1, 0, 0, 0, 64, 413, 1, 0, 0, 0, 66, 417, 1, 0, 0, 0, 68, 422, 1, 0, 0, 0, 70, 424, 1, 0, 0, 0, 72, 427, 1, 0, 0, 0, 74, 436, 1, 0, 0, 0, 76, 444, 1, 0, 0, 0, 78, 447, 1, 0, 0, 0, 80, 450, 1, 0, 0, 0, 82, 467, 1, 0, 0, 0, 84, 469, 1, 0, 0, 0, 86, 475, 1, 0, 0, 0, 88, 483, 1, 0, 0, 0, 90, 489, 1, 0, 0, 0, 92, 491, 1, 0, 0, 0, 94, 495, 1, 0, 0, 0, 96, 498, 1, 0, 0, 0, 98, 501, 1, 0, 0, 0, 100, 505, 1, 0, 0, 0, 102, 508, 1, 0, 0, 0, 104, 525, 1, 0, 0, 0, 106, 530, 1, 0, 0, 0, 108, 534, 1, 0, 0, 0, 110, 537, 1, 0, 0, 0, 112, 550, 1, 0, 0, 0, 114, 554, 1, 0, 0, 0, 116, 558, 1, 0, 0, 0, 118, 562, 1, 0, 0, 0, 120, 573, 1, 0, 0, 0, 122, 575, 1, 0, 0, 0, 124, 586, 1, 0, 0, 0, 126, 595, 1, 0, 0, 0, 128, 600, 1, 0, 0, 0, 130, 606, 1, 0, 0, 0, 132, 609, 1, 0, 0, 0, 134, 615, 1, 0, 0, 0, 136, 619, 1, 0, 0, 0, 138, 652, 1, 0, 0, 0, 140, 711, 1, 0, 0, 0, 142, 713, 1, 0, 0, 0, 144, 726, 1, 0, 0, 0, 146, 732, 1, 0, 0, 0, 148, 753, 1, 0, 0, 0, 150, 763, 1, 0, 0, 0, 152, 785, 1, 0, 0, 0, 154, 787, 1, 0, 0, 0, 156, 800, 1, 0, 0, 0, 158, 806, 1, 0, 0, 0, 160, 850, 1, 0, 0, 0, 162, 852, 1, 0, 0, 0, 164, 856, 1, 0, 0, 0, 166, 859, 1, 0, 0, 0, 168, 864, 1, 0, 0, 0, 170, 868, 1, 0, 0, 0, 172, 870, 1, 0, 0, 0, 174, 872, 1, 0, 0, 0, 176, 885, 1, 0, 0, 0, 178, 887, 1, 0, 0, 0, 180, 896, 1, 0, 0, 0, 182, 184, 4, 0, 0, 0, 183, 185, 3, 134, 67, 0, 184, 183, 1, 0, 0, 0, 185, 186, 1, 0, 0, 0, 186, 184, 1, 0, 0, 0, 186, 187, 1, 0, 0, 0, 187, 188, 1, 0, 0, 0, 188, 189, 3, 2, 1, 0, 189, 190, 5, 0, 0, 1, 190, 195, 1, 0, 0, 0, 191, 192, 3, 2, 1, 0, 192, 193, 5, 0, 0, 1, 193, 195, 1, 0, 0, 0, 194, 182, 1, 0, 0, 0, 194, 191, 1, 0, 0, 0, 195, 1, 1, 0, 0, 0, 196, 197, 3, 4, 2, 0, 197, 198, 5, 0, 0, 1, 198, 3, 1, 0, 0, 0, 199, 200, 6, 2, -1, 0, 200, 201, 3, 6, 3, 0, 201, 207, 1, 0, 0, 0, 202, 203, 10, 1, 0, 0, 203, 204, 5, 49, 0, 0, 204, 206, 3, 8, 4, 0, 205, 202, 1, 0, 0, 0, 206, 209, 1, 0, 0, 0, 207, 205, 1, 0, 0, 0, 207, 208, 1, 0, 0, 0, 208, 5, 1, 0, 0, 0, 209, 207, 1, 0, 0, 0, 210, 218, 3, 24, 12, 0, 211, 218, 3, 14, 7, 0, 212, 218, 3, 100, 50, 0, 213, 214, 4, 3, 2, 0, 214, 218, 3, 26, 13, 0, 215, 216, 4, 3, 3, 0, 216, 218, 3, 96, 48, 0, 217, 210, 1, 0, 0, 0, 217, 211, 1, 0, 0, 0, 217, 212, 1, 0, 0, 0, 217, 213, 1, 0, 0, 0, 217, 215, 1, 0, 0, 0, 218, 7, 1, 0, 0, 0, 219, 246, 3, 42, 21, 0, 220, 246, 3, 10, 5, 0, 221, 246, 3, 76, 38, 0, 222, 246, 3, 70, 35, 0, 223, 246, 3, 44, 22, 0, 224, 246, 3, 72, 36, 0, 225, 246, 3, 78, 39, 0, 226, 246, 3, 80, 40, 0, 227, 246, 3, 84, 42, 0, 228, 246, 3, 92, 46, 0, 229, 246, 3, 102, 51, 0, 230, 246, 3, 94, 47, 0, 231, 246, 3, 174, 87, 0, 232, 246, 3, 110, 55, 0, 233, 246, 3, 124, 62, 0, 234, 246, 3, 108, 54, 0, 235, 246, 3, 112, 56, 0, 236, 246, 3, 122, 61, 0, 237, 238, 4, 4, 4, 0, 238, 246, 3, 128, 64, 0, 239, 240, 4, 4, 5, 0, 240, 246, 3, 126, 63, 0, 241, 242, 4, 4, 6, 0, 242, 246, 3, 130, 65, 0, 243, 244, 4, 4, 7, 0, 244, 246, 3, 132, 66, 0, 245, 219, 1, 0, 0, 0, 245, 220, 1, 0, 0, 0, 245, 221, 1, 0, 0, 0, 245, 222, 1, 0, 0, 0, 245, 223, 1, 0, 0, 0, 245, 224, 1, 0, 0, 0, 245, 225, 1, 0, 0, 0, 245, 226, 1, 0, 0, 0, 245, 227, 1, 0, 0, 0, 245, 228, 1, 0, 0, 0, 245, 229, 1, 0, 0, 0, 245, 230, 1, 0, 0, 0, 245, 231, 1, 0, 0, 0, 245, 232, 1, 0, 0, 0, 245, 233, 1, 0, 0, 0, 245, 234, 1, 0, 0, 0, 245, 235, 1, 0, 0, 0, 245, 236, 1, 0, 0, 0, 245, 237, 1, 0, 0, 0, 245, 239, 1, 0, 0, 0, 245, 241, 1, 0, 0, 0, 245, 243, 1, 0, 0, 0, 246, 9, 1, 0, 0, 0, 247, 248, 5, 17, 0, 0, 248, 249, 3, 138, 69, 0, 249, 11, 1, 0, 0, 0, 250, 251, 3, 60, 30, 0, 251, 13, 1, 0, 0, 0, 252, 253, 5, 13, 0, 0, 253, 254, 3, 16, 8, 0, 254, 15, 1, 0, 0, 0, 255, 260, 3, 18, 9, 0, 256, 257, 5, 60, 0, 0, 257, 259, 3, 18, 9, 0, 258, 256, 1, 0, 0, 0, 259, 262, 1, 0, 0, 0, 260, 258, 1, 0, 0, 0, 260, 261, 1, 0, 0, 0, 261, 17, 1, 0, 0, 0, 262, 260, 1, 0, 0, 0, 263, 264, 3, 50, 25, 0, 264, 265, 5, 55, 0, 0, 265, 267, 1, 0, 0, 0, 266, 263, 1, 0, 0, 0, 266, 267, 1, 0, 0, 0, 267, 268, 1, 0, 0, 0, 268, 269, 3, 138, 69, 0, 269, 19, 1, 0, 0, 0, 270, 275, 3, 22, 11, 0, 271, 272, 5, 60, 0, 0, 272, 274, 3, 22, 11, 0, 273, 271, 1, 0, 0, 0, 274, 277, 1, 0, 0, 0, 275, 273, 1, 0, 0, 0, 275, 276, 1, 0, 0, 0, 276, 21, 1, 0, 0, 0, 277, 275, 1, 0, 0, 0, 278, 281, 3, 50, 25, 0, 279, 280, 5, 55, 0, 0, 280, 282, 3, 138, 69, 0, 281, 279, 1, 0, 0, 0, 281, 282, 1, 0, 0, 0, 282, 23, 1, 0, 0, 0, 283, 284, 5, 19, 0, 0, 284, 285, 3, 28, 14, 0, 285, 25, 1, 0, 0, 0, 286, 287, 5, 20, 0, 0, 287, 288, 3, 28, 14, 0, 288, 27, 1, 0, 0, 0, 289, 294, 3, 30, 15, 0, 290, 291, 5, 60, 0, 0, 291, 293, 3, 30, 15, 0, 292, 290, 1, 0, 0, 0, 293, 296, 1, 0, 0, 0, 294, 292, 1, 0, 0, 0, 294, 295, 1, 0, 0, 0, 295, 298, 1, 0, 0, 0, 296, 294, 1, 0, 0, 0, 297, 299, 3, 40, 20, 0, 298, 297, 1, 0, 0, 0, 298, 299, 1, 0, 0, 0, 299, 29, 1, 0, 0, 0, 300, 301, 3, 32, 16, 0, 301, 302, 5, 58, 0, 0, 302, 303, 3, 36, 18, 0, 303, 310, 1, 0, 0, 0, 304, 305, 3, 36, 18, 0, 305, 306, 5, 57, 0, 0, 306, 307, 3, 34, 17, 0, 307, 310, 1, 0, 0, 0, 308, 310, 3, 38, 19, 0, 309, 300, 1, 0, 0, 0, 309, 304, 1, 0, 0, 0, 309, 308, 1, 0, 0, 0, 310, 31, 1, 0, 0, 0, 311, 312, 5, 105, 0, 0, 312, 33, 1, 0, 0, 0, 313, 314, 5, 105, 0, 0, 314, 35, 1, 0, 0, 0, 315, 316, 5, 105, 0, 0, 316, 37, 1, 0, 0, 0, 317, 318, 7, 0, 0, 0, 318, 39, 1, 0, 0, 0, 319, 320, 5, 104, 0, 0, 320, 325, 5, 105, 0, 0, 321, 322, 5, 60, 0, 0, 322, 324, 5, 105, 0, 0, 323, 321, 1, 0, 0, 0, 324, 327, 1, 0, 0, 0, 325, 323, 1, 0, 0, 0, 325, 326, 1, 0, 0, 0, 326, 41, 1, 0, 0, 0, 327, 325, 1, 0, 0, 0, 328, 329, 5, 9, 0, 0, 329, 330, 3, 16, 8, 0, 330, 43, 1, 0, 0, 0, 331, 333, 5, 16, 0, 0, 332, 334, 3, 46, 23, 0, 333, 332, 1, 0, 0, 0, 333, 334, 1, 0, 0, 0, 334, 337, 1, 0, 0, 0, 335, 336, 5, 56, 0, 0, 336, 338, 3, 16, 8, 0, 337, 335, 1, 0, 0, 0, 337, 338, 1, 0, 0, 0, 338, 45, 1, 0, 0, 0, 339, 344, 3, 48, 24, 0, 340, 341, 5, 60, 0, 0, 341, 343, 3, 48, 24, 0, 342, 340, 1, 0, 0, 0, 343, 346, 1, 0, 0, 0, 344, 342, 1, 0, 0, 0, 344, 345, 1, 0, 0, 0, 345, 47, 1, 0, 0, 0, 346, 344, 1, 0, 0, 0, 347, 350, 3, 18, 9, 0, 348, 349, 5, 17, 0, 0, 349, 351, 3, 138, 69, 0, 350, 348, 1, 0, 0, 0, 350, 351, 1, 0, 0, 0, 351, 49, 1, 0, 0, 0, 352, 353, 4, 25, 8, 0, 353, 355, 5, 95, 0, 0, 354, 356, 5, 99, 0, 0, 355, 354, 1, 0, 0, 0, 355, 356, 1, 0, 0, 0, 356, 357, 1, 0, 0, 0, 357, 358, 5, 96, 0, 0, 358, 359, 5, 62, 0, 0, 359, 360, 5, 95, 0, 0, 360, 361, 3, 52, 26, 0, 361, 362, 5, 96, 0, 0, 362, 365, 1, 0, 0, 0, 363, 365, 3, 52, 26, 0, 364, 352, 1, 0, 0, 0, 364, 363, 1, 0, 0, 0, 365, 51, 1, 0, 0, 0, 366, 371, 3, 68, 34, 0, 367, 368, 5, 62, 0, 0, 368, 370, 3, 68, 34, 0, 369, 367, 1, 0, 0, 0, 370, 373, 1, 0, 0, 0, 371, 369, 1, 0, 0, 0, 371, 372, 1, 0, 0, 0, 372, 53, 1, 0, 0, 0, 373, 371, 1, 0, 0, 0, 374, 375, 4, 27, 9, 0, 375, 377, 5, 95, 0, 0, 376, 378, 5, 129, 0, 0, 377, 376, 1, 0, 0, 0, 377, 378, 1, 0, 0, 0, 378, 379, 1, 0, 0, 0, 379, 380, 5, 96, 0, 0, 380, 381, 5, 62, 0, 0, 381, 382, 5, 95, 0, 0, 382, 383, 3, 56, 28, 0, 383, 384, 5, 96, 0, 0, 384, 387, 1, 0, 0, 0, 385, 387, 3, 56, 28, 0, 386, 374, 1, 0, 0, 0, 386, 385, 1, 0, 0, 0, 387, 55, 1, 0, 0, 0, 388, 393, 3, 62, 31, 0, 389, 390, 5, 62, 0, 0, 390, 392, 3, 62, 31, 0, 391, 389, 1, 0, 0, 0, 392, 395, 1, 0, 0, 0, 393, 391, 1, 0, 0, 0, 393, 394, 1, 0, 0, 0, 394, 57, 1, 0, 0, 0, 395, 393, 1, 0, 0, 0, 396, 401, 3, 54, 27, 0, 397, 398, 5, 60, 0, 0, 398, 400, 3, 54, 27, 0, 399, 397, 1, 0, 0, 0, 400, 403, 1, 0, 0, 0, 401, 399, 1, 0, 0, 0, 401, 402, 1, 0, 0, 0, 402, 59, 1, 0, 0, 0, 403, 401, 1, 0, 0, 0, 404, 405, 7, 1, 0, 0, 405, 61, 1, 0, 0, 0, 406, 410, 5, 129, 0, 0, 407, 410, 3, 64, 32, 0, 408, 410, 3, 66, 33, 0, 409, 406, 1, 0, 0, 0, 409, 407, 1, 0, 0, 0, 409, 408, 1, 0, 0, 0, 410, 63, 1, 0, 0, 0, 411, 414, 5, 74, 0, 0, 412, 414, 5, 93, 0, 0, 413, 411, 1, 0, 0, 0, 413, 412, 1, 0, 0, 0, 414, 65, 1, 0, 0, 0, 415, 418, 5, 92, 0, 0, 416, 418, 5, 94, 0, 0, 417, 415, 1, 0, 0, 0, 417, 416, 1, 0, 0, 0, 418, 67, 1, 0, 0, 0, 419, 423, 3, 60, 30, 0, 420, 423, 3, 64, 32, 0, 421, 423, 3, 66, 33, 0, 422, 419, 1, 0, 0, 0, 422, 420, 1, 0, 0, 0, 422, 421, 1, 0, 0, 0, 423, 69, 1, 0, 0, 0, 424, 425, 5, 11, 0, 0, 425, 426, 3, 160, 80, 0, 426, 71, 1, 0, 0, 0, 427, 428, 5, 15, 0, 0, 428, 433, 3, 74, 37, 0, 429, 430, 5, 60, 0, 0, 430, 432, 3, 74, 37, 0, 431, 429, 1, 0, 0, 0, 432, 435, 1, 0, 0, 0, 433, 431, 1, 0, 0, 0, 433, 434, 1, 0, 0, 0, 434, 73, 1, 0, 0, 0, 435, 433, 1, 0, 0, 0, 436, 438, 3, 138, 69, 0, 437, 439, 7, 2, 0, 0, 438, 437, 1, 0, 0, 0, 438, 439, 1, 0, 0, 0, 439, 442, 1, 0, 0, 0, 440, 441, 5, 71, 0, 0, 441, 443, 7, 3, 0, 0, 442, 440, 1, 0, 0, 0, 442, 443, 1, 0, 0, 0, 443, 75, 1, 0, 0, 0, 444, 445, 5, 30, 0, 0, 445, 446, 3, 58, 29, 0, 446, 77, 1, 0, 0, 0, 447, 448, 5, 29, 0, 0, 448, 449, 3, 58, 29, 0, 449, 79, 1, 0, 0, 0, 450, 451, 5, 32, 0, 0, 451, 456, 3, 82, 41, 0, 452, 453, 5, 60, 0, 0, 453, 455, 3, 82, 41, 0, 454, 452, 1, 0, 0, 0, 455, 458, 1, 0, 0, 0, 456, 454, 1, 0, 0, 0, 456, 457, 1, 0, 0, 0, 457, 81, 1, 0, 0, 0, 458, 456, 1, 0, 0, 0, 459, 460, 3, 54, 27, 0, 460, 461, 5, 133, 0, 0, 461, 462, 3, 54, 27, 0, 462, 468, 1, 0, 0, 0, 463, 464, 3, 54, 27, 0, 464, 465, 5, 55, 0, 0, 465, 466, 3, 54, 27, 0, 466, 468, 1, 0, 0, 0, 467, 459, 1, 0, 0, 0, 467, 463, 1, 0, 0, 0, 468, 83, 1, 0, 0, 0, 469, 470, 5, 8, 0, 0, 470, 471, 3, 148, 74, 0, 471, 473, 3, 170, 85, 0, 472, 474, 3, 86, 43, 0, 473, 472, 1, 0, 0, 0, 473, 474, 1, 0, 0, 0, 474, 85, 1, 0, 0, 0, 475, 480, 3, 88, 44, 0, 476, 477, 5, 60, 0, 0, 477, 479, 3, 88, 44, 0, 478, 476, 1, 0, 0, 0, 479, 482, 1, 0, 0, 0, 480, 478, 1, 0, 0, 0, 480, 481, 1, 0, 0, 0, 481, 87, 1, 0, 0, 0, 482, 480, 1, 0, 0, 0, 483, 484, 3, 60, 30, 0, 484, 485, 5, 55, 0, 0, 485, 486, 3, 160, 80, 0, 486, 89, 1, 0, 0, 0, 487, 488, 5, 77, 0, 0, 488, 490, 3, 154, 77, 0, 489, 487, 1, 0, 0, 0, 489, 490, 1, 0, 0, 0, 490, 91, 1, 0, 0, 0, 491, 492, 5, 10, 0, 0, 492, 493, 3, 148, 74, 0, 493, 494, 3, 170, 85, 0, 494, 93, 1, 0, 0, 0, 495, 496, 5, 28, 0, 0, 496, 497, 3, 50, 25, 0, 497, 95, 1, 0, 0, 0, 498, 499, 5, 6, 0, 0, 499, 500, 3, 98, 49, 0, 500, 97, 1, 0, 0, 0, 501, 502, 5, 97, 0, 0, 502, 503, 3, 4, 2, 0, 503, 504, 5, 98, 0, 0, 504, 99, 1, 0, 0, 0, 505, 506, 5, 34, 0, 0, 506, 507, 5, 140, 0, 0, 507, 101, 1, 0, 0, 0, 508, 509, 5, 5, 0, 0, 509, 512, 3, 104, 52, 0, 510, 511, 5, 72, 0, 0, 511, 513, 3, 54, 27, 0, 512, 510, 1, 0, 0, 0, 512, 513, 1, 0, 0, 0, 513, 523, 1, 0, 0, 0, 514, 515, 5, 77, 0, 0, 515, 520, 3, 106, 53, 0, 516, 517, 5, 60, 0, 0, 517, 519, 3, 106, 53, 0, 518, 516, 1, 0, 0, 0, 519, 522, 1, 0, 0, 0, 520, 518, 1, 0, 0, 0, 520, 521, 1, 0, 0, 0, 521, 524, 1, 0, 0, 0, 522, 520, 1, 0, 0, 0, 523, 514, 1, 0, 0, 0, 523, 524, 1, 0, 0, 0, 524, 103, 1, 0, 0, 0, 525, 526, 7, 4, 0, 0, 526, 105, 1, 0, 0, 0, 527, 528, 3, 54, 27, 0, 528, 529, 5, 55, 0, 0, 529, 531, 1, 0, 0, 0, 530, 527, 1, 0, 0, 0, 530, 531, 1, 0, 0, 0, 531, 532, 1, 0, 0, 0, 532, 533, 3, 54, 27, 0, 533, 107, 1, 0, 0, 0, 534, 535, 5, 14, 0, 0, 535, 536, 3, 160, 80, 0, 536, 109, 1, 0, 0, 0, 537, 538, 5, 4, 0, 0, 538, 541, 3, 50, 25, 0, 539, 540, 5, 72, 0, 0, 540, 542, 3, 50, 25, 0, 541, 539, 1, 0, 0, 0, 541, 542, 1, 0, 0, 0, 542, 548, 1, 0, 0, 0, 543, 544, 5, 133, 0, 0, 544, 545, 3, 50, 25, 0, 545, 546, 5, 60, 0, 0, 546, 547, 3, 50, 25, 0, 547, 549, 1, 0, 0, 0, 548, 543, 1, 0, 0, 0, 548, 549, 1, 0, 0, 0, 549, 111, 1, 0, 0, 0, 550, 551, 5, 21, 0, 0, 551, 552, 3, 114, 57, 0, 552, 113, 1, 0, 0, 0, 553, 555, 3, 116, 58, 0, 554, 553, 1, 0, 0, 0, 555, 556, 1, 0, 0, 0, 556, 554, 1, 0, 0, 0, 556, 557, 1, 0, 0, 0, 557, 115, 1, 0, 0, 0, 558, 559, 5, 97, 0, 0, 559, 560, 3, 118, 59, 0, 560, 561, 5, 98, 0, 0, 561, 117, 1, 0, 0, 0, 562, 563, 6, 59, -1, 0, 563, 564, 3, 120, 60, 0, 564, 570, 1, 0, 0, 0, 565, 566, 10, 1, 0, 0, 566, 567, 5, 49, 0, 0, 567, 569, 3, 120, 60, 0, 568, 565, 1, 0, 0, 0, 569, 572, 1, 0, 0, 0, 570, 568, 1, 0, 0, 0, 570, 571, 1, 0, 0, 0, 571, 119, 1, 0, 0, 0, 572, 570, 1, 0, 0, 0, 573, 574, 3, 8, 4, 0, 574, 121, 1, 0, 0, 0, 575, 579, 5, 12, 0, 0, 576, 577, 3, 50, 25, 0, 577, 578, 5, 55, 0, 0, 578, 580, 1, 0, 0, 0, 579, 576, 1, 0, 0, 0, 579, 580, 1, 0, 0, 0, 580, 581, 1, 0, 0, 0, 581, 582, 3, 160, 80, 0, 582, 583, 5, 72, 0, 0, 583, 584, 3, 20, 10, 0, 584, 585, 3, 90, 45, 0, 585, 123, 1, 0, 0, 0, 586, 590, 5, 7, 0, 0, 587, 588, 3, 50, 25, 0, 588, 589, 5, 55, 0, 0, 589, 591, 1, 0, 0, 0, 590, 587, 1, 0, 0, 0, 590, 591, 1, 0, 0, 0, 591, 592, 1, 0, 0, 0, 592, 593, 3, 148, 74, 0, 593, 594, 3, 90, 45, 0, 594, 125, 1, 0, 0, 0, 595, 596, 5, 27, 0, 0, 596, 597, 3, 30, 15, 0, 597, 598, 5, 72, 0, 0, 598, 599, 3, 58, 29, 0, 599, 127, 1, 0, 0, 0, 600, 601, 5, 18, 0, 0, 601, 604, 3, 46, 23, 0, 602, 603, 5, 56, 0, 0, 603, 605, 3, 16, 8, 0, 604, 602, 1, 0, 0, 0, 604, 605, 1, 0, 0, 0, 605, 129, 1, 0, 0, 0, 606, 607, 5, 31, 0, 0, 607, 608, 3, 58, 29, 0, 608, 131, 1, 0, 0, 0, 609, 611, 5, 22, 0, 0, 610, 612, 3, 60, 30, 0, 611, 610, 1, 0, 0, 0, 611, 612, 1, 0, 0, 0, 612, 613, 1, 0, 0, 0, 613, 614, 3, 90, 45, 0, 614, 133, 1, 0, 0, 0, 615, 616, 5, 33, 0, 0, 616, 617, 3, 136, 68, 0, 617, 618, 5, 59, 0, 0, 618, 135, 1, 0, 0, 0, 619, 620, 3, 60, 30, 0, 620, 621, 5, 55, 0, 0, 621, 622, 3, 160, 80, 0, 622, 137, 1, 0, 0, 0, 623, 624, 6, 69, -1, 0, 624, 625, 5, 69, 0, 0, 625, 653, 3, 138, 69, 8, 626, 653, 3, 144, 72, 0, 627, 653, 3, 140, 70, 0, 628, 630, 3, 144, 72, 0, 629, 631, 5, 69, 0, 0, 630, 629, 1, 0, 0, 0, 630, 631, 1, 0, 0, 0, 631, 632, 1, 0, 0, 0, 632, 633, 5, 65, 0, 0, 633, 634, 5, 97, 0, 0, 634, 639, 3, 144, 72, 0, 635, 636, 5, 60, 0, 0, 636, 638, 3, 144, 72, 0, 637, 635, 1, 0, 0, 0, 638, 641, 1, 0, 0, 0, 639, 637, 1, 0, 0, 0, 639, 640, 1, 0, 0, 0, 640, 642, 1, 0, 0, 0, 641, 639, 1, 0, 0, 0, 642, 643, 5, 98, 0, 0, 643, 653, 1, 0, 0, 0, 644, 645, 3, 144, 72, 0, 645, 647, 5, 66, 0, 0, 646, 648, 5, 69, 0, 0, 647, 646, 1, 0, 0, 0, 647, 648, 1, 0, 0, 0, 648, 649, 1, 0, 0, 0, 649, 650, 5, 70, 0, 0, 650, 653, 1, 0, 0, 0, 651, 653, 3, 142, 71, 0, 652, 623, 1, 0, 0, 0, 652, 626, 1, 0, 0, 0, 652, 627, 1, 0, 0, 0, 652, 628, 1, 0, 0, 0, 652, 644, 1, 0, 0, 0, 652, 651, 1, 0, 0, 0, 653, 662, 1, 0, 0, 0, 654, 655, 10, 5, 0, 0, 655, 656, 5, 53, 0, 0, 656, 661, 3, 138, 69, 6, 657, 658, 10, 4, 0, 0, 658, 659, 5, 73, 0, 0, 659, 661, 3, 138, 69, 5, 660, 654, 1, 0, 0, 0, 660, 657, 1, 0, 0, 0, 661, 664, 1, 0, 0, 0, 662, 660, 1, 0, 0, 0, 662, 663, 1, 0, 0, 0, 663, 139, 1, 0, 0, 0, 664, 662, 1, 0, 0, 0, 665, 667, 3, 144, 72, 0, 666, 668, 5, 69, 0, 0, 667, 666, 1, 0, 0, 0, 667, 668, 1, 0, 0, 0, 668, 669, 1, 0, 0, 0, 669, 670, 5, 68, 0, 0, 670, 671, 3, 170, 85, 0, 671, 712, 1, 0, 0, 0, 672, 674, 3, 144, 72, 0, 673, 675, 5, 69, 0, 0, 674, 673, 1, 0, 0, 0, 674, 675, 1, 0, 0, 0, 675, 676, 1, 0, 0, 0, 676, 677, 5, 75, 0, 0, 677, 678, 3, 170, 85, 0, 678, 712, 1, 0, 0, 0, 679, 681, 3, 144, 72, 0, 680, 682, 5, 69, 0, 0, 681, 680, 1, 0, 0, 0, 681, 682, 1, 0, 0, 0, 682, 683, 1, 0, 0, 0, 683, 684, 5, 68, 0, 0, 684, 685, 5, 97, 0, 0, 685, 690, 3, 170, 85, 0, 686, 687, 5, 60, 0, 0, 687, 689, 3, 170, 85, 0, 688, 686, 1, 0, 0, 0, 689, 692, 1, 0, 0, 0, 690, 688, 1, 0, 0, 0, 690, 691, 1, 0, 0, 0, 691, 693, 1, 0, 0, 0, 692, 690, 1, 0, 0, 0, 693, 694, 5, 98, 0, 0, 694, 712, 1, 0, 0, 0, 695, 697, 3, 144, 72, 0, 696, 698, 5, 69, 0, 0, 697, 696, 1, 0, 0, 0, 697, 698, 1, 0, 0, 0, 698, 699, 1, 0, 0, 0, 699, 700, 5, 75, 0, 0, 700, 701, 5, 97, 0, 0, 701, 706, 3, 170, 85, 0, 702, 703, 5, 60, 0, 0, 703, 705, 3, 170, 85, 0, 704, 702, 1, 0, 0, 0, 705, 708, 1, 0, 0, 0, 706, 704, 1, 0, 0, 0, 706, 707, 1, 0, 0, 0, 707, 709, 1, 0, 0, 0, 708, 706, 1, 0, 0, 0, 709, 710, 5, 98, 0, 0, 710, 712, 1, 0, 0, 0, 711, 665, 1, 0, 0, 0, 711, 672, 1, 0, 0, 0, 711, 679, 1, 0, 0, 0, 711, 695, 1, 0, 0, 0, 712, 141, 1, 0, 0, 0, 713, 716, 3, 50, 25, 0, 714, 715, 5, 57, 0, 0, 715, 717, 3, 12, 6, 0, 716, 714, 1, 0, 0, 0, 716, 717, 1, 0, 0, 0, 717, 718, 1, 0, 0, 0, 718, 719, 5, 58, 0, 0, 719, 720, 3, 160, 80, 0, 720, 143, 1, 0, 0, 0, 721, 727, 3, 146, 73, 0, 722, 723, 3, 146, 73, 0, 723, 724, 3, 172, 86, 0, 724, 725, 3, 146, 73, 0, 725, 727, 1, 0, 0, 0, 726, 721, 1, 0, 0, 0, 726, 722, 1, 0, 0, 0, 727, 145, 1, 0, 0, 0, 728, 729, 6, 73, -1, 0, 729, 733, 3, 148, 74, 0, 730, 731, 7, 5, 0, 0, 731, 733, 3, 146, 73, 3, 732, 728, 1, 0, 0, 0, 732, 730, 1, 0, 0, 0, 733, 742, 1, 0, 0, 0, 734, 735, 10, 2, 0, 0, 735, 736, 7, 6, 0, 0, 736, 741, 3, 146, 73, 3, 737, 738, 10, 1, 0, 0, 738, 739, 7, 5, 0, 0, 739, 741, 3, 146, 73, 2, 740, 734, 1, 0, 0, 0, 740, 737, 1, 0, 0, 0, 741, 744, 1, 0, 0, 0, 742, 740, 1, 0, 0, 0, 742, 743, 1, 0, 0, 0, 743, 147, 1, 0, 0, 0, 744, 742, 1, 0, 0, 0, 745, 746, 6, 74, -1, 0, 746, 754, 3, 160, 80, 0, 747, 754, 3, 50, 25, 0, 748, 754, 3, 150, 75, 0, 749, 750, 5, 97, 0, 0, 750, 751, 3, 138, 69, 0, 751, 752, 5, 98, 0, 0, 752, 754, 1, 0, 0, 0, 753, 745, 1, 0, 0, 0, 753, 747, 1, 0, 0, 0, 753, 748, 1, 0, 0, 0, 753, 749, 1, 0, 0, 0, 754, 760, 1, 0, 0, 0, 755, 756, 10, 1, 0, 0, 756, 757, 5, 57, 0, 0, 757, 759, 3, 12, 6, 0, 758, 755, 1, 0, 0, 0, 759, 762, 1, 0, 0, 0, 760, 758, 1, 0, 0, 0, 760, 761, 1, 0, 0, 0, 761, 149, 1, 0, 0, 0, 762, 760, 1, 0, 0, 0, 763, 764, 3, 152, 76, 0, 764, 778, 5, 97, 0, 0, 765, 779, 5, 87, 0, 0, 766, 771, 3, 138, 69, 0, 767, 768, 5, 60, 0, 0, 768, 770, 3, 138, 69, 0, 769, 767, 1, 0, 0, 0, 770, 773, 1, 0, 0, 0, 771, 769, 1, 0, 0, 0, 771, 772, 1, 0, 0, 0, 772, 776, 1, 0, 0, 0, 773, 771, 1, 0, 0, 0, 774, 775, 5, 60, 0, 0, 775, 777, 3, 154, 77, 0, 776, 774, 1, 0, 0, 0, 776, 777, 1, 0, 0, 0, 777, 779, 1, 0, 0, 0, 778, 765, 1, 0, 0, 0, 778, 766, 1, 0, 0, 0, 778, 779, 1, 0, 0, 0, 779, 780, 1, 0, 0, 0, 780, 781, 5, 98, 0, 0, 781, 151, 1, 0, 0, 0, 782, 786, 3, 68, 34, 0, 783, 786, 5, 64, 0, 0, 784, 786, 5, 67, 0, 0, 785, 782, 1, 0, 0, 0, 785, 783, 1, 0, 0, 0, 785, 784, 1, 0, 0, 0, 786, 153, 1, 0, 0, 0, 787, 796, 5, 90, 0, 0, 788, 793, 3, 156, 78, 0, 789, 790, 5, 60, 0, 0, 790, 792, 3, 156, 78, 0, 791, 789, 1, 0, 0, 0, 792, 795, 1, 0, 0, 0, 793, 791, 1, 0, 0, 0, 793, 794, 1, 0, 0, 0, 794, 797, 1, 0, 0, 0, 795, 793, 1, 0, 0, 0, 796, 788, 1, 0, 0, 0, 796, 797, 1, 0, 0, 0, 797, 798, 1, 0, 0, 0, 798, 799, 5, 91, 0, 0, 799, 155, 1, 0, 0, 0, 800, 801, 3, 170, 85, 0, 801, 802, 5, 58, 0, 0, 802, 803, 3, 158, 79, 0, 803, 157, 1, 0, 0, 0, 804, 807, 3, 160, 80, 0, 805, 807, 3, 154, 77, 0, 806, 804, 1, 0, 0, 0, 806, 805, 1, 0, 0, 0, 807, 159, 1, 0, 0, 0, 808, 851, 5, 70, 0, 0, 809, 810, 3, 168, 84, 0, 810, 811, 5, 99, 0, 0, 811, 851, 1, 0, 0, 0, 812, 851, 3, 166, 83, 0, 813, 851, 3, 168, 84, 0, 814, 851, 3, 162, 81, 0, 815, 851, 3, 64, 32, 0, 816, 851, 3, 170, 85, 0, 817, 818, 5, 95, 0, 0, 818, 823, 3, 164, 82, 0, 819, 820, 5, 60, 0, 0, 820, 822, 3, 164, 82, 0, 821, 819, 1, 0, 0, 0, 822, 825, 1, 0, 0, 0, 823, 821, 1, 0, 0, 0, 823, 824, 1, 0, 0, 0, 824, 826, 1, 0, 0, 0, 825, 823, 1, 0, 0, 0, 826, 827, 5, 96, 0, 0, 827, 851, 1, 0, 0, 0, 828, 829, 5, 95, 0, 0, 829, 834, 3, 162, 81, 0, 830, 831, 5, 60, 0, 0, 831, 833, 3, 162, 81, 0, 832, 830, 1, 0, 0, 0, 833, 836, 1, 0, 0, 0, 834, 832, 1, 0, 0, 0, 834, 835, 1, 0, 0, 0, 835, 837, 1, 0, 0, 0, 836, 834, 1, 0, 0, 0, 837, 838, 5, 96, 0, 0, 838, 851, 1, 0, 0, 0, 839, 840, 5, 95, 0, 0, 840, 845, 3, 170, 85, 0, 841, 842, 5, 60, 0, 0, 842, 844, 3, 170, 85, 0, 843, 841, 1, 0, 0, 0, 844, 847, 1, 0, 0, 0, 845, 843, 1, 0, 0, 0, 845, 846, 1, 0, 0, 0, 846, 848, 1, 0, 0, 0, 847, 845, 1, 0, 0, 0, 848, 849, 5, 96, 0, 0, 849, 851, 1, 0, 0, 0, 850, 808, 1, 0, 0, 0, 850, 809, 1, 0, 0, 0, 850, 812, 1, 0, 0, 0, 850, 813, 1, 0, 0, 0, 850, 814, 1, 0, 0, 0, 850, 815, 1, 0, 0, 0, 850, 816, 1, 0, 0, 0, 850, 817, 1, 0, 0, 0, 850, 828, 1, 0, 0, 0, 850, 839, 1, 0, 0, 0, 851, 161, 1, 0, 0, 0, 852, 853, 7, 7, 0, 0, 853, 163, 1, 0, 0, 0, 854, 857, 3, 166, 83, 0, 855, 857, 3, 168, 84, 0, 856, 854, 1, 0, 0, 0, 856, 855, 1, 0, 0, 0, 857, 165, 1, 0, 0, 0, 858, 860, 7, 5, 0, 0, 859, 858, 1, 0, 0, 0, 859, 860, 1, 0, 0, 0, 860, 861, 1, 0, 0, 0, 861, 862, 5, 52, 0, 0, 862, 167, 1, 0, 0, 0, 863, 865, 7, 5, 0, 0, 864, 863, 1, 0, 0, 0, 864, 865, 1, 0, 0, 0, 865, 866, 1, 0, 0, 0, 866, 867, 5, 51, 0, 0, 867, 169, 1, 0, 0, 0, 868, 869, 5, 50, 0, 0, 869, 171, 1, 0, 0, 0, 870, 871, 7, 8, 0, 0, 871, 173, 1, 0, 0, 0, 872, 873, 7, 9, 0, 0, 873, 874, 5, 115, 0, 0, 874, 875, 3, 176, 88, 0, 875, 876, 3, 178, 89, 0, 876, 175, 1, 0, 0, 0, 877, 878, 4, 88, 16, 0, 878, 880, 3, 30, 15, 0, 879, 881, 5, 133, 0, 0, 880, 879, 1, 0, 0, 0, 880, 881, 1, 0, 0, 0, 881, 882, 1, 0, 0, 0, 882, 883, 5, 105, 0, 0, 883, 886, 1, 0, 0, 0, 884, 886, 3, 30, 15, 0, 885, 877, 1, 0, 0, 0, 885, 884, 1, 0, 0, 0, 886, 177, 1, 0, 0, 0, 887, 888, 5, 72, 0, 0, 888, 893, 3, 180, 90, 0, 889, 890, 5, 60, 0, 0, 890, 892, 3, 180, 90, 0, 891, 889, 1, 0, 0, 0, 892, 895, 1, 0, 0, 0, 893, 891, 1, 0, 0, 0, 893, 894, 1, 0, 0, 0, 894, 179, 1, 0, 0, 0, 895, 893, 1, 0, 0, 0, 896, 897, 3, 144, 72, 0, 897, 181, 1, 0, 0, 0, 85, 186, 194, 207, 217, 245, 260, 266, 275, 281, 294, 298, 309, 325, 333, 337, 344, 350, 355, 364, 371, 377, 386, 393, 401, 409, 413, 417, 422, 433, 438, 442, 456, 467, 473, 480, 489, 512, 520, 523, 530, 541, 548, 556, 570, 579, 590, 604, 611, 630, 639, 647, 652, 660, 662, 667, 674, 681, 690, 697, 706, 711, 716, 726, 732, 740, 742, 753, 760, 771, 776, 778, 785, 793, 796, 806, 823, 834, 845, 850, 856, 859, 864, 880, 885, 893] \ No newline at end of file +[4, 1, 143, 895, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 2, 66, 7, 66, 2, 67, 7, 67, 2, 68, 7, 68, 2, 69, 7, 69, 2, 70, 7, 70, 2, 71, 7, 71, 2, 72, 7, 72, 2, 73, 7, 73, 2, 74, 7, 74, 2, 75, 7, 75, 2, 76, 7, 76, 2, 77, 7, 77, 2, 78, 7, 78, 2, 79, 7, 79, 2, 80, 7, 80, 2, 81, 7, 81, 2, 82, 7, 82, 2, 83, 7, 83, 2, 84, 7, 84, 2, 85, 7, 85, 2, 86, 7, 86, 2, 87, 7, 87, 2, 88, 7, 88, 2, 89, 7, 89, 1, 0, 1, 0, 4, 0, 183, 8, 0, 11, 0, 12, 0, 184, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 3, 0, 193, 8, 0, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 5, 2, 204, 8, 2, 10, 2, 12, 2, 207, 9, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 3, 216, 8, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 3, 4, 244, 8, 4, 1, 5, 1, 5, 1, 5, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 5, 8, 257, 8, 8, 10, 8, 12, 8, 260, 9, 8, 1, 9, 1, 9, 1, 9, 3, 9, 265, 8, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 5, 10, 272, 8, 10, 10, 10, 12, 10, 275, 9, 10, 1, 11, 1, 11, 1, 11, 3, 11, 280, 8, 11, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 5, 14, 291, 8, 14, 10, 14, 12, 14, 294, 9, 14, 1, 14, 3, 14, 297, 8, 14, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 3, 15, 308, 8, 15, 1, 16, 1, 16, 1, 17, 1, 17, 1, 18, 1, 18, 1, 19, 1, 19, 1, 20, 1, 20, 1, 20, 1, 20, 5, 20, 322, 8, 20, 10, 20, 12, 20, 325, 9, 20, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 3, 22, 332, 8, 22, 1, 22, 1, 22, 3, 22, 336, 8, 22, 1, 23, 1, 23, 1, 23, 5, 23, 341, 8, 23, 10, 23, 12, 23, 344, 9, 23, 1, 24, 1, 24, 1, 24, 3, 24, 349, 8, 24, 1, 25, 1, 25, 1, 25, 3, 25, 354, 8, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 3, 25, 363, 8, 25, 1, 26, 1, 26, 1, 26, 5, 26, 368, 8, 26, 10, 26, 12, 26, 371, 9, 26, 1, 27, 1, 27, 1, 27, 3, 27, 376, 8, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 3, 27, 385, 8, 27, 1, 28, 1, 28, 1, 28, 5, 28, 390, 8, 28, 10, 28, 12, 28, 393, 9, 28, 1, 29, 1, 29, 1, 29, 5, 29, 398, 8, 29, 10, 29, 12, 29, 401, 9, 29, 1, 30, 1, 30, 1, 31, 1, 31, 1, 31, 3, 31, 408, 8, 31, 1, 32, 1, 32, 3, 32, 412, 8, 32, 1, 33, 1, 33, 3, 33, 416, 8, 33, 1, 34, 1, 34, 1, 34, 3, 34, 421, 8, 34, 1, 35, 1, 35, 1, 35, 1, 36, 1, 36, 1, 36, 1, 36, 5, 36, 430, 8, 36, 10, 36, 12, 36, 433, 9, 36, 1, 37, 1, 37, 3, 37, 437, 8, 37, 1, 37, 1, 37, 3, 37, 441, 8, 37, 1, 38, 1, 38, 1, 38, 1, 39, 1, 39, 1, 39, 1, 40, 1, 40, 1, 40, 1, 40, 5, 40, 453, 8, 40, 10, 40, 12, 40, 456, 9, 40, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 3, 41, 466, 8, 41, 1, 42, 1, 42, 1, 42, 1, 42, 3, 42, 472, 8, 42, 1, 43, 1, 43, 1, 43, 5, 43, 477, 8, 43, 10, 43, 12, 43, 480, 9, 43, 1, 44, 1, 44, 1, 44, 1, 44, 1, 45, 1, 45, 3, 45, 488, 8, 45, 1, 46, 1, 46, 1, 46, 1, 46, 1, 47, 1, 47, 1, 47, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49, 1, 49, 1, 49, 1, 50, 1, 50, 1, 50, 1, 51, 1, 51, 1, 51, 1, 51, 3, 51, 511, 8, 51, 1, 51, 1, 51, 1, 51, 1, 51, 5, 51, 517, 8, 51, 10, 51, 12, 51, 520, 9, 51, 3, 51, 522, 8, 51, 1, 52, 1, 52, 1, 53, 1, 53, 1, 53, 3, 53, 529, 8, 53, 1, 53, 1, 53, 1, 54, 1, 54, 1, 54, 1, 55, 1, 55, 1, 55, 1, 55, 3, 55, 540, 8, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 3, 55, 547, 8, 55, 1, 56, 1, 56, 1, 56, 1, 57, 4, 57, 553, 8, 57, 11, 57, 12, 57, 554, 1, 58, 1, 58, 1, 58, 1, 58, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 5, 59, 567, 8, 59, 10, 59, 12, 59, 570, 9, 59, 1, 60, 1, 60, 1, 61, 1, 61, 1, 61, 1, 61, 3, 61, 578, 8, 61, 1, 61, 1, 61, 1, 61, 1, 61, 1, 61, 1, 62, 1, 62, 1, 62, 1, 62, 3, 62, 589, 8, 62, 1, 62, 1, 62, 1, 62, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 64, 1, 64, 1, 64, 1, 64, 3, 64, 603, 8, 64, 1, 65, 1, 65, 1, 65, 1, 66, 1, 66, 3, 66, 610, 8, 66, 1, 66, 1, 66, 1, 67, 1, 67, 1, 67, 1, 67, 1, 68, 1, 68, 1, 68, 1, 68, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 3, 69, 629, 8, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 5, 69, 636, 8, 69, 10, 69, 12, 69, 639, 9, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 3, 69, 646, 8, 69, 1, 69, 1, 69, 1, 69, 3, 69, 651, 8, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 5, 69, 659, 8, 69, 10, 69, 12, 69, 662, 9, 69, 1, 70, 1, 70, 3, 70, 666, 8, 70, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 3, 70, 673, 8, 70, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 3, 70, 680, 8, 70, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 5, 70, 687, 8, 70, 10, 70, 12, 70, 690, 9, 70, 1, 70, 1, 70, 1, 70, 1, 70, 3, 70, 696, 8, 70, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 5, 70, 703, 8, 70, 10, 70, 12, 70, 706, 9, 70, 1, 70, 1, 70, 3, 70, 710, 8, 70, 1, 71, 1, 71, 1, 71, 3, 71, 715, 8, 71, 1, 71, 1, 71, 1, 71, 1, 72, 1, 72, 1, 72, 1, 72, 1, 72, 3, 72, 725, 8, 72, 1, 73, 1, 73, 1, 73, 1, 73, 3, 73, 731, 8, 73, 1, 73, 1, 73, 1, 73, 1, 73, 1, 73, 1, 73, 5, 73, 739, 8, 73, 10, 73, 12, 73, 742, 9, 73, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 3, 74, 752, 8, 74, 1, 74, 1, 74, 1, 74, 5, 74, 757, 8, 74, 10, 74, 12, 74, 760, 9, 74, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 5, 75, 768, 8, 75, 10, 75, 12, 75, 771, 9, 75, 1, 75, 1, 75, 3, 75, 775, 8, 75, 3, 75, 777, 8, 75, 1, 75, 1, 75, 1, 76, 1, 76, 1, 76, 3, 76, 784, 8, 76, 1, 77, 1, 77, 1, 77, 1, 77, 5, 77, 790, 8, 77, 10, 77, 12, 77, 793, 9, 77, 3, 77, 795, 8, 77, 1, 77, 1, 77, 1, 78, 1, 78, 1, 78, 1, 78, 1, 79, 1, 79, 3, 79, 805, 8, 79, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 5, 80, 820, 8, 80, 10, 80, 12, 80, 823, 9, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 5, 80, 831, 8, 80, 10, 80, 12, 80, 834, 9, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 5, 80, 842, 8, 80, 10, 80, 12, 80, 845, 9, 80, 1, 80, 1, 80, 3, 80, 849, 8, 80, 1, 81, 1, 81, 1, 82, 1, 82, 3, 82, 855, 8, 82, 1, 83, 3, 83, 858, 8, 83, 1, 83, 1, 83, 1, 84, 3, 84, 863, 8, 84, 1, 84, 1, 84, 1, 85, 1, 85, 1, 86, 1, 86, 1, 87, 1, 87, 1, 87, 1, 87, 1, 87, 1, 88, 1, 88, 1, 88, 3, 88, 879, 8, 88, 1, 88, 1, 88, 1, 88, 3, 88, 884, 8, 88, 1, 89, 1, 89, 1, 89, 1, 89, 5, 89, 890, 8, 89, 10, 89, 12, 89, 893, 9, 89, 1, 89, 0, 5, 4, 118, 138, 146, 148, 90, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164, 166, 168, 170, 172, 174, 176, 178, 0, 10, 2, 0, 50, 50, 105, 105, 1, 0, 99, 100, 2, 0, 54, 54, 61, 61, 2, 0, 64, 64, 67, 67, 2, 0, 39, 39, 50, 50, 1, 0, 85, 86, 1, 0, 87, 89, 2, 0, 63, 63, 76, 76, 2, 0, 78, 78, 80, 84, 2, 0, 23, 23, 25, 26, 933, 0, 192, 1, 0, 0, 0, 2, 194, 1, 0, 0, 0, 4, 197, 1, 0, 0, 0, 6, 215, 1, 0, 0, 0, 8, 243, 1, 0, 0, 0, 10, 245, 1, 0, 0, 0, 12, 248, 1, 0, 0, 0, 14, 250, 1, 0, 0, 0, 16, 253, 1, 0, 0, 0, 18, 264, 1, 0, 0, 0, 20, 268, 1, 0, 0, 0, 22, 276, 1, 0, 0, 0, 24, 281, 1, 0, 0, 0, 26, 284, 1, 0, 0, 0, 28, 287, 1, 0, 0, 0, 30, 307, 1, 0, 0, 0, 32, 309, 1, 0, 0, 0, 34, 311, 1, 0, 0, 0, 36, 313, 1, 0, 0, 0, 38, 315, 1, 0, 0, 0, 40, 317, 1, 0, 0, 0, 42, 326, 1, 0, 0, 0, 44, 329, 1, 0, 0, 0, 46, 337, 1, 0, 0, 0, 48, 345, 1, 0, 0, 0, 50, 362, 1, 0, 0, 0, 52, 364, 1, 0, 0, 0, 54, 384, 1, 0, 0, 0, 56, 386, 1, 0, 0, 0, 58, 394, 1, 0, 0, 0, 60, 402, 1, 0, 0, 0, 62, 407, 1, 0, 0, 0, 64, 411, 1, 0, 0, 0, 66, 415, 1, 0, 0, 0, 68, 420, 1, 0, 0, 0, 70, 422, 1, 0, 0, 0, 72, 425, 1, 0, 0, 0, 74, 434, 1, 0, 0, 0, 76, 442, 1, 0, 0, 0, 78, 445, 1, 0, 0, 0, 80, 448, 1, 0, 0, 0, 82, 465, 1, 0, 0, 0, 84, 467, 1, 0, 0, 0, 86, 473, 1, 0, 0, 0, 88, 481, 1, 0, 0, 0, 90, 487, 1, 0, 0, 0, 92, 489, 1, 0, 0, 0, 94, 493, 1, 0, 0, 0, 96, 496, 1, 0, 0, 0, 98, 499, 1, 0, 0, 0, 100, 503, 1, 0, 0, 0, 102, 506, 1, 0, 0, 0, 104, 523, 1, 0, 0, 0, 106, 528, 1, 0, 0, 0, 108, 532, 1, 0, 0, 0, 110, 535, 1, 0, 0, 0, 112, 548, 1, 0, 0, 0, 114, 552, 1, 0, 0, 0, 116, 556, 1, 0, 0, 0, 118, 560, 1, 0, 0, 0, 120, 571, 1, 0, 0, 0, 122, 573, 1, 0, 0, 0, 124, 584, 1, 0, 0, 0, 126, 593, 1, 0, 0, 0, 128, 598, 1, 0, 0, 0, 130, 604, 1, 0, 0, 0, 132, 607, 1, 0, 0, 0, 134, 613, 1, 0, 0, 0, 136, 617, 1, 0, 0, 0, 138, 650, 1, 0, 0, 0, 140, 709, 1, 0, 0, 0, 142, 711, 1, 0, 0, 0, 144, 724, 1, 0, 0, 0, 146, 730, 1, 0, 0, 0, 148, 751, 1, 0, 0, 0, 150, 761, 1, 0, 0, 0, 152, 783, 1, 0, 0, 0, 154, 785, 1, 0, 0, 0, 156, 798, 1, 0, 0, 0, 158, 804, 1, 0, 0, 0, 160, 848, 1, 0, 0, 0, 162, 850, 1, 0, 0, 0, 164, 854, 1, 0, 0, 0, 166, 857, 1, 0, 0, 0, 168, 862, 1, 0, 0, 0, 170, 866, 1, 0, 0, 0, 172, 868, 1, 0, 0, 0, 174, 870, 1, 0, 0, 0, 176, 883, 1, 0, 0, 0, 178, 885, 1, 0, 0, 0, 180, 182, 4, 0, 0, 0, 181, 183, 3, 134, 67, 0, 182, 181, 1, 0, 0, 0, 183, 184, 1, 0, 0, 0, 184, 182, 1, 0, 0, 0, 184, 185, 1, 0, 0, 0, 185, 186, 1, 0, 0, 0, 186, 187, 3, 2, 1, 0, 187, 188, 5, 0, 0, 1, 188, 193, 1, 0, 0, 0, 189, 190, 3, 2, 1, 0, 190, 191, 5, 0, 0, 1, 191, 193, 1, 0, 0, 0, 192, 180, 1, 0, 0, 0, 192, 189, 1, 0, 0, 0, 193, 1, 1, 0, 0, 0, 194, 195, 3, 4, 2, 0, 195, 196, 5, 0, 0, 1, 196, 3, 1, 0, 0, 0, 197, 198, 6, 2, -1, 0, 198, 199, 3, 6, 3, 0, 199, 205, 1, 0, 0, 0, 200, 201, 10, 1, 0, 0, 201, 202, 5, 49, 0, 0, 202, 204, 3, 8, 4, 0, 203, 200, 1, 0, 0, 0, 204, 207, 1, 0, 0, 0, 205, 203, 1, 0, 0, 0, 205, 206, 1, 0, 0, 0, 206, 5, 1, 0, 0, 0, 207, 205, 1, 0, 0, 0, 208, 216, 3, 24, 12, 0, 209, 216, 3, 14, 7, 0, 210, 216, 3, 100, 50, 0, 211, 212, 4, 3, 2, 0, 212, 216, 3, 26, 13, 0, 213, 214, 4, 3, 3, 0, 214, 216, 3, 96, 48, 0, 215, 208, 1, 0, 0, 0, 215, 209, 1, 0, 0, 0, 215, 210, 1, 0, 0, 0, 215, 211, 1, 0, 0, 0, 215, 213, 1, 0, 0, 0, 216, 7, 1, 0, 0, 0, 217, 244, 3, 42, 21, 0, 218, 244, 3, 10, 5, 0, 219, 244, 3, 76, 38, 0, 220, 244, 3, 70, 35, 0, 221, 244, 3, 44, 22, 0, 222, 244, 3, 72, 36, 0, 223, 244, 3, 78, 39, 0, 224, 244, 3, 80, 40, 0, 225, 244, 3, 84, 42, 0, 226, 244, 3, 92, 46, 0, 227, 244, 3, 102, 51, 0, 228, 244, 3, 94, 47, 0, 229, 244, 3, 174, 87, 0, 230, 244, 3, 110, 55, 0, 231, 244, 3, 124, 62, 0, 232, 244, 3, 108, 54, 0, 233, 244, 3, 112, 56, 0, 234, 244, 3, 122, 61, 0, 235, 236, 4, 4, 4, 0, 236, 244, 3, 128, 64, 0, 237, 238, 4, 4, 5, 0, 238, 244, 3, 126, 63, 0, 239, 240, 4, 4, 6, 0, 240, 244, 3, 130, 65, 0, 241, 242, 4, 4, 7, 0, 242, 244, 3, 132, 66, 0, 243, 217, 1, 0, 0, 0, 243, 218, 1, 0, 0, 0, 243, 219, 1, 0, 0, 0, 243, 220, 1, 0, 0, 0, 243, 221, 1, 0, 0, 0, 243, 222, 1, 0, 0, 0, 243, 223, 1, 0, 0, 0, 243, 224, 1, 0, 0, 0, 243, 225, 1, 0, 0, 0, 243, 226, 1, 0, 0, 0, 243, 227, 1, 0, 0, 0, 243, 228, 1, 0, 0, 0, 243, 229, 1, 0, 0, 0, 243, 230, 1, 0, 0, 0, 243, 231, 1, 0, 0, 0, 243, 232, 1, 0, 0, 0, 243, 233, 1, 0, 0, 0, 243, 234, 1, 0, 0, 0, 243, 235, 1, 0, 0, 0, 243, 237, 1, 0, 0, 0, 243, 239, 1, 0, 0, 0, 243, 241, 1, 0, 0, 0, 244, 9, 1, 0, 0, 0, 245, 246, 5, 17, 0, 0, 246, 247, 3, 138, 69, 0, 247, 11, 1, 0, 0, 0, 248, 249, 3, 60, 30, 0, 249, 13, 1, 0, 0, 0, 250, 251, 5, 13, 0, 0, 251, 252, 3, 16, 8, 0, 252, 15, 1, 0, 0, 0, 253, 258, 3, 18, 9, 0, 254, 255, 5, 60, 0, 0, 255, 257, 3, 18, 9, 0, 256, 254, 1, 0, 0, 0, 257, 260, 1, 0, 0, 0, 258, 256, 1, 0, 0, 0, 258, 259, 1, 0, 0, 0, 259, 17, 1, 0, 0, 0, 260, 258, 1, 0, 0, 0, 261, 262, 3, 50, 25, 0, 262, 263, 5, 55, 0, 0, 263, 265, 1, 0, 0, 0, 264, 261, 1, 0, 0, 0, 264, 265, 1, 0, 0, 0, 265, 266, 1, 0, 0, 0, 266, 267, 3, 138, 69, 0, 267, 19, 1, 0, 0, 0, 268, 273, 3, 22, 11, 0, 269, 270, 5, 60, 0, 0, 270, 272, 3, 22, 11, 0, 271, 269, 1, 0, 0, 0, 272, 275, 1, 0, 0, 0, 273, 271, 1, 0, 0, 0, 273, 274, 1, 0, 0, 0, 274, 21, 1, 0, 0, 0, 275, 273, 1, 0, 0, 0, 276, 279, 3, 50, 25, 0, 277, 278, 5, 55, 0, 0, 278, 280, 3, 138, 69, 0, 279, 277, 1, 0, 0, 0, 279, 280, 1, 0, 0, 0, 280, 23, 1, 0, 0, 0, 281, 282, 5, 19, 0, 0, 282, 283, 3, 28, 14, 0, 283, 25, 1, 0, 0, 0, 284, 285, 5, 20, 0, 0, 285, 286, 3, 28, 14, 0, 286, 27, 1, 0, 0, 0, 287, 292, 3, 30, 15, 0, 288, 289, 5, 60, 0, 0, 289, 291, 3, 30, 15, 0, 290, 288, 1, 0, 0, 0, 291, 294, 1, 0, 0, 0, 292, 290, 1, 0, 0, 0, 292, 293, 1, 0, 0, 0, 293, 296, 1, 0, 0, 0, 294, 292, 1, 0, 0, 0, 295, 297, 3, 40, 20, 0, 296, 295, 1, 0, 0, 0, 296, 297, 1, 0, 0, 0, 297, 29, 1, 0, 0, 0, 298, 299, 3, 32, 16, 0, 299, 300, 5, 58, 0, 0, 300, 301, 3, 36, 18, 0, 301, 308, 1, 0, 0, 0, 302, 303, 3, 36, 18, 0, 303, 304, 5, 57, 0, 0, 304, 305, 3, 34, 17, 0, 305, 308, 1, 0, 0, 0, 306, 308, 3, 38, 19, 0, 307, 298, 1, 0, 0, 0, 307, 302, 1, 0, 0, 0, 307, 306, 1, 0, 0, 0, 308, 31, 1, 0, 0, 0, 309, 310, 5, 105, 0, 0, 310, 33, 1, 0, 0, 0, 311, 312, 5, 105, 0, 0, 312, 35, 1, 0, 0, 0, 313, 314, 5, 105, 0, 0, 314, 37, 1, 0, 0, 0, 315, 316, 7, 0, 0, 0, 316, 39, 1, 0, 0, 0, 317, 318, 5, 104, 0, 0, 318, 323, 5, 105, 0, 0, 319, 320, 5, 60, 0, 0, 320, 322, 5, 105, 0, 0, 321, 319, 1, 0, 0, 0, 322, 325, 1, 0, 0, 0, 323, 321, 1, 0, 0, 0, 323, 324, 1, 0, 0, 0, 324, 41, 1, 0, 0, 0, 325, 323, 1, 0, 0, 0, 326, 327, 5, 9, 0, 0, 327, 328, 3, 16, 8, 0, 328, 43, 1, 0, 0, 0, 329, 331, 5, 16, 0, 0, 330, 332, 3, 46, 23, 0, 331, 330, 1, 0, 0, 0, 331, 332, 1, 0, 0, 0, 332, 335, 1, 0, 0, 0, 333, 334, 5, 56, 0, 0, 334, 336, 3, 16, 8, 0, 335, 333, 1, 0, 0, 0, 335, 336, 1, 0, 0, 0, 336, 45, 1, 0, 0, 0, 337, 342, 3, 48, 24, 0, 338, 339, 5, 60, 0, 0, 339, 341, 3, 48, 24, 0, 340, 338, 1, 0, 0, 0, 341, 344, 1, 0, 0, 0, 342, 340, 1, 0, 0, 0, 342, 343, 1, 0, 0, 0, 343, 47, 1, 0, 0, 0, 344, 342, 1, 0, 0, 0, 345, 348, 3, 18, 9, 0, 346, 347, 5, 17, 0, 0, 347, 349, 3, 138, 69, 0, 348, 346, 1, 0, 0, 0, 348, 349, 1, 0, 0, 0, 349, 49, 1, 0, 0, 0, 350, 351, 4, 25, 8, 0, 351, 353, 5, 95, 0, 0, 352, 354, 5, 99, 0, 0, 353, 352, 1, 0, 0, 0, 353, 354, 1, 0, 0, 0, 354, 355, 1, 0, 0, 0, 355, 356, 5, 96, 0, 0, 356, 357, 5, 62, 0, 0, 357, 358, 5, 95, 0, 0, 358, 359, 3, 52, 26, 0, 359, 360, 5, 96, 0, 0, 360, 363, 1, 0, 0, 0, 361, 363, 3, 52, 26, 0, 362, 350, 1, 0, 0, 0, 362, 361, 1, 0, 0, 0, 363, 51, 1, 0, 0, 0, 364, 369, 3, 68, 34, 0, 365, 366, 5, 62, 0, 0, 366, 368, 3, 68, 34, 0, 367, 365, 1, 0, 0, 0, 368, 371, 1, 0, 0, 0, 369, 367, 1, 0, 0, 0, 369, 370, 1, 0, 0, 0, 370, 53, 1, 0, 0, 0, 371, 369, 1, 0, 0, 0, 372, 373, 4, 27, 9, 0, 373, 375, 5, 95, 0, 0, 374, 376, 5, 129, 0, 0, 375, 374, 1, 0, 0, 0, 375, 376, 1, 0, 0, 0, 376, 377, 1, 0, 0, 0, 377, 378, 5, 96, 0, 0, 378, 379, 5, 62, 0, 0, 379, 380, 5, 95, 0, 0, 380, 381, 3, 56, 28, 0, 381, 382, 5, 96, 0, 0, 382, 385, 1, 0, 0, 0, 383, 385, 3, 56, 28, 0, 384, 372, 1, 0, 0, 0, 384, 383, 1, 0, 0, 0, 385, 55, 1, 0, 0, 0, 386, 391, 3, 62, 31, 0, 387, 388, 5, 62, 0, 0, 388, 390, 3, 62, 31, 0, 389, 387, 1, 0, 0, 0, 390, 393, 1, 0, 0, 0, 391, 389, 1, 0, 0, 0, 391, 392, 1, 0, 0, 0, 392, 57, 1, 0, 0, 0, 393, 391, 1, 0, 0, 0, 394, 399, 3, 54, 27, 0, 395, 396, 5, 60, 0, 0, 396, 398, 3, 54, 27, 0, 397, 395, 1, 0, 0, 0, 398, 401, 1, 0, 0, 0, 399, 397, 1, 0, 0, 0, 399, 400, 1, 0, 0, 0, 400, 59, 1, 0, 0, 0, 401, 399, 1, 0, 0, 0, 402, 403, 7, 1, 0, 0, 403, 61, 1, 0, 0, 0, 404, 408, 5, 129, 0, 0, 405, 408, 3, 64, 32, 0, 406, 408, 3, 66, 33, 0, 407, 404, 1, 0, 0, 0, 407, 405, 1, 0, 0, 0, 407, 406, 1, 0, 0, 0, 408, 63, 1, 0, 0, 0, 409, 412, 5, 74, 0, 0, 410, 412, 5, 93, 0, 0, 411, 409, 1, 0, 0, 0, 411, 410, 1, 0, 0, 0, 412, 65, 1, 0, 0, 0, 413, 416, 5, 92, 0, 0, 414, 416, 5, 94, 0, 0, 415, 413, 1, 0, 0, 0, 415, 414, 1, 0, 0, 0, 416, 67, 1, 0, 0, 0, 417, 421, 3, 60, 30, 0, 418, 421, 3, 64, 32, 0, 419, 421, 3, 66, 33, 0, 420, 417, 1, 0, 0, 0, 420, 418, 1, 0, 0, 0, 420, 419, 1, 0, 0, 0, 421, 69, 1, 0, 0, 0, 422, 423, 5, 11, 0, 0, 423, 424, 3, 160, 80, 0, 424, 71, 1, 0, 0, 0, 425, 426, 5, 15, 0, 0, 426, 431, 3, 74, 37, 0, 427, 428, 5, 60, 0, 0, 428, 430, 3, 74, 37, 0, 429, 427, 1, 0, 0, 0, 430, 433, 1, 0, 0, 0, 431, 429, 1, 0, 0, 0, 431, 432, 1, 0, 0, 0, 432, 73, 1, 0, 0, 0, 433, 431, 1, 0, 0, 0, 434, 436, 3, 138, 69, 0, 435, 437, 7, 2, 0, 0, 436, 435, 1, 0, 0, 0, 436, 437, 1, 0, 0, 0, 437, 440, 1, 0, 0, 0, 438, 439, 5, 71, 0, 0, 439, 441, 7, 3, 0, 0, 440, 438, 1, 0, 0, 0, 440, 441, 1, 0, 0, 0, 441, 75, 1, 0, 0, 0, 442, 443, 5, 30, 0, 0, 443, 444, 3, 58, 29, 0, 444, 77, 1, 0, 0, 0, 445, 446, 5, 29, 0, 0, 446, 447, 3, 58, 29, 0, 447, 79, 1, 0, 0, 0, 448, 449, 5, 32, 0, 0, 449, 454, 3, 82, 41, 0, 450, 451, 5, 60, 0, 0, 451, 453, 3, 82, 41, 0, 452, 450, 1, 0, 0, 0, 453, 456, 1, 0, 0, 0, 454, 452, 1, 0, 0, 0, 454, 455, 1, 0, 0, 0, 455, 81, 1, 0, 0, 0, 456, 454, 1, 0, 0, 0, 457, 458, 3, 54, 27, 0, 458, 459, 5, 133, 0, 0, 459, 460, 3, 54, 27, 0, 460, 466, 1, 0, 0, 0, 461, 462, 3, 54, 27, 0, 462, 463, 5, 55, 0, 0, 463, 464, 3, 54, 27, 0, 464, 466, 1, 0, 0, 0, 465, 457, 1, 0, 0, 0, 465, 461, 1, 0, 0, 0, 466, 83, 1, 0, 0, 0, 467, 468, 5, 8, 0, 0, 468, 469, 3, 148, 74, 0, 469, 471, 3, 170, 85, 0, 470, 472, 3, 86, 43, 0, 471, 470, 1, 0, 0, 0, 471, 472, 1, 0, 0, 0, 472, 85, 1, 0, 0, 0, 473, 478, 3, 88, 44, 0, 474, 475, 5, 60, 0, 0, 475, 477, 3, 88, 44, 0, 476, 474, 1, 0, 0, 0, 477, 480, 1, 0, 0, 0, 478, 476, 1, 0, 0, 0, 478, 479, 1, 0, 0, 0, 479, 87, 1, 0, 0, 0, 480, 478, 1, 0, 0, 0, 481, 482, 3, 60, 30, 0, 482, 483, 5, 55, 0, 0, 483, 484, 3, 160, 80, 0, 484, 89, 1, 0, 0, 0, 485, 486, 5, 77, 0, 0, 486, 488, 3, 154, 77, 0, 487, 485, 1, 0, 0, 0, 487, 488, 1, 0, 0, 0, 488, 91, 1, 0, 0, 0, 489, 490, 5, 10, 0, 0, 490, 491, 3, 148, 74, 0, 491, 492, 3, 170, 85, 0, 492, 93, 1, 0, 0, 0, 493, 494, 5, 28, 0, 0, 494, 495, 3, 50, 25, 0, 495, 95, 1, 0, 0, 0, 496, 497, 5, 6, 0, 0, 497, 498, 3, 98, 49, 0, 498, 97, 1, 0, 0, 0, 499, 500, 5, 97, 0, 0, 500, 501, 3, 4, 2, 0, 501, 502, 5, 98, 0, 0, 502, 99, 1, 0, 0, 0, 503, 504, 5, 34, 0, 0, 504, 505, 5, 140, 0, 0, 505, 101, 1, 0, 0, 0, 506, 507, 5, 5, 0, 0, 507, 510, 3, 104, 52, 0, 508, 509, 5, 72, 0, 0, 509, 511, 3, 54, 27, 0, 510, 508, 1, 0, 0, 0, 510, 511, 1, 0, 0, 0, 511, 521, 1, 0, 0, 0, 512, 513, 5, 77, 0, 0, 513, 518, 3, 106, 53, 0, 514, 515, 5, 60, 0, 0, 515, 517, 3, 106, 53, 0, 516, 514, 1, 0, 0, 0, 517, 520, 1, 0, 0, 0, 518, 516, 1, 0, 0, 0, 518, 519, 1, 0, 0, 0, 519, 522, 1, 0, 0, 0, 520, 518, 1, 0, 0, 0, 521, 512, 1, 0, 0, 0, 521, 522, 1, 0, 0, 0, 522, 103, 1, 0, 0, 0, 523, 524, 7, 4, 0, 0, 524, 105, 1, 0, 0, 0, 525, 526, 3, 54, 27, 0, 526, 527, 5, 55, 0, 0, 527, 529, 1, 0, 0, 0, 528, 525, 1, 0, 0, 0, 528, 529, 1, 0, 0, 0, 529, 530, 1, 0, 0, 0, 530, 531, 3, 54, 27, 0, 531, 107, 1, 0, 0, 0, 532, 533, 5, 14, 0, 0, 533, 534, 3, 160, 80, 0, 534, 109, 1, 0, 0, 0, 535, 536, 5, 4, 0, 0, 536, 539, 3, 50, 25, 0, 537, 538, 5, 72, 0, 0, 538, 540, 3, 50, 25, 0, 539, 537, 1, 0, 0, 0, 539, 540, 1, 0, 0, 0, 540, 546, 1, 0, 0, 0, 541, 542, 5, 133, 0, 0, 542, 543, 3, 50, 25, 0, 543, 544, 5, 60, 0, 0, 544, 545, 3, 50, 25, 0, 545, 547, 1, 0, 0, 0, 546, 541, 1, 0, 0, 0, 546, 547, 1, 0, 0, 0, 547, 111, 1, 0, 0, 0, 548, 549, 5, 21, 0, 0, 549, 550, 3, 114, 57, 0, 550, 113, 1, 0, 0, 0, 551, 553, 3, 116, 58, 0, 552, 551, 1, 0, 0, 0, 553, 554, 1, 0, 0, 0, 554, 552, 1, 0, 0, 0, 554, 555, 1, 0, 0, 0, 555, 115, 1, 0, 0, 0, 556, 557, 5, 97, 0, 0, 557, 558, 3, 118, 59, 0, 558, 559, 5, 98, 0, 0, 559, 117, 1, 0, 0, 0, 560, 561, 6, 59, -1, 0, 561, 562, 3, 120, 60, 0, 562, 568, 1, 0, 0, 0, 563, 564, 10, 1, 0, 0, 564, 565, 5, 49, 0, 0, 565, 567, 3, 120, 60, 0, 566, 563, 1, 0, 0, 0, 567, 570, 1, 0, 0, 0, 568, 566, 1, 0, 0, 0, 568, 569, 1, 0, 0, 0, 569, 119, 1, 0, 0, 0, 570, 568, 1, 0, 0, 0, 571, 572, 3, 8, 4, 0, 572, 121, 1, 0, 0, 0, 573, 577, 5, 12, 0, 0, 574, 575, 3, 50, 25, 0, 575, 576, 5, 55, 0, 0, 576, 578, 1, 0, 0, 0, 577, 574, 1, 0, 0, 0, 577, 578, 1, 0, 0, 0, 578, 579, 1, 0, 0, 0, 579, 580, 3, 160, 80, 0, 580, 581, 5, 72, 0, 0, 581, 582, 3, 20, 10, 0, 582, 583, 3, 90, 45, 0, 583, 123, 1, 0, 0, 0, 584, 588, 5, 7, 0, 0, 585, 586, 3, 50, 25, 0, 586, 587, 5, 55, 0, 0, 587, 589, 1, 0, 0, 0, 588, 585, 1, 0, 0, 0, 588, 589, 1, 0, 0, 0, 589, 590, 1, 0, 0, 0, 590, 591, 3, 148, 74, 0, 591, 592, 3, 90, 45, 0, 592, 125, 1, 0, 0, 0, 593, 594, 5, 27, 0, 0, 594, 595, 3, 30, 15, 0, 595, 596, 5, 72, 0, 0, 596, 597, 3, 58, 29, 0, 597, 127, 1, 0, 0, 0, 598, 599, 5, 18, 0, 0, 599, 602, 3, 46, 23, 0, 600, 601, 5, 56, 0, 0, 601, 603, 3, 16, 8, 0, 602, 600, 1, 0, 0, 0, 602, 603, 1, 0, 0, 0, 603, 129, 1, 0, 0, 0, 604, 605, 5, 31, 0, 0, 605, 606, 3, 58, 29, 0, 606, 131, 1, 0, 0, 0, 607, 609, 5, 22, 0, 0, 608, 610, 3, 60, 30, 0, 609, 608, 1, 0, 0, 0, 609, 610, 1, 0, 0, 0, 610, 611, 1, 0, 0, 0, 611, 612, 3, 90, 45, 0, 612, 133, 1, 0, 0, 0, 613, 614, 5, 33, 0, 0, 614, 615, 3, 136, 68, 0, 615, 616, 5, 59, 0, 0, 616, 135, 1, 0, 0, 0, 617, 618, 3, 60, 30, 0, 618, 619, 5, 55, 0, 0, 619, 620, 3, 160, 80, 0, 620, 137, 1, 0, 0, 0, 621, 622, 6, 69, -1, 0, 622, 623, 5, 69, 0, 0, 623, 651, 3, 138, 69, 8, 624, 651, 3, 144, 72, 0, 625, 651, 3, 140, 70, 0, 626, 628, 3, 144, 72, 0, 627, 629, 5, 69, 0, 0, 628, 627, 1, 0, 0, 0, 628, 629, 1, 0, 0, 0, 629, 630, 1, 0, 0, 0, 630, 631, 5, 65, 0, 0, 631, 632, 5, 97, 0, 0, 632, 637, 3, 144, 72, 0, 633, 634, 5, 60, 0, 0, 634, 636, 3, 144, 72, 0, 635, 633, 1, 0, 0, 0, 636, 639, 1, 0, 0, 0, 637, 635, 1, 0, 0, 0, 637, 638, 1, 0, 0, 0, 638, 640, 1, 0, 0, 0, 639, 637, 1, 0, 0, 0, 640, 641, 5, 98, 0, 0, 641, 651, 1, 0, 0, 0, 642, 643, 3, 144, 72, 0, 643, 645, 5, 66, 0, 0, 644, 646, 5, 69, 0, 0, 645, 644, 1, 0, 0, 0, 645, 646, 1, 0, 0, 0, 646, 647, 1, 0, 0, 0, 647, 648, 5, 70, 0, 0, 648, 651, 1, 0, 0, 0, 649, 651, 3, 142, 71, 0, 650, 621, 1, 0, 0, 0, 650, 624, 1, 0, 0, 0, 650, 625, 1, 0, 0, 0, 650, 626, 1, 0, 0, 0, 650, 642, 1, 0, 0, 0, 650, 649, 1, 0, 0, 0, 651, 660, 1, 0, 0, 0, 652, 653, 10, 5, 0, 0, 653, 654, 5, 53, 0, 0, 654, 659, 3, 138, 69, 6, 655, 656, 10, 4, 0, 0, 656, 657, 5, 73, 0, 0, 657, 659, 3, 138, 69, 5, 658, 652, 1, 0, 0, 0, 658, 655, 1, 0, 0, 0, 659, 662, 1, 0, 0, 0, 660, 658, 1, 0, 0, 0, 660, 661, 1, 0, 0, 0, 661, 139, 1, 0, 0, 0, 662, 660, 1, 0, 0, 0, 663, 665, 3, 144, 72, 0, 664, 666, 5, 69, 0, 0, 665, 664, 1, 0, 0, 0, 665, 666, 1, 0, 0, 0, 666, 667, 1, 0, 0, 0, 667, 668, 5, 68, 0, 0, 668, 669, 3, 170, 85, 0, 669, 710, 1, 0, 0, 0, 670, 672, 3, 144, 72, 0, 671, 673, 5, 69, 0, 0, 672, 671, 1, 0, 0, 0, 672, 673, 1, 0, 0, 0, 673, 674, 1, 0, 0, 0, 674, 675, 5, 75, 0, 0, 675, 676, 3, 170, 85, 0, 676, 710, 1, 0, 0, 0, 677, 679, 3, 144, 72, 0, 678, 680, 5, 69, 0, 0, 679, 678, 1, 0, 0, 0, 679, 680, 1, 0, 0, 0, 680, 681, 1, 0, 0, 0, 681, 682, 5, 68, 0, 0, 682, 683, 5, 97, 0, 0, 683, 688, 3, 170, 85, 0, 684, 685, 5, 60, 0, 0, 685, 687, 3, 170, 85, 0, 686, 684, 1, 0, 0, 0, 687, 690, 1, 0, 0, 0, 688, 686, 1, 0, 0, 0, 688, 689, 1, 0, 0, 0, 689, 691, 1, 0, 0, 0, 690, 688, 1, 0, 0, 0, 691, 692, 5, 98, 0, 0, 692, 710, 1, 0, 0, 0, 693, 695, 3, 144, 72, 0, 694, 696, 5, 69, 0, 0, 695, 694, 1, 0, 0, 0, 695, 696, 1, 0, 0, 0, 696, 697, 1, 0, 0, 0, 697, 698, 5, 75, 0, 0, 698, 699, 5, 97, 0, 0, 699, 704, 3, 170, 85, 0, 700, 701, 5, 60, 0, 0, 701, 703, 3, 170, 85, 0, 702, 700, 1, 0, 0, 0, 703, 706, 1, 0, 0, 0, 704, 702, 1, 0, 0, 0, 704, 705, 1, 0, 0, 0, 705, 707, 1, 0, 0, 0, 706, 704, 1, 0, 0, 0, 707, 708, 5, 98, 0, 0, 708, 710, 1, 0, 0, 0, 709, 663, 1, 0, 0, 0, 709, 670, 1, 0, 0, 0, 709, 677, 1, 0, 0, 0, 709, 693, 1, 0, 0, 0, 710, 141, 1, 0, 0, 0, 711, 714, 3, 50, 25, 0, 712, 713, 5, 57, 0, 0, 713, 715, 3, 12, 6, 0, 714, 712, 1, 0, 0, 0, 714, 715, 1, 0, 0, 0, 715, 716, 1, 0, 0, 0, 716, 717, 5, 58, 0, 0, 717, 718, 3, 160, 80, 0, 718, 143, 1, 0, 0, 0, 719, 725, 3, 146, 73, 0, 720, 721, 3, 146, 73, 0, 721, 722, 3, 172, 86, 0, 722, 723, 3, 146, 73, 0, 723, 725, 1, 0, 0, 0, 724, 719, 1, 0, 0, 0, 724, 720, 1, 0, 0, 0, 725, 145, 1, 0, 0, 0, 726, 727, 6, 73, -1, 0, 727, 731, 3, 148, 74, 0, 728, 729, 7, 5, 0, 0, 729, 731, 3, 146, 73, 3, 730, 726, 1, 0, 0, 0, 730, 728, 1, 0, 0, 0, 731, 740, 1, 0, 0, 0, 732, 733, 10, 2, 0, 0, 733, 734, 7, 6, 0, 0, 734, 739, 3, 146, 73, 3, 735, 736, 10, 1, 0, 0, 736, 737, 7, 5, 0, 0, 737, 739, 3, 146, 73, 2, 738, 732, 1, 0, 0, 0, 738, 735, 1, 0, 0, 0, 739, 742, 1, 0, 0, 0, 740, 738, 1, 0, 0, 0, 740, 741, 1, 0, 0, 0, 741, 147, 1, 0, 0, 0, 742, 740, 1, 0, 0, 0, 743, 744, 6, 74, -1, 0, 744, 752, 3, 160, 80, 0, 745, 752, 3, 50, 25, 0, 746, 752, 3, 150, 75, 0, 747, 748, 5, 97, 0, 0, 748, 749, 3, 138, 69, 0, 749, 750, 5, 98, 0, 0, 750, 752, 1, 0, 0, 0, 751, 743, 1, 0, 0, 0, 751, 745, 1, 0, 0, 0, 751, 746, 1, 0, 0, 0, 751, 747, 1, 0, 0, 0, 752, 758, 1, 0, 0, 0, 753, 754, 10, 1, 0, 0, 754, 755, 5, 57, 0, 0, 755, 757, 3, 12, 6, 0, 756, 753, 1, 0, 0, 0, 757, 760, 1, 0, 0, 0, 758, 756, 1, 0, 0, 0, 758, 759, 1, 0, 0, 0, 759, 149, 1, 0, 0, 0, 760, 758, 1, 0, 0, 0, 761, 762, 3, 152, 76, 0, 762, 776, 5, 97, 0, 0, 763, 777, 5, 87, 0, 0, 764, 769, 3, 138, 69, 0, 765, 766, 5, 60, 0, 0, 766, 768, 3, 138, 69, 0, 767, 765, 1, 0, 0, 0, 768, 771, 1, 0, 0, 0, 769, 767, 1, 0, 0, 0, 769, 770, 1, 0, 0, 0, 770, 774, 1, 0, 0, 0, 771, 769, 1, 0, 0, 0, 772, 773, 5, 60, 0, 0, 773, 775, 3, 154, 77, 0, 774, 772, 1, 0, 0, 0, 774, 775, 1, 0, 0, 0, 775, 777, 1, 0, 0, 0, 776, 763, 1, 0, 0, 0, 776, 764, 1, 0, 0, 0, 776, 777, 1, 0, 0, 0, 777, 778, 1, 0, 0, 0, 778, 779, 5, 98, 0, 0, 779, 151, 1, 0, 0, 0, 780, 784, 3, 68, 34, 0, 781, 784, 5, 64, 0, 0, 782, 784, 5, 67, 0, 0, 783, 780, 1, 0, 0, 0, 783, 781, 1, 0, 0, 0, 783, 782, 1, 0, 0, 0, 784, 153, 1, 0, 0, 0, 785, 794, 5, 90, 0, 0, 786, 791, 3, 156, 78, 0, 787, 788, 5, 60, 0, 0, 788, 790, 3, 156, 78, 0, 789, 787, 1, 0, 0, 0, 790, 793, 1, 0, 0, 0, 791, 789, 1, 0, 0, 0, 791, 792, 1, 0, 0, 0, 792, 795, 1, 0, 0, 0, 793, 791, 1, 0, 0, 0, 794, 786, 1, 0, 0, 0, 794, 795, 1, 0, 0, 0, 795, 796, 1, 0, 0, 0, 796, 797, 5, 91, 0, 0, 797, 155, 1, 0, 0, 0, 798, 799, 3, 170, 85, 0, 799, 800, 5, 58, 0, 0, 800, 801, 3, 158, 79, 0, 801, 157, 1, 0, 0, 0, 802, 805, 3, 160, 80, 0, 803, 805, 3, 154, 77, 0, 804, 802, 1, 0, 0, 0, 804, 803, 1, 0, 0, 0, 805, 159, 1, 0, 0, 0, 806, 849, 5, 70, 0, 0, 807, 808, 3, 168, 84, 0, 808, 809, 5, 99, 0, 0, 809, 849, 1, 0, 0, 0, 810, 849, 3, 166, 83, 0, 811, 849, 3, 168, 84, 0, 812, 849, 3, 162, 81, 0, 813, 849, 3, 64, 32, 0, 814, 849, 3, 170, 85, 0, 815, 816, 5, 95, 0, 0, 816, 821, 3, 164, 82, 0, 817, 818, 5, 60, 0, 0, 818, 820, 3, 164, 82, 0, 819, 817, 1, 0, 0, 0, 820, 823, 1, 0, 0, 0, 821, 819, 1, 0, 0, 0, 821, 822, 1, 0, 0, 0, 822, 824, 1, 0, 0, 0, 823, 821, 1, 0, 0, 0, 824, 825, 5, 96, 0, 0, 825, 849, 1, 0, 0, 0, 826, 827, 5, 95, 0, 0, 827, 832, 3, 162, 81, 0, 828, 829, 5, 60, 0, 0, 829, 831, 3, 162, 81, 0, 830, 828, 1, 0, 0, 0, 831, 834, 1, 0, 0, 0, 832, 830, 1, 0, 0, 0, 832, 833, 1, 0, 0, 0, 833, 835, 1, 0, 0, 0, 834, 832, 1, 0, 0, 0, 835, 836, 5, 96, 0, 0, 836, 849, 1, 0, 0, 0, 837, 838, 5, 95, 0, 0, 838, 843, 3, 170, 85, 0, 839, 840, 5, 60, 0, 0, 840, 842, 3, 170, 85, 0, 841, 839, 1, 0, 0, 0, 842, 845, 1, 0, 0, 0, 843, 841, 1, 0, 0, 0, 843, 844, 1, 0, 0, 0, 844, 846, 1, 0, 0, 0, 845, 843, 1, 0, 0, 0, 846, 847, 5, 96, 0, 0, 847, 849, 1, 0, 0, 0, 848, 806, 1, 0, 0, 0, 848, 807, 1, 0, 0, 0, 848, 810, 1, 0, 0, 0, 848, 811, 1, 0, 0, 0, 848, 812, 1, 0, 0, 0, 848, 813, 1, 0, 0, 0, 848, 814, 1, 0, 0, 0, 848, 815, 1, 0, 0, 0, 848, 826, 1, 0, 0, 0, 848, 837, 1, 0, 0, 0, 849, 161, 1, 0, 0, 0, 850, 851, 7, 7, 0, 0, 851, 163, 1, 0, 0, 0, 852, 855, 3, 166, 83, 0, 853, 855, 3, 168, 84, 0, 854, 852, 1, 0, 0, 0, 854, 853, 1, 0, 0, 0, 855, 165, 1, 0, 0, 0, 856, 858, 7, 5, 0, 0, 857, 856, 1, 0, 0, 0, 857, 858, 1, 0, 0, 0, 858, 859, 1, 0, 0, 0, 859, 860, 5, 52, 0, 0, 860, 167, 1, 0, 0, 0, 861, 863, 7, 5, 0, 0, 862, 861, 1, 0, 0, 0, 862, 863, 1, 0, 0, 0, 863, 864, 1, 0, 0, 0, 864, 865, 5, 51, 0, 0, 865, 169, 1, 0, 0, 0, 866, 867, 5, 50, 0, 0, 867, 171, 1, 0, 0, 0, 868, 869, 7, 8, 0, 0, 869, 173, 1, 0, 0, 0, 870, 871, 7, 9, 0, 0, 871, 872, 5, 115, 0, 0, 872, 873, 3, 176, 88, 0, 873, 874, 3, 178, 89, 0, 874, 175, 1, 0, 0, 0, 875, 876, 4, 88, 16, 0, 876, 878, 3, 30, 15, 0, 877, 879, 5, 133, 0, 0, 878, 877, 1, 0, 0, 0, 878, 879, 1, 0, 0, 0, 879, 880, 1, 0, 0, 0, 880, 881, 5, 105, 0, 0, 881, 884, 1, 0, 0, 0, 882, 884, 3, 30, 15, 0, 883, 875, 1, 0, 0, 0, 883, 882, 1, 0, 0, 0, 884, 177, 1, 0, 0, 0, 885, 886, 5, 72, 0, 0, 886, 891, 3, 138, 69, 0, 887, 888, 5, 60, 0, 0, 888, 890, 3, 138, 69, 0, 889, 887, 1, 0, 0, 0, 890, 893, 1, 0, 0, 0, 891, 889, 1, 0, 0, 0, 891, 892, 1, 0, 0, 0, 892, 179, 1, 0, 0, 0, 893, 891, 1, 0, 0, 0, 85, 184, 192, 205, 215, 243, 258, 264, 273, 279, 292, 296, 307, 323, 331, 335, 342, 348, 353, 362, 369, 375, 384, 391, 399, 407, 411, 415, 420, 431, 436, 440, 454, 465, 471, 478, 487, 510, 518, 521, 528, 539, 546, 554, 568, 577, 588, 602, 609, 628, 637, 645, 650, 658, 660, 665, 672, 679, 688, 695, 704, 709, 714, 724, 730, 738, 740, 751, 758, 769, 774, 776, 783, 791, 794, 804, 821, 832, 843, 848, 854, 857, 862, 878, 883, 891] \ No newline at end of file diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParser.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParser.java index cf25d34bb36a3..6772db4e9fa4e 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParser.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParser.java @@ -82,8 +82,7 @@ public class EsqlBaseParser extends ParserConfig { RULE_functionName = 76, RULE_mapExpression = 77, RULE_entryExpression = 78, RULE_mapValue = 79, RULE_constant = 80, RULE_booleanValue = 81, RULE_numericValue = 82, RULE_decimalValue = 83, RULE_integerValue = 84, RULE_string = 85, RULE_comparisonOperator = 86, - RULE_joinCommand = 87, RULE_joinTarget = 88, RULE_joinCondition = 89, - RULE_joinPredicate = 90; + RULE_joinCommand = 87, RULE_joinTarget = 88, RULE_joinCondition = 89; private static String[] makeRuleNames() { return new String[] { "statements", "singleStatement", "query", "sourceCommand", "processingCommand", @@ -106,7 +105,7 @@ private static String[] makeRuleNames() { "operatorExpression", "primaryExpression", "functionExpression", "functionName", "mapExpression", "entryExpression", "mapValue", "constant", "booleanValue", "numericValue", "decimalValue", "integerValue", "string", "comparisonOperator", - "joinCommand", "joinTarget", "joinCondition", "joinPredicate" + "joinCommand", "joinTarget", "joinCondition" }; } public static final String[] ruleNames = makeRuleNames(); @@ -252,15 +251,15 @@ public final StatementsContext statements() throws RecognitionException { enterRule(_localctx, 0, RULE_statements); try { int _alt; - setState(194); + setState(192); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,1,_ctx) ) { case 1: enterOuterAlt(_localctx, 1); { - setState(182); + setState(180); if (!(this.isDevVersion())) throw new FailedPredicateException(this, "this.isDevVersion()"); - setState(184); + setState(182); _errHandler.sync(this); _alt = 1; do { @@ -268,7 +267,7 @@ public final StatementsContext statements() throws RecognitionException { case 1: { { - setState(183); + setState(181); setCommand(); } } @@ -276,22 +275,22 @@ public final StatementsContext statements() throws RecognitionException { default: throw new NoViableAltException(this); } - setState(186); + setState(184); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,0,_ctx); } while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ); - setState(188); + setState(186); singleStatement(); - setState(189); + setState(187); match(EOF); } break; case 2: enterOuterAlt(_localctx, 2); { - setState(191); + setState(189); singleStatement(); - setState(192); + setState(190); match(EOF); } break; @@ -340,9 +339,9 @@ public final SingleStatementContext singleStatement() throws RecognitionExceptio try { enterOuterAlt(_localctx, 1); { - setState(196); + setState(194); query(0); - setState(197); + setState(195); match(EOF); } } @@ -438,11 +437,11 @@ private QueryContext query(int _p) throws RecognitionException { _ctx = _localctx; _prevctx = _localctx; - setState(200); + setState(198); sourceCommand(); } _ctx.stop = _input.LT(-1); - setState(207); + setState(205); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,2,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { @@ -453,16 +452,16 @@ private QueryContext query(int _p) throws RecognitionException { { _localctx = new CompositeQueryContext(new QueryContext(_parentctx, _parentState)); pushNewRecursionContext(_localctx, _startState, RULE_query); - setState(202); + setState(200); if (!(precpred(_ctx, 1))) throw new FailedPredicateException(this, "precpred(_ctx, 1)"); - setState(203); + setState(201); match(PIPE); - setState(204); + setState(202); processingCommand(); } } } - setState(209); + setState(207); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,2,_ctx); } @@ -520,45 +519,45 @@ public final SourceCommandContext sourceCommand() throws RecognitionException { SourceCommandContext _localctx = new SourceCommandContext(_ctx, getState()); enterRule(_localctx, 6, RULE_sourceCommand); try { - setState(217); + setState(215); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,3,_ctx) ) { case 1: enterOuterAlt(_localctx, 1); { - setState(210); + setState(208); fromCommand(); } break; case 2: enterOuterAlt(_localctx, 2); { - setState(211); + setState(209); rowCommand(); } break; case 3: enterOuterAlt(_localctx, 3); { - setState(212); + setState(210); showCommand(); } break; case 4: enterOuterAlt(_localctx, 4); { - setState(213); + setState(211); if (!(this.isDevVersion())) throw new FailedPredicateException(this, "this.isDevVersion()"); - setState(214); + setState(212); timeSeriesCommand(); } break; case 5: enterOuterAlt(_localctx, 5); { - setState(215); + setState(213); if (!(this.isDevVersion())) throw new FailedPredicateException(this, "this.isDevVersion()"); - setState(216); + setState(214); explainCommand(); } break; @@ -667,168 +666,168 @@ public final ProcessingCommandContext processingCommand() throws RecognitionExce ProcessingCommandContext _localctx = new ProcessingCommandContext(_ctx, getState()); enterRule(_localctx, 8, RULE_processingCommand); try { - setState(245); + setState(243); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,4,_ctx) ) { case 1: enterOuterAlt(_localctx, 1); { - setState(219); + setState(217); evalCommand(); } break; case 2: enterOuterAlt(_localctx, 2); { - setState(220); + setState(218); whereCommand(); } break; case 3: enterOuterAlt(_localctx, 3); { - setState(221); + setState(219); keepCommand(); } break; case 4: enterOuterAlt(_localctx, 4); { - setState(222); + setState(220); limitCommand(); } break; case 5: enterOuterAlt(_localctx, 5); { - setState(223); + setState(221); statsCommand(); } break; case 6: enterOuterAlt(_localctx, 6); { - setState(224); + setState(222); sortCommand(); } break; case 7: enterOuterAlt(_localctx, 7); { - setState(225); + setState(223); dropCommand(); } break; case 8: enterOuterAlt(_localctx, 8); { - setState(226); + setState(224); renameCommand(); } break; case 9: enterOuterAlt(_localctx, 9); { - setState(227); + setState(225); dissectCommand(); } break; case 10: enterOuterAlt(_localctx, 10); { - setState(228); + setState(226); grokCommand(); } break; case 11: enterOuterAlt(_localctx, 11); { - setState(229); + setState(227); enrichCommand(); } break; case 12: enterOuterAlt(_localctx, 12); { - setState(230); + setState(228); mvExpandCommand(); } break; case 13: enterOuterAlt(_localctx, 13); { - setState(231); + setState(229); joinCommand(); } break; case 14: enterOuterAlt(_localctx, 14); { - setState(232); + setState(230); changePointCommand(); } break; case 15: enterOuterAlt(_localctx, 15); { - setState(233); + setState(231); completionCommand(); } break; case 16: enterOuterAlt(_localctx, 16); { - setState(234); + setState(232); sampleCommand(); } break; case 17: enterOuterAlt(_localctx, 17); { - setState(235); + setState(233); forkCommand(); } break; case 18: enterOuterAlt(_localctx, 18); { - setState(236); + setState(234); rerankCommand(); } break; case 19: enterOuterAlt(_localctx, 19); { - setState(237); + setState(235); if (!(this.isDevVersion())) throw new FailedPredicateException(this, "this.isDevVersion()"); - setState(238); + setState(236); inlinestatsCommand(); } break; case 20: enterOuterAlt(_localctx, 20); { - setState(239); + setState(237); if (!(this.isDevVersion())) throw new FailedPredicateException(this, "this.isDevVersion()"); - setState(240); + setState(238); lookupCommand(); } break; case 21: enterOuterAlt(_localctx, 21); { - setState(241); + setState(239); if (!(this.isDevVersion())) throw new FailedPredicateException(this, "this.isDevVersion()"); - setState(242); + setState(240); insistCommand(); } break; case 22: enterOuterAlt(_localctx, 22); { - setState(243); + setState(241); if (!(this.isDevVersion())) throw new FailedPredicateException(this, "this.isDevVersion()"); - setState(244); + setState(242); fuseCommand(); } break; @@ -877,9 +876,9 @@ public final WhereCommandContext whereCommand() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(247); + setState(245); match(WHERE); - setState(248); + setState(246); booleanExpression(0); } } @@ -937,7 +936,7 @@ public final DataTypeContext dataType() throws RecognitionException { _localctx = new ToDataTypeContext(_localctx); enterOuterAlt(_localctx, 1); { - setState(250); + setState(248); identifier(); } } @@ -984,9 +983,9 @@ public final RowCommandContext rowCommand() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(252); + setState(250); match(ROW); - setState(253); + setState(251); fields(); } } @@ -1040,23 +1039,23 @@ public final FieldsContext fields() throws RecognitionException { int _alt; enterOuterAlt(_localctx, 1); { - setState(255); + setState(253); field(); - setState(260); + setState(258); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,5,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(256); + setState(254); match(COMMA); - setState(257); + setState(255); field(); } } } - setState(262); + setState(260); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,5,_ctx); } @@ -1108,19 +1107,19 @@ public final FieldContext field() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(266); + setState(264); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,6,_ctx) ) { case 1: { - setState(263); + setState(261); qualifiedName(); - setState(264); + setState(262); match(ASSIGN); } break; } - setState(268); + setState(266); booleanExpression(0); } } @@ -1174,23 +1173,23 @@ public final RerankFieldsContext rerankFields() throws RecognitionException { int _alt; enterOuterAlt(_localctx, 1); { - setState(270); + setState(268); rerankField(); - setState(275); + setState(273); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,7,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(271); + setState(269); match(COMMA); - setState(272); + setState(270); rerankField(); } } } - setState(277); + setState(275); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,7,_ctx); } @@ -1242,16 +1241,16 @@ public final RerankFieldContext rerankField() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(278); + setState(276); qualifiedName(); - setState(281); + setState(279); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,8,_ctx) ) { case 1: { - setState(279); + setState(277); match(ASSIGN); - setState(280); + setState(278); booleanExpression(0); } break; @@ -1301,9 +1300,9 @@ public final FromCommandContext fromCommand() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(283); + setState(281); match(FROM); - setState(284); + setState(282); indexPatternAndMetadataFields(); } } @@ -1350,9 +1349,9 @@ public final TimeSeriesCommandContext timeSeriesCommand() throws RecognitionExce try { enterOuterAlt(_localctx, 1); { - setState(286); + setState(284); match(DEV_TIME_SERIES); - setState(287); + setState(285); indexPatternAndMetadataFields(); } } @@ -1409,32 +1408,32 @@ public final IndexPatternAndMetadataFieldsContext indexPatternAndMetadataFields( int _alt; enterOuterAlt(_localctx, 1); { - setState(289); + setState(287); indexPattern(); - setState(294); + setState(292); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,9,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(290); + setState(288); match(COMMA); - setState(291); + setState(289); indexPattern(); } } } - setState(296); + setState(294); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,9,_ctx); } - setState(298); + setState(296); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,10,_ctx) ) { case 1: { - setState(297); + setState(295); metadata(); } break; @@ -1492,35 +1491,35 @@ public final IndexPatternContext indexPattern() throws RecognitionException { IndexPatternContext _localctx = new IndexPatternContext(_ctx, getState()); enterRule(_localctx, 30, RULE_indexPattern); try { - setState(309); + setState(307); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,11,_ctx) ) { case 1: enterOuterAlt(_localctx, 1); { - setState(300); + setState(298); clusterString(); - setState(301); + setState(299); match(COLON); - setState(302); + setState(300); unquotedIndexString(); } break; case 2: enterOuterAlt(_localctx, 2); { - setState(304); + setState(302); unquotedIndexString(); - setState(305); + setState(303); match(CAST_OP); - setState(306); + setState(304); selectorString(); } break; case 3: enterOuterAlt(_localctx, 3); { - setState(308); + setState(306); indexString(); } break; @@ -1566,7 +1565,7 @@ public final ClusterStringContext clusterString() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(311); + setState(309); match(UNQUOTED_SOURCE); } } @@ -1610,7 +1609,7 @@ public final SelectorStringContext selectorString() throws RecognitionException try { enterOuterAlt(_localctx, 1); { - setState(313); + setState(311); match(UNQUOTED_SOURCE); } } @@ -1654,7 +1653,7 @@ public final UnquotedIndexStringContext unquotedIndexString() throws Recognition try { enterOuterAlt(_localctx, 1); { - setState(315); + setState(313); match(UNQUOTED_SOURCE); } } @@ -1700,7 +1699,7 @@ public final IndexStringContext indexString() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(317); + setState(315); _la = _input.LA(1); if ( !(_la==QUOTED_STRING || _la==UNQUOTED_SOURCE) ) { _errHandler.recoverInline(this); @@ -1761,25 +1760,25 @@ public final MetadataContext metadata() throws RecognitionException { int _alt; enterOuterAlt(_localctx, 1); { - setState(319); + setState(317); match(METADATA); - setState(320); + setState(318); match(UNQUOTED_SOURCE); - setState(325); + setState(323); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,12,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(321); + setState(319); match(COMMA); - setState(322); + setState(320); match(UNQUOTED_SOURCE); } } } - setState(327); + setState(325); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,12,_ctx); } @@ -1828,9 +1827,9 @@ public final EvalCommandContext evalCommand() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(328); + setState(326); match(EVAL); - setState(329); + setState(327); fields(); } } @@ -1883,26 +1882,26 @@ public final StatsCommandContext statsCommand() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(331); + setState(329); match(STATS); - setState(333); + setState(331); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,13,_ctx) ) { case 1: { - setState(332); + setState(330); ((StatsCommandContext)_localctx).stats = aggFields(); } break; } - setState(337); + setState(335); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,14,_ctx) ) { case 1: { - setState(335); + setState(333); match(BY); - setState(336); + setState(334); ((StatsCommandContext)_localctx).grouping = fields(); } break; @@ -1959,23 +1958,23 @@ public final AggFieldsContext aggFields() throws RecognitionException { int _alt; enterOuterAlt(_localctx, 1); { - setState(339); + setState(337); aggField(); - setState(344); + setState(342); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,15,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(340); + setState(338); match(COMMA); - setState(341); + setState(339); aggField(); } } } - setState(346); + setState(344); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,15,_ctx); } @@ -2027,16 +2026,16 @@ public final AggFieldContext aggField() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(347); + setState(345); field(); - setState(350); + setState(348); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,16,_ctx) ) { case 1: { - setState(348); + setState(346); match(WHERE); - setState(349); + setState(347); booleanExpression(0); } break; @@ -2096,42 +2095,42 @@ public final QualifiedNameContext qualifiedName() throws RecognitionException { enterRule(_localctx, 50, RULE_qualifiedName); int _la; try { - setState(364); + setState(362); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,18,_ctx) ) { case 1: enterOuterAlt(_localctx, 1); { - setState(352); + setState(350); if (!(this.isDevVersion())) throw new FailedPredicateException(this, "this.isDevVersion()"); - setState(353); + setState(351); match(OPENING_BRACKET); - setState(355); + setState(353); _errHandler.sync(this); _la = _input.LA(1); if (_la==UNQUOTED_IDENTIFIER) { { - setState(354); + setState(352); ((QualifiedNameContext)_localctx).qualifier = match(UNQUOTED_IDENTIFIER); } } - setState(357); + setState(355); match(CLOSING_BRACKET); - setState(358); + setState(356); match(DOT); - setState(359); + setState(357); match(OPENING_BRACKET); - setState(360); + setState(358); ((QualifiedNameContext)_localctx).name = fieldName(); - setState(361); + setState(359); match(CLOSING_BRACKET); } break; case 2: enterOuterAlt(_localctx, 2); { - setState(363); + setState(361); ((QualifiedNameContext)_localctx).name = fieldName(); } break; @@ -2187,23 +2186,23 @@ public final FieldNameContext fieldName() throws RecognitionException { int _alt; enterOuterAlt(_localctx, 1); { - setState(366); + setState(364); identifierOrParameter(); - setState(371); + setState(369); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,19,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(367); + setState(365); match(DOT); - setState(368); + setState(366); identifierOrParameter(); } } } - setState(373); + setState(371); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,19,_ctx); } @@ -2262,42 +2261,42 @@ public final QualifiedNamePatternContext qualifiedNamePattern() throws Recogniti enterRule(_localctx, 54, RULE_qualifiedNamePattern); int _la; try { - setState(386); + setState(384); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,21,_ctx) ) { case 1: enterOuterAlt(_localctx, 1); { - setState(374); + setState(372); if (!(this.isDevVersion())) throw new FailedPredicateException(this, "this.isDevVersion()"); - setState(375); + setState(373); match(OPENING_BRACKET); - setState(377); + setState(375); _errHandler.sync(this); _la = _input.LA(1); if (_la==ID_PATTERN) { { - setState(376); + setState(374); ((QualifiedNamePatternContext)_localctx).qualifier = match(ID_PATTERN); } } - setState(379); + setState(377); match(CLOSING_BRACKET); - setState(380); + setState(378); match(DOT); - setState(381); + setState(379); match(OPENING_BRACKET); - setState(382); + setState(380); ((QualifiedNamePatternContext)_localctx).name = fieldNamePattern(); - setState(383); + setState(381); match(CLOSING_BRACKET); } break; case 2: enterOuterAlt(_localctx, 2); { - setState(385); + setState(383); ((QualifiedNamePatternContext)_localctx).name = fieldNamePattern(); } break; @@ -2354,23 +2353,23 @@ public final FieldNamePatternContext fieldNamePattern() throws RecognitionExcept enterOuterAlt(_localctx, 1); { { - setState(388); + setState(386); identifierPattern(); - setState(393); + setState(391); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,22,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(389); + setState(387); match(DOT); - setState(390); + setState(388); identifierPattern(); } } } - setState(395); + setState(393); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,22,_ctx); } @@ -2427,23 +2426,23 @@ public final QualifiedNamePatternsContext qualifiedNamePatterns() throws Recogni int _alt; enterOuterAlt(_localctx, 1); { - setState(396); + setState(394); qualifiedNamePattern(); - setState(401); + setState(399); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,23,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(397); + setState(395); match(COMMA); - setState(398); + setState(396); qualifiedNamePattern(); } } } - setState(403); + setState(401); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,23,_ctx); } @@ -2491,7 +2490,7 @@ public final IdentifierContext identifier() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(404); + setState(402); _la = _input.LA(1); if ( !(_la==UNQUOTED_IDENTIFIER || _la==QUOTED_IDENTIFIER) ) { _errHandler.recoverInline(this); @@ -2547,13 +2546,13 @@ public final IdentifierPatternContext identifierPattern() throws RecognitionExce IdentifierPatternContext _localctx = new IdentifierPatternContext(_ctx, getState()); enterRule(_localctx, 62, RULE_identifierPattern); try { - setState(409); + setState(407); _errHandler.sync(this); switch (_input.LA(1)) { case ID_PATTERN: enterOuterAlt(_localctx, 1); { - setState(406); + setState(404); match(ID_PATTERN); } break; @@ -2561,7 +2560,7 @@ public final IdentifierPatternContext identifierPattern() throws RecognitionExce case NAMED_OR_POSITIONAL_PARAM: enterOuterAlt(_localctx, 2); { - setState(407); + setState(405); parameter(); } break; @@ -2569,7 +2568,7 @@ public final IdentifierPatternContext identifierPattern() throws RecognitionExce case NAMED_OR_POSITIONAL_DOUBLE_PARAMS: enterOuterAlt(_localctx, 3); { - setState(408); + setState(406); doubleParameter(); } break; @@ -2645,14 +2644,14 @@ public final ParameterContext parameter() throws RecognitionException { ParameterContext _localctx = new ParameterContext(_ctx, getState()); enterRule(_localctx, 64, RULE_parameter); try { - setState(413); + setState(411); _errHandler.sync(this); switch (_input.LA(1)) { case PARAM: _localctx = new InputParamContext(_localctx); enterOuterAlt(_localctx, 1); { - setState(411); + setState(409); match(PARAM); } break; @@ -2660,7 +2659,7 @@ public final ParameterContext parameter() throws RecognitionException { _localctx = new InputNamedOrPositionalParamContext(_localctx); enterOuterAlt(_localctx, 2); { - setState(412); + setState(410); match(NAMED_OR_POSITIONAL_PARAM); } break; @@ -2736,14 +2735,14 @@ public final DoubleParameterContext doubleParameter() throws RecognitionExceptio DoubleParameterContext _localctx = new DoubleParameterContext(_ctx, getState()); enterRule(_localctx, 66, RULE_doubleParameter); try { - setState(417); + setState(415); _errHandler.sync(this); switch (_input.LA(1)) { case DOUBLE_PARAMS: _localctx = new InputDoubleParamsContext(_localctx); enterOuterAlt(_localctx, 1); { - setState(415); + setState(413); match(DOUBLE_PARAMS); } break; @@ -2751,7 +2750,7 @@ public final DoubleParameterContext doubleParameter() throws RecognitionExceptio _localctx = new InputNamedOrPositionalDoubleParamsContext(_localctx); enterOuterAlt(_localctx, 2); { - setState(416); + setState(414); match(NAMED_OR_POSITIONAL_DOUBLE_PARAMS); } break; @@ -2805,14 +2804,14 @@ public final IdentifierOrParameterContext identifierOrParameter() throws Recogni IdentifierOrParameterContext _localctx = new IdentifierOrParameterContext(_ctx, getState()); enterRule(_localctx, 68, RULE_identifierOrParameter); try { - setState(422); + setState(420); _errHandler.sync(this); switch (_input.LA(1)) { case UNQUOTED_IDENTIFIER: case QUOTED_IDENTIFIER: enterOuterAlt(_localctx, 1); { - setState(419); + setState(417); identifier(); } break; @@ -2820,7 +2819,7 @@ public final IdentifierOrParameterContext identifierOrParameter() throws Recogni case NAMED_OR_POSITIONAL_PARAM: enterOuterAlt(_localctx, 2); { - setState(420); + setState(418); parameter(); } break; @@ -2828,7 +2827,7 @@ public final IdentifierOrParameterContext identifierOrParameter() throws Recogni case NAMED_OR_POSITIONAL_DOUBLE_PARAMS: enterOuterAlt(_localctx, 3); { - setState(421); + setState(419); doubleParameter(); } break; @@ -2879,9 +2878,9 @@ public final LimitCommandContext limitCommand() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(424); + setState(422); match(LIMIT); - setState(425); + setState(423); constant(); } } @@ -2936,25 +2935,25 @@ public final SortCommandContext sortCommand() throws RecognitionException { int _alt; enterOuterAlt(_localctx, 1); { - setState(427); + setState(425); match(SORT); - setState(428); + setState(426); orderExpression(); - setState(433); + setState(431); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,28,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(429); + setState(427); match(COMMA); - setState(430); + setState(428); orderExpression(); } } } - setState(435); + setState(433); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,28,_ctx); } @@ -3010,14 +3009,14 @@ public final OrderExpressionContext orderExpression() throws RecognitionExceptio try { enterOuterAlt(_localctx, 1); { - setState(436); + setState(434); booleanExpression(0); - setState(438); + setState(436); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,29,_ctx) ) { case 1: { - setState(437); + setState(435); ((OrderExpressionContext)_localctx).ordering = _input.LT(1); _la = _input.LA(1); if ( !(_la==ASC || _la==DESC) ) { @@ -3031,14 +3030,14 @@ public final OrderExpressionContext orderExpression() throws RecognitionExceptio } break; } - setState(442); + setState(440); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,30,_ctx) ) { case 1: { - setState(440); + setState(438); match(NULLS); - setState(441); + setState(439); ((OrderExpressionContext)_localctx).nullOrdering = _input.LT(1); _la = _input.LA(1); if ( !(_la==FIRST || _la==LAST) ) { @@ -3097,9 +3096,9 @@ public final KeepCommandContext keepCommand() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(444); + setState(442); match(KEEP); - setState(445); + setState(443); qualifiedNamePatterns(); } } @@ -3146,9 +3145,9 @@ public final DropCommandContext dropCommand() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(447); + setState(445); match(DROP); - setState(448); + setState(446); qualifiedNamePatterns(); } } @@ -3203,25 +3202,25 @@ public final RenameCommandContext renameCommand() throws RecognitionException { int _alt; enterOuterAlt(_localctx, 1); { - setState(450); + setState(448); match(RENAME); - setState(451); + setState(449); renameClause(); - setState(456); + setState(454); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,31,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(452); + setState(450); match(COMMA); - setState(453); + setState(451); renameClause(); } } } - setState(458); + setState(456); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,31,_ctx); } @@ -3274,28 +3273,28 @@ public final RenameClauseContext renameClause() throws RecognitionException { RenameClauseContext _localctx = new RenameClauseContext(_ctx, getState()); enterRule(_localctx, 82, RULE_renameClause); try { - setState(467); + setState(465); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,32,_ctx) ) { case 1: enterOuterAlt(_localctx, 1); { - setState(459); + setState(457); ((RenameClauseContext)_localctx).oldName = qualifiedNamePattern(); - setState(460); + setState(458); match(AS); - setState(461); + setState(459); ((RenameClauseContext)_localctx).newName = qualifiedNamePattern(); } break; case 2: enterOuterAlt(_localctx, 2); { - setState(463); + setState(461); ((RenameClauseContext)_localctx).newName = qualifiedNamePattern(); - setState(464); + setState(462); match(ASSIGN); - setState(465); + setState(463); ((RenameClauseContext)_localctx).oldName = qualifiedNamePattern(); } break; @@ -3350,18 +3349,18 @@ public final DissectCommandContext dissectCommand() throws RecognitionException try { enterOuterAlt(_localctx, 1); { - setState(469); + setState(467); match(DISSECT); - setState(470); + setState(468); primaryExpression(0); - setState(471); + setState(469); string(); - setState(473); + setState(471); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,33,_ctx) ) { case 1: { - setState(472); + setState(470); dissectCommandOptions(); } break; @@ -3418,23 +3417,23 @@ public final DissectCommandOptionsContext dissectCommandOptions() throws Recogni int _alt; enterOuterAlt(_localctx, 1); { - setState(475); + setState(473); dissectCommandOption(); - setState(480); + setState(478); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,34,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(476); + setState(474); match(COMMA); - setState(477); + setState(475); dissectCommandOption(); } } } - setState(482); + setState(480); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,34,_ctx); } @@ -3486,11 +3485,11 @@ public final DissectCommandOptionContext dissectCommandOption() throws Recogniti try { enterOuterAlt(_localctx, 1); { - setState(483); + setState(481); identifier(); - setState(484); + setState(482); match(ASSIGN); - setState(485); + setState(483); constant(); } } @@ -3537,14 +3536,14 @@ public final CommandNamedParametersContext commandNamedParameters() throws Recog try { enterOuterAlt(_localctx, 1); { - setState(489); + setState(487); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,35,_ctx) ) { case 1: { - setState(487); + setState(485); match(WITH); - setState(488); + setState(486); mapExpression(); } break; @@ -3597,11 +3596,11 @@ public final GrokCommandContext grokCommand() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(491); + setState(489); match(GROK); - setState(492); + setState(490); primaryExpression(0); - setState(493); + setState(491); string(); } } @@ -3648,9 +3647,9 @@ public final MvExpandCommandContext mvExpandCommand() throws RecognitionExceptio try { enterOuterAlt(_localctx, 1); { - setState(495); + setState(493); match(MV_EXPAND); - setState(496); + setState(494); qualifiedName(); } } @@ -3697,9 +3696,9 @@ public final ExplainCommandContext explainCommand() throws RecognitionException try { enterOuterAlt(_localctx, 1); { - setState(498); + setState(496); match(DEV_EXPLAIN); - setState(499); + setState(497); subqueryExpression(); } } @@ -3747,11 +3746,11 @@ public final SubqueryExpressionContext subqueryExpression() throws RecognitionEx try { enterOuterAlt(_localctx, 1); { - setState(501); + setState(499); match(LP); - setState(502); + setState(500); query(0); - setState(503); + setState(501); match(RP); } } @@ -3808,9 +3807,9 @@ public final ShowCommandContext showCommand() throws RecognitionException { _localctx = new ShowInfoContext(_localctx); enterOuterAlt(_localctx, 1); { - setState(505); + setState(503); match(SHOW); - setState(506); + setState(504); match(INFO); } } @@ -3875,46 +3874,46 @@ public final EnrichCommandContext enrichCommand() throws RecognitionException { int _alt; enterOuterAlt(_localctx, 1); { - setState(508); + setState(506); match(ENRICH); - setState(509); + setState(507); ((EnrichCommandContext)_localctx).policyName = enrichPolicyName(); - setState(512); + setState(510); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,36,_ctx) ) { case 1: { - setState(510); + setState(508); match(ON); - setState(511); + setState(509); ((EnrichCommandContext)_localctx).matchField = qualifiedNamePattern(); } break; } - setState(523); + setState(521); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,38,_ctx) ) { case 1: { - setState(514); + setState(512); match(WITH); - setState(515); + setState(513); enrichWithClause(); - setState(520); + setState(518); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,37,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(516); + setState(514); match(COMMA); - setState(517); + setState(515); enrichWithClause(); } } } - setState(522); + setState(520); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,37,_ctx); } @@ -3965,7 +3964,7 @@ public final EnrichPolicyNameContext enrichPolicyName() throws RecognitionExcept try { enterOuterAlt(_localctx, 1); { - setState(525); + setState(523); _la = _input.LA(1); if ( !(_la==ENRICH_POLICY_NAME || _la==QUOTED_STRING) ) { _errHandler.recoverInline(this); @@ -4025,19 +4024,19 @@ public final EnrichWithClauseContext enrichWithClause() throws RecognitionExcept try { enterOuterAlt(_localctx, 1); { - setState(530); + setState(528); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,39,_ctx) ) { case 1: { - setState(527); + setState(525); ((EnrichWithClauseContext)_localctx).newName = qualifiedNamePattern(); - setState(528); + setState(526); match(ASSIGN); } break; } - setState(532); + setState(530); ((EnrichWithClauseContext)_localctx).enrichField = qualifiedNamePattern(); } } @@ -4085,9 +4084,9 @@ public final SampleCommandContext sampleCommand() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(534); + setState(532); match(SAMPLE); - setState(535); + setState(533); ((SampleCommandContext)_localctx).probability = constant(); } } @@ -4144,34 +4143,34 @@ public final ChangePointCommandContext changePointCommand() throws RecognitionEx try { enterOuterAlt(_localctx, 1); { - setState(537); + setState(535); match(CHANGE_POINT); - setState(538); + setState(536); ((ChangePointCommandContext)_localctx).value = qualifiedName(); - setState(541); + setState(539); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,40,_ctx) ) { case 1: { - setState(539); + setState(537); match(ON); - setState(540); + setState(538); ((ChangePointCommandContext)_localctx).key = qualifiedName(); } break; } - setState(548); + setState(546); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,41,_ctx) ) { case 1: { - setState(543); + setState(541); match(AS); - setState(544); + setState(542); ((ChangePointCommandContext)_localctx).targetType = qualifiedName(); - setState(545); + setState(543); match(COMMA); - setState(546); + setState(544); ((ChangePointCommandContext)_localctx).targetPvalue = qualifiedName(); } break; @@ -4221,9 +4220,9 @@ public final ForkCommandContext forkCommand() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(550); + setState(548); match(FORK); - setState(551); + setState(549); forkSubQueries(); } } @@ -4273,7 +4272,7 @@ public final ForkSubQueriesContext forkSubQueries() throws RecognitionException int _alt; enterOuterAlt(_localctx, 1); { - setState(554); + setState(552); _errHandler.sync(this); _alt = 1; do { @@ -4281,7 +4280,7 @@ public final ForkSubQueriesContext forkSubQueries() throws RecognitionException case 1: { { - setState(553); + setState(551); forkSubQuery(); } } @@ -4289,7 +4288,7 @@ public final ForkSubQueriesContext forkSubQueries() throws RecognitionException default: throw new NoViableAltException(this); } - setState(556); + setState(554); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,42,_ctx); } while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ); @@ -4339,11 +4338,11 @@ public final ForkSubQueryContext forkSubQuery() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(558); + setState(556); match(LP); - setState(559); + setState(557); forkSubQueryCommand(0); - setState(560); + setState(558); match(RP); } } @@ -4439,11 +4438,11 @@ private ForkSubQueryCommandContext forkSubQueryCommand(int _p) throws Recognitio _ctx = _localctx; _prevctx = _localctx; - setState(563); + setState(561); forkSubQueryProcessingCommand(); } _ctx.stop = _input.LT(-1); - setState(570); + setState(568); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,43,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { @@ -4454,16 +4453,16 @@ private ForkSubQueryCommandContext forkSubQueryCommand(int _p) throws Recognitio { _localctx = new CompositeForkSubQueryContext(new ForkSubQueryCommandContext(_parentctx, _parentState)); pushNewRecursionContext(_localctx, _startState, RULE_forkSubQueryCommand); - setState(565); + setState(563); if (!(precpred(_ctx, 1))) throw new FailedPredicateException(this, "precpred(_ctx, 1)"); - setState(566); + setState(564); match(PIPE); - setState(567); + setState(565); forkSubQueryProcessingCommand(); } } } - setState(572); + setState(570); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,43,_ctx); } @@ -4511,7 +4510,7 @@ public final ForkSubQueryProcessingCommandContext forkSubQueryProcessingCommand( try { enterOuterAlt(_localctx, 1); { - setState(573); + setState(571); processingCommand(); } } @@ -4571,27 +4570,27 @@ public final RerankCommandContext rerankCommand() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(575); + setState(573); match(RERANK); - setState(579); + setState(577); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,44,_ctx) ) { case 1: { - setState(576); + setState(574); ((RerankCommandContext)_localctx).targetField = qualifiedName(); - setState(577); + setState(575); match(ASSIGN); } break; } - setState(581); + setState(579); ((RerankCommandContext)_localctx).queryText = constant(); - setState(582); + setState(580); match(ON); - setState(583); + setState(581); rerankFields(); - setState(584); + setState(582); commandNamedParameters(); } } @@ -4647,23 +4646,23 @@ public final CompletionCommandContext completionCommand() throws RecognitionExce try { enterOuterAlt(_localctx, 1); { - setState(586); + setState(584); match(COMPLETION); - setState(590); + setState(588); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,45,_ctx) ) { case 1: { - setState(587); + setState(585); ((CompletionCommandContext)_localctx).targetField = qualifiedName(); - setState(588); + setState(586); match(ASSIGN); } break; } - setState(592); + setState(590); ((CompletionCommandContext)_localctx).prompt = primaryExpression(0); - setState(593); + setState(591); commandNamedParameters(); } } @@ -4716,13 +4715,13 @@ public final LookupCommandContext lookupCommand() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(595); + setState(593); match(DEV_LOOKUP); - setState(596); + setState(594); ((LookupCommandContext)_localctx).tableName = indexPattern(); - setState(597); + setState(595); match(ON); - setState(598); + setState(596); ((LookupCommandContext)_localctx).matchFields = qualifiedNamePatterns(); } } @@ -4775,18 +4774,18 @@ public final InlinestatsCommandContext inlinestatsCommand() throws RecognitionEx try { enterOuterAlt(_localctx, 1); { - setState(600); + setState(598); match(DEV_INLINESTATS); - setState(601); + setState(599); ((InlinestatsCommandContext)_localctx).stats = aggFields(); - setState(604); + setState(602); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,46,_ctx) ) { case 1: { - setState(602); + setState(600); match(BY); - setState(603); + setState(601); ((InlinestatsCommandContext)_localctx).grouping = fields(); } break; @@ -4836,9 +4835,9 @@ public final InsistCommandContext insistCommand() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(606); + setState(604); match(DEV_INSIST); - setState(607); + setState(605); qualifiedNamePatterns(); } } @@ -4890,19 +4889,19 @@ public final FuseCommandContext fuseCommand() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(609); + setState(607); match(DEV_FUSE); - setState(611); + setState(609); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,47,_ctx) ) { case 1: { - setState(610); + setState(608); ((FuseCommandContext)_localctx).fuseType = identifier(); } break; } - setState(613); + setState(611); ((FuseCommandContext)_localctx).fuseOptions = commandNamedParameters(); } } @@ -4950,11 +4949,11 @@ public final SetCommandContext setCommand() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(615); + setState(613); match(SET); - setState(616); + setState(614); setField(); - setState(617); + setState(615); match(SEMICOLON); } } @@ -5004,11 +5003,11 @@ public final SetFieldContext setField() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(619); + setState(617); identifier(); - setState(620); + setState(618); match(ASSIGN); - setState(621); + setState(619); constant(); } } @@ -5224,7 +5223,7 @@ private BooleanExpressionContext booleanExpression(int _p) throws RecognitionExc int _alt; enterOuterAlt(_localctx, 1); { - setState(652); + setState(650); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,51,_ctx) ) { case 1: @@ -5233,9 +5232,9 @@ private BooleanExpressionContext booleanExpression(int _p) throws RecognitionExc _ctx = _localctx; _prevctx = _localctx; - setState(624); + setState(622); match(NOT); - setState(625); + setState(623); booleanExpression(8); } break; @@ -5244,7 +5243,7 @@ private BooleanExpressionContext booleanExpression(int _p) throws RecognitionExc _localctx = new BooleanDefaultContext(_localctx); _ctx = _localctx; _prevctx = _localctx; - setState(626); + setState(624); valueExpression(); } break; @@ -5253,7 +5252,7 @@ private BooleanExpressionContext booleanExpression(int _p) throws RecognitionExc _localctx = new RegexExpressionContext(_localctx); _ctx = _localctx; _prevctx = _localctx; - setState(627); + setState(625); regexBooleanExpression(); } break; @@ -5262,41 +5261,41 @@ private BooleanExpressionContext booleanExpression(int _p) throws RecognitionExc _localctx = new LogicalInContext(_localctx); _ctx = _localctx; _prevctx = _localctx; - setState(628); + setState(626); valueExpression(); - setState(630); + setState(628); _errHandler.sync(this); _la = _input.LA(1); if (_la==NOT) { { - setState(629); + setState(627); match(NOT); } } - setState(632); + setState(630); match(IN); - setState(633); + setState(631); match(LP); - setState(634); + setState(632); valueExpression(); - setState(639); + setState(637); _errHandler.sync(this); _la = _input.LA(1); while (_la==COMMA) { { { - setState(635); + setState(633); match(COMMA); - setState(636); + setState(634); valueExpression(); } } - setState(641); + setState(639); _errHandler.sync(this); _la = _input.LA(1); } - setState(642); + setState(640); match(RP); } break; @@ -5305,21 +5304,21 @@ private BooleanExpressionContext booleanExpression(int _p) throws RecognitionExc _localctx = new IsNullContext(_localctx); _ctx = _localctx; _prevctx = _localctx; - setState(644); + setState(642); valueExpression(); - setState(645); + setState(643); match(IS); - setState(647); + setState(645); _errHandler.sync(this); _la = _input.LA(1); if (_la==NOT) { { - setState(646); + setState(644); match(NOT); } } - setState(649); + setState(647); match(NULL); } break; @@ -5328,13 +5327,13 @@ private BooleanExpressionContext booleanExpression(int _p) throws RecognitionExc _localctx = new MatchExpressionContext(_localctx); _ctx = _localctx; _prevctx = _localctx; - setState(651); + setState(649); matchBooleanExpression(); } break; } _ctx.stop = _input.LT(-1); - setState(662); + setState(660); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,53,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { @@ -5342,7 +5341,7 @@ private BooleanExpressionContext booleanExpression(int _p) throws RecognitionExc if ( _parseListeners!=null ) triggerExitRuleEvent(); _prevctx = _localctx; { - setState(660); + setState(658); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,52,_ctx) ) { case 1: @@ -5350,11 +5349,11 @@ private BooleanExpressionContext booleanExpression(int _p) throws RecognitionExc _localctx = new LogicalBinaryContext(new BooleanExpressionContext(_parentctx, _parentState)); ((LogicalBinaryContext)_localctx).left = _prevctx; pushNewRecursionContext(_localctx, _startState, RULE_booleanExpression); - setState(654); + setState(652); if (!(precpred(_ctx, 5))) throw new FailedPredicateException(this, "precpred(_ctx, 5)"); - setState(655); + setState(653); ((LogicalBinaryContext)_localctx).operator = match(AND); - setState(656); + setState(654); ((LogicalBinaryContext)_localctx).right = booleanExpression(6); } break; @@ -5363,18 +5362,18 @@ private BooleanExpressionContext booleanExpression(int _p) throws RecognitionExc _localctx = new LogicalBinaryContext(new BooleanExpressionContext(_parentctx, _parentState)); ((LogicalBinaryContext)_localctx).left = _prevctx; pushNewRecursionContext(_localctx, _startState, RULE_booleanExpression); - setState(657); + setState(655); if (!(precpred(_ctx, 4))) throw new FailedPredicateException(this, "precpred(_ctx, 4)"); - setState(658); + setState(656); ((LogicalBinaryContext)_localctx).operator = match(OR); - setState(659); + setState(657); ((LogicalBinaryContext)_localctx).right = booleanExpression(5); } break; } } } - setState(664); + setState(662); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,53,_ctx); } @@ -5533,28 +5532,28 @@ public final RegexBooleanExpressionContext regexBooleanExpression() throws Recog enterRule(_localctx, 140, RULE_regexBooleanExpression); int _la; try { - setState(711); + setState(709); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,60,_ctx) ) { case 1: _localctx = new LikeExpressionContext(_localctx); enterOuterAlt(_localctx, 1); { - setState(665); + setState(663); valueExpression(); - setState(667); + setState(665); _errHandler.sync(this); _la = _input.LA(1); if (_la==NOT) { { - setState(666); + setState(664); match(NOT); } } - setState(669); + setState(667); match(LIKE); - setState(670); + setState(668); string(); } break; @@ -5562,21 +5561,21 @@ public final RegexBooleanExpressionContext regexBooleanExpression() throws Recog _localctx = new RlikeExpressionContext(_localctx); enterOuterAlt(_localctx, 2); { - setState(672); + setState(670); valueExpression(); - setState(674); + setState(672); _errHandler.sync(this); _la = _input.LA(1); if (_la==NOT) { { - setState(673); + setState(671); match(NOT); } } - setState(676); + setState(674); match(RLIKE); - setState(677); + setState(675); string(); } break; @@ -5584,41 +5583,41 @@ public final RegexBooleanExpressionContext regexBooleanExpression() throws Recog _localctx = new LikeListExpressionContext(_localctx); enterOuterAlt(_localctx, 3); { - setState(679); + setState(677); valueExpression(); - setState(681); + setState(679); _errHandler.sync(this); _la = _input.LA(1); if (_la==NOT) { { - setState(680); + setState(678); match(NOT); } } - setState(683); + setState(681); match(LIKE); - setState(684); + setState(682); match(LP); - setState(685); + setState(683); string(); - setState(690); + setState(688); _errHandler.sync(this); _la = _input.LA(1); while (_la==COMMA) { { { - setState(686); + setState(684); match(COMMA); - setState(687); + setState(685); string(); } } - setState(692); + setState(690); _errHandler.sync(this); _la = _input.LA(1); } - setState(693); + setState(691); match(RP); } break; @@ -5626,41 +5625,41 @@ public final RegexBooleanExpressionContext regexBooleanExpression() throws Recog _localctx = new RlikeListExpressionContext(_localctx); enterOuterAlt(_localctx, 4); { - setState(695); + setState(693); valueExpression(); - setState(697); + setState(695); _errHandler.sync(this); _la = _input.LA(1); if (_la==NOT) { { - setState(696); + setState(694); match(NOT); } } - setState(699); + setState(697); match(RLIKE); - setState(700); + setState(698); match(LP); - setState(701); + setState(699); string(); - setState(706); + setState(704); _errHandler.sync(this); _la = _input.LA(1); while (_la==COMMA) { { { - setState(702); + setState(700); match(COMMA); - setState(703); + setState(701); string(); } } - setState(708); + setState(706); _errHandler.sync(this); _la = _input.LA(1); } - setState(709); + setState(707); match(RP); } break; @@ -5720,23 +5719,23 @@ public final MatchBooleanExpressionContext matchBooleanExpression() throws Recog try { enterOuterAlt(_localctx, 1); { - setState(713); + setState(711); ((MatchBooleanExpressionContext)_localctx).fieldExp = qualifiedName(); - setState(716); + setState(714); _errHandler.sync(this); _la = _input.LA(1); if (_la==CAST_OP) { { - setState(714); + setState(712); match(CAST_OP); - setState(715); + setState(713); ((MatchBooleanExpressionContext)_localctx).fieldType = dataType(); } } - setState(718); + setState(716); match(COLON); - setState(719); + setState(717); ((MatchBooleanExpressionContext)_localctx).matchQuery = constant(); } } @@ -5820,14 +5819,14 @@ public final ValueExpressionContext valueExpression() throws RecognitionExceptio ValueExpressionContext _localctx = new ValueExpressionContext(_ctx, getState()); enterRule(_localctx, 144, RULE_valueExpression); try { - setState(726); + setState(724); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,62,_ctx) ) { case 1: _localctx = new ValueExpressionDefaultContext(_localctx); enterOuterAlt(_localctx, 1); { - setState(721); + setState(719); operatorExpression(0); } break; @@ -5835,11 +5834,11 @@ public final ValueExpressionContext valueExpression() throws RecognitionExceptio _localctx = new ComparisonContext(_localctx); enterOuterAlt(_localctx, 2); { - setState(722); + setState(720); ((ComparisonContext)_localctx).left = operatorExpression(0); - setState(723); + setState(721); comparisonOperator(); - setState(724); + setState(722); ((ComparisonContext)_localctx).right = operatorExpression(0); } break; @@ -5964,7 +5963,7 @@ private OperatorExpressionContext operatorExpression(int _p) throws RecognitionE int _alt; enterOuterAlt(_localctx, 1); { - setState(732); + setState(730); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,63,_ctx) ) { case 1: @@ -5973,7 +5972,7 @@ private OperatorExpressionContext operatorExpression(int _p) throws RecognitionE _ctx = _localctx; _prevctx = _localctx; - setState(729); + setState(727); primaryExpression(0); } break; @@ -5982,7 +5981,7 @@ private OperatorExpressionContext operatorExpression(int _p) throws RecognitionE _localctx = new ArithmeticUnaryContext(_localctx); _ctx = _localctx; _prevctx = _localctx; - setState(730); + setState(728); ((ArithmeticUnaryContext)_localctx).operator = _input.LT(1); _la = _input.LA(1); if ( !(_la==PLUS || _la==MINUS) ) { @@ -5993,13 +5992,13 @@ private OperatorExpressionContext operatorExpression(int _p) throws RecognitionE _errHandler.reportMatch(this); consume(); } - setState(731); + setState(729); operatorExpression(3); } break; } _ctx.stop = _input.LT(-1); - setState(742); + setState(740); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,65,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { @@ -6007,7 +6006,7 @@ private OperatorExpressionContext operatorExpression(int _p) throws RecognitionE if ( _parseListeners!=null ) triggerExitRuleEvent(); _prevctx = _localctx; { - setState(740); + setState(738); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,64,_ctx) ) { case 1: @@ -6015,9 +6014,9 @@ private OperatorExpressionContext operatorExpression(int _p) throws RecognitionE _localctx = new ArithmeticBinaryContext(new OperatorExpressionContext(_parentctx, _parentState)); ((ArithmeticBinaryContext)_localctx).left = _prevctx; pushNewRecursionContext(_localctx, _startState, RULE_operatorExpression); - setState(734); + setState(732); if (!(precpred(_ctx, 2))) throw new FailedPredicateException(this, "precpred(_ctx, 2)"); - setState(735); + setState(733); ((ArithmeticBinaryContext)_localctx).operator = _input.LT(1); _la = _input.LA(1); if ( !(((((_la - 87)) & ~0x3f) == 0 && ((1L << (_la - 87)) & 7L) != 0)) ) { @@ -6028,7 +6027,7 @@ private OperatorExpressionContext operatorExpression(int _p) throws RecognitionE _errHandler.reportMatch(this); consume(); } - setState(736); + setState(734); ((ArithmeticBinaryContext)_localctx).right = operatorExpression(3); } break; @@ -6037,9 +6036,9 @@ private OperatorExpressionContext operatorExpression(int _p) throws RecognitionE _localctx = new ArithmeticBinaryContext(new OperatorExpressionContext(_parentctx, _parentState)); ((ArithmeticBinaryContext)_localctx).left = _prevctx; pushNewRecursionContext(_localctx, _startState, RULE_operatorExpression); - setState(737); + setState(735); if (!(precpred(_ctx, 1))) throw new FailedPredicateException(this, "precpred(_ctx, 1)"); - setState(738); + setState(736); ((ArithmeticBinaryContext)_localctx).operator = _input.LT(1); _la = _input.LA(1); if ( !(_la==PLUS || _la==MINUS) ) { @@ -6050,14 +6049,14 @@ private OperatorExpressionContext operatorExpression(int _p) throws RecognitionE _errHandler.reportMatch(this); consume(); } - setState(739); + setState(737); ((ArithmeticBinaryContext)_localctx).right = operatorExpression(2); } break; } } } - setState(744); + setState(742); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,65,_ctx); } @@ -6215,7 +6214,7 @@ private PrimaryExpressionContext primaryExpression(int _p) throws RecognitionExc int _alt; enterOuterAlt(_localctx, 1); { - setState(753); + setState(751); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,66,_ctx) ) { case 1: @@ -6224,7 +6223,7 @@ private PrimaryExpressionContext primaryExpression(int _p) throws RecognitionExc _ctx = _localctx; _prevctx = _localctx; - setState(746); + setState(744); constant(); } break; @@ -6233,7 +6232,7 @@ private PrimaryExpressionContext primaryExpression(int _p) throws RecognitionExc _localctx = new DereferenceContext(_localctx); _ctx = _localctx; _prevctx = _localctx; - setState(747); + setState(745); qualifiedName(); } break; @@ -6242,7 +6241,7 @@ private PrimaryExpressionContext primaryExpression(int _p) throws RecognitionExc _localctx = new FunctionContext(_localctx); _ctx = _localctx; _prevctx = _localctx; - setState(748); + setState(746); functionExpression(); } break; @@ -6251,17 +6250,17 @@ private PrimaryExpressionContext primaryExpression(int _p) throws RecognitionExc _localctx = new ParenthesizedExpressionContext(_localctx); _ctx = _localctx; _prevctx = _localctx; - setState(749); + setState(747); match(LP); - setState(750); + setState(748); booleanExpression(0); - setState(751); + setState(749); match(RP); } break; } _ctx.stop = _input.LT(-1); - setState(760); + setState(758); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,67,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { @@ -6272,16 +6271,16 @@ private PrimaryExpressionContext primaryExpression(int _p) throws RecognitionExc { _localctx = new InlineCastContext(new PrimaryExpressionContext(_parentctx, _parentState)); pushNewRecursionContext(_localctx, _startState, RULE_primaryExpression); - setState(755); + setState(753); if (!(precpred(_ctx, 1))) throw new FailedPredicateException(this, "precpred(_ctx, 1)"); - setState(756); + setState(754); match(CAST_OP); - setState(757); + setState(755); dataType(); } } } - setState(762); + setState(760); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,67,_ctx); } @@ -6347,50 +6346,50 @@ public final FunctionExpressionContext functionExpression() throws RecognitionEx int _alt; enterOuterAlt(_localctx, 1); { - setState(763); + setState(761); functionName(); - setState(764); + setState(762); match(LP); - setState(778); + setState(776); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,70,_ctx) ) { case 1: { - setState(765); + setState(763); match(ASTERISK); } break; case 2: { { - setState(766); + setState(764); booleanExpression(0); - setState(771); + setState(769); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,68,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(767); + setState(765); match(COMMA); - setState(768); + setState(766); booleanExpression(0); } } } - setState(773); + setState(771); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,68,_ctx); } - setState(776); + setState(774); _errHandler.sync(this); _la = _input.LA(1); if (_la==COMMA) { { - setState(774); + setState(772); match(COMMA); - setState(775); + setState(773); mapExpression(); } } @@ -6399,7 +6398,7 @@ public final FunctionExpressionContext functionExpression() throws RecognitionEx } break; } - setState(780); + setState(778); match(RP); } } @@ -6445,7 +6444,7 @@ public final FunctionNameContext functionName() throws RecognitionException { FunctionNameContext _localctx = new FunctionNameContext(_ctx, getState()); enterRule(_localctx, 152, RULE_functionName); try { - setState(785); + setState(783); _errHandler.sync(this); switch (_input.LA(1)) { case PARAM: @@ -6456,21 +6455,21 @@ public final FunctionNameContext functionName() throws RecognitionException { case QUOTED_IDENTIFIER: enterOuterAlt(_localctx, 1); { - setState(782); + setState(780); identifierOrParameter(); } break; case FIRST: enterOuterAlt(_localctx, 2); { - setState(783); + setState(781); match(FIRST); } break; case LAST: enterOuterAlt(_localctx, 3); { - setState(784); + setState(782); match(LAST); } break; @@ -6530,35 +6529,35 @@ public final MapExpressionContext mapExpression() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(787); + setState(785); match(LEFT_BRACES); - setState(796); + setState(794); _errHandler.sync(this); _la = _input.LA(1); if (_la==QUOTED_STRING) { { - setState(788); + setState(786); entryExpression(); - setState(793); + setState(791); _errHandler.sync(this); _la = _input.LA(1); while (_la==COMMA) { { { - setState(789); + setState(787); match(COMMA); - setState(790); + setState(788); entryExpression(); } } - setState(795); + setState(793); _errHandler.sync(this); _la = _input.LA(1); } } } - setState(798); + setState(796); match(RIGHT_BRACES); } } @@ -6610,11 +6609,11 @@ public final EntryExpressionContext entryExpression() throws RecognitionExceptio try { enterOuterAlt(_localctx, 1); { - setState(800); + setState(798); ((EntryExpressionContext)_localctx).key = string(); - setState(801); + setState(799); match(COLON); - setState(802); + setState(800); ((EntryExpressionContext)_localctx).value = mapValue(); } } @@ -6661,7 +6660,7 @@ public final MapValueContext mapValue() throws RecognitionException { MapValueContext _localctx = new MapValueContext(_ctx, getState()); enterRule(_localctx, 158, RULE_mapValue); try { - setState(806); + setState(804); _errHandler.sync(this); switch (_input.LA(1)) { case QUOTED_STRING: @@ -6677,14 +6676,14 @@ public final MapValueContext mapValue() throws RecognitionException { case OPENING_BRACKET: enterOuterAlt(_localctx, 1); { - setState(804); + setState(802); constant(); } break; case LEFT_BRACES: enterOuterAlt(_localctx, 2); { - setState(805); + setState(803); mapExpression(); } break; @@ -6959,14 +6958,14 @@ public final ConstantContext constant() throws RecognitionException { enterRule(_localctx, 160, RULE_constant); int _la; try { - setState(850); + setState(848); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,78,_ctx) ) { case 1: _localctx = new NullLiteralContext(_localctx); enterOuterAlt(_localctx, 1); { - setState(808); + setState(806); match(NULL); } break; @@ -6974,9 +6973,9 @@ public final ConstantContext constant() throws RecognitionException { _localctx = new QualifiedIntegerLiteralContext(_localctx); enterOuterAlt(_localctx, 2); { - setState(809); + setState(807); integerValue(); - setState(810); + setState(808); match(UNQUOTED_IDENTIFIER); } break; @@ -6984,7 +6983,7 @@ public final ConstantContext constant() throws RecognitionException { _localctx = new DecimalLiteralContext(_localctx); enterOuterAlt(_localctx, 3); { - setState(812); + setState(810); decimalValue(); } break; @@ -6992,7 +6991,7 @@ public final ConstantContext constant() throws RecognitionException { _localctx = new IntegerLiteralContext(_localctx); enterOuterAlt(_localctx, 4); { - setState(813); + setState(811); integerValue(); } break; @@ -7000,7 +6999,7 @@ public final ConstantContext constant() throws RecognitionException { _localctx = new BooleanLiteralContext(_localctx); enterOuterAlt(_localctx, 5); { - setState(814); + setState(812); booleanValue(); } break; @@ -7008,7 +7007,7 @@ public final ConstantContext constant() throws RecognitionException { _localctx = new InputParameterContext(_localctx); enterOuterAlt(_localctx, 6); { - setState(815); + setState(813); parameter(); } break; @@ -7016,7 +7015,7 @@ public final ConstantContext constant() throws RecognitionException { _localctx = new StringLiteralContext(_localctx); enterOuterAlt(_localctx, 7); { - setState(816); + setState(814); string(); } break; @@ -7024,27 +7023,27 @@ public final ConstantContext constant() throws RecognitionException { _localctx = new NumericArrayLiteralContext(_localctx); enterOuterAlt(_localctx, 8); { - setState(817); + setState(815); match(OPENING_BRACKET); - setState(818); + setState(816); numericValue(); - setState(823); + setState(821); _errHandler.sync(this); _la = _input.LA(1); while (_la==COMMA) { { { - setState(819); + setState(817); match(COMMA); - setState(820); + setState(818); numericValue(); } } - setState(825); + setState(823); _errHandler.sync(this); _la = _input.LA(1); } - setState(826); + setState(824); match(CLOSING_BRACKET); } break; @@ -7052,27 +7051,27 @@ public final ConstantContext constant() throws RecognitionException { _localctx = new BooleanArrayLiteralContext(_localctx); enterOuterAlt(_localctx, 9); { - setState(828); + setState(826); match(OPENING_BRACKET); - setState(829); + setState(827); booleanValue(); - setState(834); + setState(832); _errHandler.sync(this); _la = _input.LA(1); while (_la==COMMA) { { { - setState(830); + setState(828); match(COMMA); - setState(831); + setState(829); booleanValue(); } } - setState(836); + setState(834); _errHandler.sync(this); _la = _input.LA(1); } - setState(837); + setState(835); match(CLOSING_BRACKET); } break; @@ -7080,27 +7079,27 @@ public final ConstantContext constant() throws RecognitionException { _localctx = new StringArrayLiteralContext(_localctx); enterOuterAlt(_localctx, 10); { - setState(839); + setState(837); match(OPENING_BRACKET); - setState(840); + setState(838); string(); - setState(845); + setState(843); _errHandler.sync(this); _la = _input.LA(1); while (_la==COMMA) { { { - setState(841); + setState(839); match(COMMA); - setState(842); + setState(840); string(); } } - setState(847); + setState(845); _errHandler.sync(this); _la = _input.LA(1); } - setState(848); + setState(846); match(CLOSING_BRACKET); } break; @@ -7148,7 +7147,7 @@ public final BooleanValueContext booleanValue() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(852); + setState(850); _la = _input.LA(1); if ( !(_la==FALSE || _la==TRUE) ) { _errHandler.recoverInline(this); @@ -7203,20 +7202,20 @@ public final NumericValueContext numericValue() throws RecognitionException { NumericValueContext _localctx = new NumericValueContext(_ctx, getState()); enterRule(_localctx, 164, RULE_numericValue); try { - setState(856); + setState(854); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,79,_ctx) ) { case 1: enterOuterAlt(_localctx, 1); { - setState(854); + setState(852); decimalValue(); } break; case 2: enterOuterAlt(_localctx, 2); { - setState(855); + setState(853); integerValue(); } break; @@ -7265,12 +7264,12 @@ public final DecimalValueContext decimalValue() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(859); + setState(857); _errHandler.sync(this); _la = _input.LA(1); if (_la==PLUS || _la==MINUS) { { - setState(858); + setState(856); _la = _input.LA(1); if ( !(_la==PLUS || _la==MINUS) ) { _errHandler.recoverInline(this); @@ -7283,7 +7282,7 @@ public final DecimalValueContext decimalValue() throws RecognitionException { } } - setState(861); + setState(859); match(DECIMAL_LITERAL); } } @@ -7330,12 +7329,12 @@ public final IntegerValueContext integerValue() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(864); + setState(862); _errHandler.sync(this); _la = _input.LA(1); if (_la==PLUS || _la==MINUS) { { - setState(863); + setState(861); _la = _input.LA(1); if ( !(_la==PLUS || _la==MINUS) ) { _errHandler.recoverInline(this); @@ -7348,7 +7347,7 @@ public final IntegerValueContext integerValue() throws RecognitionException { } } - setState(866); + setState(864); match(INTEGER_LITERAL); } } @@ -7392,7 +7391,7 @@ public final StringContext string() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(868); + setState(866); match(QUOTED_STRING); } } @@ -7442,7 +7441,7 @@ public final ComparisonOperatorContext comparisonOperator() throws RecognitionEx try { enterOuterAlt(_localctx, 1); { - setState(870); + setState(868); _la = _input.LA(1); if ( !(((((_la - 78)) & ~0x3f) == 0 && ((1L << (_la - 78)) & 125L) != 0)) ) { _errHandler.recoverInline(this); @@ -7505,7 +7504,7 @@ public final JoinCommandContext joinCommand() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(872); + setState(870); ((JoinCommandContext)_localctx).type = _input.LT(1); _la = _input.LA(1); if ( !((((_la) & ~0x3f) == 0 && ((1L << _la) & 109051904L) != 0)) ) { @@ -7516,11 +7515,11 @@ public final JoinCommandContext joinCommand() throws RecognitionException { _errHandler.reportMatch(this); consume(); } - setState(873); + setState(871); match(JOIN); - setState(874); + setState(872); joinTarget(); - setState(875); + setState(873); joinCondition(); } } @@ -7569,34 +7568,34 @@ public final JoinTargetContext joinTarget() throws RecognitionException { enterRule(_localctx, 176, RULE_joinTarget); int _la; try { - setState(885); + setState(883); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,83,_ctx) ) { case 1: enterOuterAlt(_localctx, 1); { - setState(877); + setState(875); if (!(this.isDevVersion())) throw new FailedPredicateException(this, "this.isDevVersion()"); - setState(878); + setState(876); ((JoinTargetContext)_localctx).index = indexPattern(); - setState(880); + setState(878); _errHandler.sync(this); _la = _input.LA(1); if (_la==AS) { { - setState(879); + setState(877); match(AS); } } - setState(882); + setState(880); ((JoinTargetContext)_localctx).qualifier = match(UNQUOTED_SOURCE); } break; case 2: enterOuterAlt(_localctx, 2); { - setState(884); + setState(882); ((JoinTargetContext)_localctx).index = indexPattern(); } break; @@ -7616,11 +7615,11 @@ public final JoinTargetContext joinTarget() throws RecognitionException { @SuppressWarnings("CheckReturnValue") public static class JoinConditionContext extends ParserRuleContext { public TerminalNode ON() { return getToken(EsqlBaseParser.ON, 0); } - public List joinPredicate() { - return getRuleContexts(JoinPredicateContext.class); + public List booleanExpression() { + return getRuleContexts(BooleanExpressionContext.class); } - public JoinPredicateContext joinPredicate(int i) { - return getRuleContext(JoinPredicateContext.class,i); + public BooleanExpressionContext booleanExpression(int i) { + return getRuleContext(BooleanExpressionContext.class,i); } public List COMMA() { return getTokens(EsqlBaseParser.COMMA); } public TerminalNode COMMA(int i) { @@ -7653,25 +7652,25 @@ public final JoinConditionContext joinCondition() throws RecognitionException { int _alt; enterOuterAlt(_localctx, 1); { - setState(887); + setState(885); match(ON); - setState(888); - joinPredicate(); - setState(893); + setState(886); + booleanExpression(0); + setState(891); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,84,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(889); + setState(887); match(COMMA); - setState(890); - joinPredicate(); + setState(888); + booleanExpression(0); } } } - setState(895); + setState(893); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,84,_ctx); } @@ -7688,52 +7687,6 @@ public final JoinConditionContext joinCondition() throws RecognitionException { return _localctx; } - @SuppressWarnings("CheckReturnValue") - public static class JoinPredicateContext extends ParserRuleContext { - public ValueExpressionContext valueExpression() { - return getRuleContext(ValueExpressionContext.class,0); - } - @SuppressWarnings("this-escape") - public JoinPredicateContext(ParserRuleContext parent, int invokingState) { - super(parent, invokingState); - } - @Override public int getRuleIndex() { return RULE_joinPredicate; } - @Override - public void enterRule(ParseTreeListener listener) { - if ( listener instanceof EsqlBaseParserListener ) ((EsqlBaseParserListener)listener).enterJoinPredicate(this); - } - @Override - public void exitRule(ParseTreeListener listener) { - if ( listener instanceof EsqlBaseParserListener ) ((EsqlBaseParserListener)listener).exitJoinPredicate(this); - } - @Override - public T accept(ParseTreeVisitor visitor) { - if ( visitor instanceof EsqlBaseParserVisitor ) return ((EsqlBaseParserVisitor)visitor).visitJoinPredicate(this); - else return visitor.visitChildren(this); - } - } - - public final JoinPredicateContext joinPredicate() throws RecognitionException { - JoinPredicateContext _localctx = new JoinPredicateContext(_ctx, getState()); - enterRule(_localctx, 180, RULE_joinPredicate); - try { - enterOuterAlt(_localctx, 1); - { - setState(896); - valueExpression(); - } - } - catch (RecognitionException re) { - _localctx.exception = re; - _errHandler.reportError(this, re); - _errHandler.recover(this, re); - } - finally { - exitRule(); - } - return _localctx; - } - public boolean sempred(RuleContext _localctx, int ruleIndex, int predIndex) { switch (ruleIndex) { case 0: @@ -7852,7 +7805,7 @@ private boolean joinTarget_sempred(JoinTargetContext _localctx, int predIndex) { } public static final String _serializedATN = - "\u0004\u0001\u008f\u0383\u0002\u0000\u0007\u0000\u0002\u0001\u0007\u0001"+ + "\u0004\u0001\u008f\u037f\u0002\u0000\u0007\u0000\u0002\u0001\u0007\u0001"+ "\u0002\u0002\u0007\u0002\u0002\u0003\u0007\u0003\u0002\u0004\u0007\u0004"+ "\u0002\u0005\u0007\u0005\u0002\u0006\u0007\u0006\u0002\u0007\u0007\u0007"+ "\u0002\b\u0007\b\u0002\t\u0007\t\u0002\n\u0007\n\u0002\u000b\u0007\u000b"+ @@ -7874,539 +7827,538 @@ private boolean joinTarget_sempred(JoinTargetContext _localctx, int predIndex) { "J\u0002K\u0007K\u0002L\u0007L\u0002M\u0007M\u0002N\u0007N\u0002O\u0007"+ "O\u0002P\u0007P\u0002Q\u0007Q\u0002R\u0007R\u0002S\u0007S\u0002T\u0007"+ "T\u0002U\u0007U\u0002V\u0007V\u0002W\u0007W\u0002X\u0007X\u0002Y\u0007"+ - "Y\u0002Z\u0007Z\u0001\u0000\u0001\u0000\u0004\u0000\u00b9\b\u0000\u000b"+ - "\u0000\f\u0000\u00ba\u0001\u0000\u0001\u0000\u0001\u0000\u0001\u0000\u0001"+ - "\u0000\u0001\u0000\u0003\u0000\u00c3\b\u0000\u0001\u0001\u0001\u0001\u0001"+ - "\u0001\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0001"+ - "\u0002\u0005\u0002\u00ce\b\u0002\n\u0002\f\u0002\u00d1\t\u0002\u0001\u0003"+ - "\u0001\u0003\u0001\u0003\u0001\u0003\u0001\u0003\u0001\u0003\u0001\u0003"+ - "\u0003\u0003\u00da\b\u0003\u0001\u0004\u0001\u0004\u0001\u0004\u0001\u0004"+ + "Y\u0001\u0000\u0001\u0000\u0004\u0000\u00b7\b\u0000\u000b\u0000\f\u0000"+ + "\u00b8\u0001\u0000\u0001\u0000\u0001\u0000\u0001\u0000\u0001\u0000\u0001"+ + "\u0000\u0003\u0000\u00c1\b\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001"+ + "\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0005"+ + "\u0002\u00cc\b\u0002\n\u0002\f\u0002\u00cf\t\u0002\u0001\u0003\u0001\u0003"+ + "\u0001\u0003\u0001\u0003\u0001\u0003\u0001\u0003\u0001\u0003\u0003\u0003"+ + "\u00d8\b\u0003\u0001\u0004\u0001\u0004\u0001\u0004\u0001\u0004\u0001\u0004"+ "\u0001\u0004\u0001\u0004\u0001\u0004\u0001\u0004\u0001\u0004\u0001\u0004"+ "\u0001\u0004\u0001\u0004\u0001\u0004\u0001\u0004\u0001\u0004\u0001\u0004"+ "\u0001\u0004\u0001\u0004\u0001\u0004\u0001\u0004\u0001\u0004\u0001\u0004"+ - "\u0001\u0004\u0001\u0004\u0001\u0004\u0001\u0004\u0003\u0004\u00f6\b\u0004"+ - "\u0001\u0005\u0001\u0005\u0001\u0005\u0001\u0006\u0001\u0006\u0001\u0007"+ - "\u0001\u0007\u0001\u0007\u0001\b\u0001\b\u0001\b\u0005\b\u0103\b\b\n\b"+ - "\f\b\u0106\t\b\u0001\t\u0001\t\u0001\t\u0003\t\u010b\b\t\u0001\t\u0001"+ - "\t\u0001\n\u0001\n\u0001\n\u0005\n\u0112\b\n\n\n\f\n\u0115\t\n\u0001\u000b"+ - "\u0001\u000b\u0001\u000b\u0003\u000b\u011a\b\u000b\u0001\f\u0001\f\u0001"+ - "\f\u0001\r\u0001\r\u0001\r\u0001\u000e\u0001\u000e\u0001\u000e\u0005\u000e"+ - "\u0125\b\u000e\n\u000e\f\u000e\u0128\t\u000e\u0001\u000e\u0003\u000e\u012b"+ - "\b\u000e\u0001\u000f\u0001\u000f\u0001\u000f\u0001\u000f\u0001\u000f\u0001"+ - "\u000f\u0001\u000f\u0001\u000f\u0001\u000f\u0003\u000f\u0136\b\u000f\u0001"+ + "\u0001\u0004\u0001\u0004\u0001\u0004\u0003\u0004\u00f4\b\u0004\u0001\u0005"+ + "\u0001\u0005\u0001\u0005\u0001\u0006\u0001\u0006\u0001\u0007\u0001\u0007"+ + "\u0001\u0007\u0001\b\u0001\b\u0001\b\u0005\b\u0101\b\b\n\b\f\b\u0104\t"+ + "\b\u0001\t\u0001\t\u0001\t\u0003\t\u0109\b\t\u0001\t\u0001\t\u0001\n\u0001"+ + "\n\u0001\n\u0005\n\u0110\b\n\n\n\f\n\u0113\t\n\u0001\u000b\u0001\u000b"+ + "\u0001\u000b\u0003\u000b\u0118\b\u000b\u0001\f\u0001\f\u0001\f\u0001\r"+ + "\u0001\r\u0001\r\u0001\u000e\u0001\u000e\u0001\u000e\u0005\u000e\u0123"+ + "\b\u000e\n\u000e\f\u000e\u0126\t\u000e\u0001\u000e\u0003\u000e\u0129\b"+ + "\u000e\u0001\u000f\u0001\u000f\u0001\u000f\u0001\u000f\u0001\u000f\u0001"+ + "\u000f\u0001\u000f\u0001\u000f\u0001\u000f\u0003\u000f\u0134\b\u000f\u0001"+ "\u0010\u0001\u0010\u0001\u0011\u0001\u0011\u0001\u0012\u0001\u0012\u0001"+ "\u0013\u0001\u0013\u0001\u0014\u0001\u0014\u0001\u0014\u0001\u0014\u0005"+ - "\u0014\u0144\b\u0014\n\u0014\f\u0014\u0147\t\u0014\u0001\u0015\u0001\u0015"+ - "\u0001\u0015\u0001\u0016\u0001\u0016\u0003\u0016\u014e\b\u0016\u0001\u0016"+ - "\u0001\u0016\u0003\u0016\u0152\b\u0016\u0001\u0017\u0001\u0017\u0001\u0017"+ - "\u0005\u0017\u0157\b\u0017\n\u0017\f\u0017\u015a\t\u0017\u0001\u0018\u0001"+ - "\u0018\u0001\u0018\u0003\u0018\u015f\b\u0018\u0001\u0019\u0001\u0019\u0001"+ - "\u0019\u0003\u0019\u0164\b\u0019\u0001\u0019\u0001\u0019\u0001\u0019\u0001"+ - "\u0019\u0001\u0019\u0001\u0019\u0001\u0019\u0003\u0019\u016d\b\u0019\u0001"+ - "\u001a\u0001\u001a\u0001\u001a\u0005\u001a\u0172\b\u001a\n\u001a\f\u001a"+ - "\u0175\t\u001a\u0001\u001b\u0001\u001b\u0001\u001b\u0003\u001b\u017a\b"+ + "\u0014\u0142\b\u0014\n\u0014\f\u0014\u0145\t\u0014\u0001\u0015\u0001\u0015"+ + "\u0001\u0015\u0001\u0016\u0001\u0016\u0003\u0016\u014c\b\u0016\u0001\u0016"+ + "\u0001\u0016\u0003\u0016\u0150\b\u0016\u0001\u0017\u0001\u0017\u0001\u0017"+ + "\u0005\u0017\u0155\b\u0017\n\u0017\f\u0017\u0158\t\u0017\u0001\u0018\u0001"+ + "\u0018\u0001\u0018\u0003\u0018\u015d\b\u0018\u0001\u0019\u0001\u0019\u0001"+ + "\u0019\u0003\u0019\u0162\b\u0019\u0001\u0019\u0001\u0019\u0001\u0019\u0001"+ + "\u0019\u0001\u0019\u0001\u0019\u0001\u0019\u0003\u0019\u016b\b\u0019\u0001"+ + "\u001a\u0001\u001a\u0001\u001a\u0005\u001a\u0170\b\u001a\n\u001a\f\u001a"+ + "\u0173\t\u001a\u0001\u001b\u0001\u001b\u0001\u001b\u0003\u001b\u0178\b"+ "\u001b\u0001\u001b\u0001\u001b\u0001\u001b\u0001\u001b\u0001\u001b\u0001"+ - "\u001b\u0001\u001b\u0003\u001b\u0183\b\u001b\u0001\u001c\u0001\u001c\u0001"+ - "\u001c\u0005\u001c\u0188\b\u001c\n\u001c\f\u001c\u018b\t\u001c\u0001\u001d"+ - "\u0001\u001d\u0001\u001d\u0005\u001d\u0190\b\u001d\n\u001d\f\u001d\u0193"+ + "\u001b\u0001\u001b\u0003\u001b\u0181\b\u001b\u0001\u001c\u0001\u001c\u0001"+ + "\u001c\u0005\u001c\u0186\b\u001c\n\u001c\f\u001c\u0189\t\u001c\u0001\u001d"+ + "\u0001\u001d\u0001\u001d\u0005\u001d\u018e\b\u001d\n\u001d\f\u001d\u0191"+ "\t\u001d\u0001\u001e\u0001\u001e\u0001\u001f\u0001\u001f\u0001\u001f\u0003"+ - "\u001f\u019a\b\u001f\u0001 \u0001 \u0003 \u019e\b \u0001!\u0001!\u0003"+ - "!\u01a2\b!\u0001\"\u0001\"\u0001\"\u0003\"\u01a7\b\"\u0001#\u0001#\u0001"+ - "#\u0001$\u0001$\u0001$\u0001$\u0005$\u01b0\b$\n$\f$\u01b3\t$\u0001%\u0001"+ - "%\u0003%\u01b7\b%\u0001%\u0001%\u0003%\u01bb\b%\u0001&\u0001&\u0001&\u0001"+ - "\'\u0001\'\u0001\'\u0001(\u0001(\u0001(\u0001(\u0005(\u01c7\b(\n(\f(\u01ca"+ - "\t(\u0001)\u0001)\u0001)\u0001)\u0001)\u0001)\u0001)\u0001)\u0003)\u01d4"+ - "\b)\u0001*\u0001*\u0001*\u0001*\u0003*\u01da\b*\u0001+\u0001+\u0001+\u0005"+ - "+\u01df\b+\n+\f+\u01e2\t+\u0001,\u0001,\u0001,\u0001,\u0001-\u0001-\u0003"+ - "-\u01ea\b-\u0001.\u0001.\u0001.\u0001.\u0001/\u0001/\u0001/\u00010\u0001"+ + "\u001f\u0198\b\u001f\u0001 \u0001 \u0003 \u019c\b \u0001!\u0001!\u0003"+ + "!\u01a0\b!\u0001\"\u0001\"\u0001\"\u0003\"\u01a5\b\"\u0001#\u0001#\u0001"+ + "#\u0001$\u0001$\u0001$\u0001$\u0005$\u01ae\b$\n$\f$\u01b1\t$\u0001%\u0001"+ + "%\u0003%\u01b5\b%\u0001%\u0001%\u0003%\u01b9\b%\u0001&\u0001&\u0001&\u0001"+ + "\'\u0001\'\u0001\'\u0001(\u0001(\u0001(\u0001(\u0005(\u01c5\b(\n(\f(\u01c8"+ + "\t(\u0001)\u0001)\u0001)\u0001)\u0001)\u0001)\u0001)\u0001)\u0003)\u01d2"+ + "\b)\u0001*\u0001*\u0001*\u0001*\u0003*\u01d8\b*\u0001+\u0001+\u0001+\u0005"+ + "+\u01dd\b+\n+\f+\u01e0\t+\u0001,\u0001,\u0001,\u0001,\u0001-\u0001-\u0003"+ + "-\u01e8\b-\u0001.\u0001.\u0001.\u0001.\u0001/\u0001/\u0001/\u00010\u0001"+ "0\u00010\u00011\u00011\u00011\u00011\u00012\u00012\u00012\u00013\u0001"+ - "3\u00013\u00013\u00033\u0201\b3\u00013\u00013\u00013\u00013\u00053\u0207"+ - "\b3\n3\f3\u020a\t3\u00033\u020c\b3\u00014\u00014\u00015\u00015\u00015"+ - "\u00035\u0213\b5\u00015\u00015\u00016\u00016\u00016\u00017\u00017\u0001"+ - "7\u00017\u00037\u021e\b7\u00017\u00017\u00017\u00017\u00017\u00037\u0225"+ - "\b7\u00018\u00018\u00018\u00019\u00049\u022b\b9\u000b9\f9\u022c\u0001"+ + "3\u00013\u00013\u00033\u01ff\b3\u00013\u00013\u00013\u00013\u00053\u0205"+ + "\b3\n3\f3\u0208\t3\u00033\u020a\b3\u00014\u00014\u00015\u00015\u00015"+ + "\u00035\u0211\b5\u00015\u00015\u00016\u00016\u00016\u00017\u00017\u0001"+ + "7\u00017\u00037\u021c\b7\u00017\u00017\u00017\u00017\u00017\u00037\u0223"+ + "\b7\u00018\u00018\u00018\u00019\u00049\u0229\b9\u000b9\f9\u022a\u0001"+ ":\u0001:\u0001:\u0001:\u0001;\u0001;\u0001;\u0001;\u0001;\u0001;\u0005"+ - ";\u0239\b;\n;\f;\u023c\t;\u0001<\u0001<\u0001=\u0001=\u0001=\u0001=\u0003"+ - "=\u0244\b=\u0001=\u0001=\u0001=\u0001=\u0001=\u0001>\u0001>\u0001>\u0001"+ - ">\u0003>\u024f\b>\u0001>\u0001>\u0001>\u0001?\u0001?\u0001?\u0001?\u0001"+ - "?\u0001@\u0001@\u0001@\u0001@\u0003@\u025d\b@\u0001A\u0001A\u0001A\u0001"+ - "B\u0001B\u0003B\u0264\bB\u0001B\u0001B\u0001C\u0001C\u0001C\u0001C\u0001"+ + ";\u0237\b;\n;\f;\u023a\t;\u0001<\u0001<\u0001=\u0001=\u0001=\u0001=\u0003"+ + "=\u0242\b=\u0001=\u0001=\u0001=\u0001=\u0001=\u0001>\u0001>\u0001>\u0001"+ + ">\u0003>\u024d\b>\u0001>\u0001>\u0001>\u0001?\u0001?\u0001?\u0001?\u0001"+ + "?\u0001@\u0001@\u0001@\u0001@\u0003@\u025b\b@\u0001A\u0001A\u0001A\u0001"+ + "B\u0001B\u0003B\u0262\bB\u0001B\u0001B\u0001C\u0001C\u0001C\u0001C\u0001"+ "D\u0001D\u0001D\u0001D\u0001E\u0001E\u0001E\u0001E\u0001E\u0001E\u0001"+ - "E\u0003E\u0277\bE\u0001E\u0001E\u0001E\u0001E\u0001E\u0005E\u027e\bE\n"+ - "E\fE\u0281\tE\u0001E\u0001E\u0001E\u0001E\u0001E\u0003E\u0288\bE\u0001"+ - "E\u0001E\u0001E\u0003E\u028d\bE\u0001E\u0001E\u0001E\u0001E\u0001E\u0001"+ - "E\u0005E\u0295\bE\nE\fE\u0298\tE\u0001F\u0001F\u0003F\u029c\bF\u0001F"+ - "\u0001F\u0001F\u0001F\u0001F\u0003F\u02a3\bF\u0001F\u0001F\u0001F\u0001"+ - "F\u0001F\u0003F\u02aa\bF\u0001F\u0001F\u0001F\u0001F\u0001F\u0005F\u02b1"+ - "\bF\nF\fF\u02b4\tF\u0001F\u0001F\u0001F\u0001F\u0003F\u02ba\bF\u0001F"+ - "\u0001F\u0001F\u0001F\u0001F\u0005F\u02c1\bF\nF\fF\u02c4\tF\u0001F\u0001"+ - "F\u0003F\u02c8\bF\u0001G\u0001G\u0001G\u0003G\u02cd\bG\u0001G\u0001G\u0001"+ - "G\u0001H\u0001H\u0001H\u0001H\u0001H\u0003H\u02d7\bH\u0001I\u0001I\u0001"+ - "I\u0001I\u0003I\u02dd\bI\u0001I\u0001I\u0001I\u0001I\u0001I\u0001I\u0005"+ - "I\u02e5\bI\nI\fI\u02e8\tI\u0001J\u0001J\u0001J\u0001J\u0001J\u0001J\u0001"+ - "J\u0001J\u0003J\u02f2\bJ\u0001J\u0001J\u0001J\u0005J\u02f7\bJ\nJ\fJ\u02fa"+ - "\tJ\u0001K\u0001K\u0001K\u0001K\u0001K\u0001K\u0005K\u0302\bK\nK\fK\u0305"+ - "\tK\u0001K\u0001K\u0003K\u0309\bK\u0003K\u030b\bK\u0001K\u0001K\u0001"+ - "L\u0001L\u0001L\u0003L\u0312\bL\u0001M\u0001M\u0001M\u0001M\u0005M\u0318"+ - "\bM\nM\fM\u031b\tM\u0003M\u031d\bM\u0001M\u0001M\u0001N\u0001N\u0001N"+ - "\u0001N\u0001O\u0001O\u0003O\u0327\bO\u0001P\u0001P\u0001P\u0001P\u0001"+ - "P\u0001P\u0001P\u0001P\u0001P\u0001P\u0001P\u0001P\u0001P\u0005P\u0336"+ - "\bP\nP\fP\u0339\tP\u0001P\u0001P\u0001P\u0001P\u0001P\u0001P\u0005P\u0341"+ - "\bP\nP\fP\u0344\tP\u0001P\u0001P\u0001P\u0001P\u0001P\u0001P\u0005P\u034c"+ - "\bP\nP\fP\u034f\tP\u0001P\u0001P\u0003P\u0353\bP\u0001Q\u0001Q\u0001R"+ - "\u0001R\u0003R\u0359\bR\u0001S\u0003S\u035c\bS\u0001S\u0001S\u0001T\u0003"+ - "T\u0361\bT\u0001T\u0001T\u0001U\u0001U\u0001V\u0001V\u0001W\u0001W\u0001"+ - "W\u0001W\u0001W\u0001X\u0001X\u0001X\u0003X\u0371\bX\u0001X\u0001X\u0001"+ - "X\u0003X\u0376\bX\u0001Y\u0001Y\u0001Y\u0001Y\u0005Y\u037c\bY\nY\fY\u037f"+ - "\tY\u0001Z\u0001Z\u0001Z\u0000\u0005\u0004v\u008a\u0092\u0094[\u0000\u0002"+ - "\u0004\u0006\b\n\f\u000e\u0010\u0012\u0014\u0016\u0018\u001a\u001c\u001e"+ - " \"$&(*,.02468:<>@BDFHJLNPRTVXZ\\^`bdfhjlnprtvxz|~\u0080\u0082\u0084\u0086"+ - "\u0088\u008a\u008c\u008e\u0090\u0092\u0094\u0096\u0098\u009a\u009c\u009e"+ - "\u00a0\u00a2\u00a4\u00a6\u00a8\u00aa\u00ac\u00ae\u00b0\u00b2\u00b4\u0000"+ - "\n\u0002\u000022ii\u0001\u0000cd\u0002\u000066==\u0002\u0000@@CC\u0002"+ - "\u0000\'\'22\u0001\u0000UV\u0001\u0000WY\u0002\u0000??LL\u0002\u0000N"+ - "NPT\u0002\u0000\u0017\u0017\u0019\u001a\u03a8\u0000\u00c2\u0001\u0000"+ - "\u0000\u0000\u0002\u00c4\u0001\u0000\u0000\u0000\u0004\u00c7\u0001\u0000"+ - "\u0000\u0000\u0006\u00d9\u0001\u0000\u0000\u0000\b\u00f5\u0001\u0000\u0000"+ - "\u0000\n\u00f7\u0001\u0000\u0000\u0000\f\u00fa\u0001\u0000\u0000\u0000"+ - "\u000e\u00fc\u0001\u0000\u0000\u0000\u0010\u00ff\u0001\u0000\u0000\u0000"+ - "\u0012\u010a\u0001\u0000\u0000\u0000\u0014\u010e\u0001\u0000\u0000\u0000"+ - "\u0016\u0116\u0001\u0000\u0000\u0000\u0018\u011b\u0001\u0000\u0000\u0000"+ - "\u001a\u011e\u0001\u0000\u0000\u0000\u001c\u0121\u0001\u0000\u0000\u0000"+ - "\u001e\u0135\u0001\u0000\u0000\u0000 \u0137\u0001\u0000\u0000\u0000\""+ - "\u0139\u0001\u0000\u0000\u0000$\u013b\u0001\u0000\u0000\u0000&\u013d\u0001"+ - "\u0000\u0000\u0000(\u013f\u0001\u0000\u0000\u0000*\u0148\u0001\u0000\u0000"+ - "\u0000,\u014b\u0001\u0000\u0000\u0000.\u0153\u0001\u0000\u0000\u00000"+ - "\u015b\u0001\u0000\u0000\u00002\u016c\u0001\u0000\u0000\u00004\u016e\u0001"+ - "\u0000\u0000\u00006\u0182\u0001\u0000\u0000\u00008\u0184\u0001\u0000\u0000"+ - "\u0000:\u018c\u0001\u0000\u0000\u0000<\u0194\u0001\u0000\u0000\u0000>"+ - "\u0199\u0001\u0000\u0000\u0000@\u019d\u0001\u0000\u0000\u0000B\u01a1\u0001"+ - "\u0000\u0000\u0000D\u01a6\u0001\u0000\u0000\u0000F\u01a8\u0001\u0000\u0000"+ - "\u0000H\u01ab\u0001\u0000\u0000\u0000J\u01b4\u0001\u0000\u0000\u0000L"+ - "\u01bc\u0001\u0000\u0000\u0000N\u01bf\u0001\u0000\u0000\u0000P\u01c2\u0001"+ - "\u0000\u0000\u0000R\u01d3\u0001\u0000\u0000\u0000T\u01d5\u0001\u0000\u0000"+ - "\u0000V\u01db\u0001\u0000\u0000\u0000X\u01e3\u0001\u0000\u0000\u0000Z"+ - "\u01e9\u0001\u0000\u0000\u0000\\\u01eb\u0001\u0000\u0000\u0000^\u01ef"+ - "\u0001\u0000\u0000\u0000`\u01f2\u0001\u0000\u0000\u0000b\u01f5\u0001\u0000"+ - "\u0000\u0000d\u01f9\u0001\u0000\u0000\u0000f\u01fc\u0001\u0000\u0000\u0000"+ - "h\u020d\u0001\u0000\u0000\u0000j\u0212\u0001\u0000\u0000\u0000l\u0216"+ - "\u0001\u0000\u0000\u0000n\u0219\u0001\u0000\u0000\u0000p\u0226\u0001\u0000"+ - "\u0000\u0000r\u022a\u0001\u0000\u0000\u0000t\u022e\u0001\u0000\u0000\u0000"+ - "v\u0232\u0001\u0000\u0000\u0000x\u023d\u0001\u0000\u0000\u0000z\u023f"+ - "\u0001\u0000\u0000\u0000|\u024a\u0001\u0000\u0000\u0000~\u0253\u0001\u0000"+ - "\u0000\u0000\u0080\u0258\u0001\u0000\u0000\u0000\u0082\u025e\u0001\u0000"+ - "\u0000\u0000\u0084\u0261\u0001\u0000\u0000\u0000\u0086\u0267\u0001\u0000"+ - "\u0000\u0000\u0088\u026b\u0001\u0000\u0000\u0000\u008a\u028c\u0001\u0000"+ - "\u0000\u0000\u008c\u02c7\u0001\u0000\u0000\u0000\u008e\u02c9\u0001\u0000"+ - "\u0000\u0000\u0090\u02d6\u0001\u0000\u0000\u0000\u0092\u02dc\u0001\u0000"+ - "\u0000\u0000\u0094\u02f1\u0001\u0000\u0000\u0000\u0096\u02fb\u0001\u0000"+ - "\u0000\u0000\u0098\u0311\u0001\u0000\u0000\u0000\u009a\u0313\u0001\u0000"+ - "\u0000\u0000\u009c\u0320\u0001\u0000\u0000\u0000\u009e\u0326\u0001\u0000"+ - "\u0000\u0000\u00a0\u0352\u0001\u0000\u0000\u0000\u00a2\u0354\u0001\u0000"+ - "\u0000\u0000\u00a4\u0358\u0001\u0000\u0000\u0000\u00a6\u035b\u0001\u0000"+ - "\u0000\u0000\u00a8\u0360\u0001\u0000\u0000\u0000\u00aa\u0364\u0001\u0000"+ - "\u0000\u0000\u00ac\u0366\u0001\u0000\u0000\u0000\u00ae\u0368\u0001\u0000"+ - "\u0000\u0000\u00b0\u0375\u0001\u0000\u0000\u0000\u00b2\u0377\u0001\u0000"+ - "\u0000\u0000\u00b4\u0380\u0001\u0000\u0000\u0000\u00b6\u00b8\u0004\u0000"+ - "\u0000\u0000\u00b7\u00b9\u0003\u0086C\u0000\u00b8\u00b7\u0001\u0000\u0000"+ - "\u0000\u00b9\u00ba\u0001\u0000\u0000\u0000\u00ba\u00b8\u0001\u0000\u0000"+ - "\u0000\u00ba\u00bb\u0001\u0000\u0000\u0000\u00bb\u00bc\u0001\u0000\u0000"+ - "\u0000\u00bc\u00bd\u0003\u0002\u0001\u0000\u00bd\u00be\u0005\u0000\u0000"+ - "\u0001\u00be\u00c3\u0001\u0000\u0000\u0000\u00bf\u00c0\u0003\u0002\u0001"+ - "\u0000\u00c0\u00c1\u0005\u0000\u0000\u0001\u00c1\u00c3\u0001\u0000\u0000"+ - "\u0000\u00c2\u00b6\u0001\u0000\u0000\u0000\u00c2\u00bf\u0001\u0000\u0000"+ - "\u0000\u00c3\u0001\u0001\u0000\u0000\u0000\u00c4\u00c5\u0003\u0004\u0002"+ - "\u0000\u00c5\u00c6\u0005\u0000\u0000\u0001\u00c6\u0003\u0001\u0000\u0000"+ - "\u0000\u00c7\u00c8\u0006\u0002\uffff\uffff\u0000\u00c8\u00c9\u0003\u0006"+ - "\u0003\u0000\u00c9\u00cf\u0001\u0000\u0000\u0000\u00ca\u00cb\n\u0001\u0000"+ - "\u0000\u00cb\u00cc\u00051\u0000\u0000\u00cc\u00ce\u0003\b\u0004\u0000"+ - "\u00cd\u00ca\u0001\u0000\u0000\u0000\u00ce\u00d1\u0001\u0000\u0000\u0000"+ - "\u00cf\u00cd\u0001\u0000\u0000\u0000\u00cf\u00d0\u0001\u0000\u0000\u0000"+ - "\u00d0\u0005\u0001\u0000\u0000\u0000\u00d1\u00cf\u0001\u0000\u0000\u0000"+ - "\u00d2\u00da\u0003\u0018\f\u0000\u00d3\u00da\u0003\u000e\u0007\u0000\u00d4"+ - "\u00da\u0003d2\u0000\u00d5\u00d6\u0004\u0003\u0002\u0000\u00d6\u00da\u0003"+ - "\u001a\r\u0000\u00d7\u00d8\u0004\u0003\u0003\u0000\u00d8\u00da\u0003`"+ - "0\u0000\u00d9\u00d2\u0001\u0000\u0000\u0000\u00d9\u00d3\u0001\u0000\u0000"+ - "\u0000\u00d9\u00d4\u0001\u0000\u0000\u0000\u00d9\u00d5\u0001\u0000\u0000"+ - "\u0000\u00d9\u00d7\u0001\u0000\u0000\u0000\u00da\u0007\u0001\u0000\u0000"+ - "\u0000\u00db\u00f6\u0003*\u0015\u0000\u00dc\u00f6\u0003\n\u0005\u0000"+ - "\u00dd\u00f6\u0003L&\u0000\u00de\u00f6\u0003F#\u0000\u00df\u00f6\u0003"+ - ",\u0016\u0000\u00e0\u00f6\u0003H$\u0000\u00e1\u00f6\u0003N\'\u0000\u00e2"+ - "\u00f6\u0003P(\u0000\u00e3\u00f6\u0003T*\u0000\u00e4\u00f6\u0003\\.\u0000"+ - "\u00e5\u00f6\u0003f3\u0000\u00e6\u00f6\u0003^/\u0000\u00e7\u00f6\u0003"+ - "\u00aeW\u0000\u00e8\u00f6\u0003n7\u0000\u00e9\u00f6\u0003|>\u0000\u00ea"+ - "\u00f6\u0003l6\u0000\u00eb\u00f6\u0003p8\u0000\u00ec\u00f6\u0003z=\u0000"+ - "\u00ed\u00ee\u0004\u0004\u0004\u0000\u00ee\u00f6\u0003\u0080@\u0000\u00ef"+ - "\u00f0\u0004\u0004\u0005\u0000\u00f0\u00f6\u0003~?\u0000\u00f1\u00f2\u0004"+ - "\u0004\u0006\u0000\u00f2\u00f6\u0003\u0082A\u0000\u00f3\u00f4\u0004\u0004"+ - "\u0007\u0000\u00f4\u00f6\u0003\u0084B\u0000\u00f5\u00db\u0001\u0000\u0000"+ - "\u0000\u00f5\u00dc\u0001\u0000\u0000\u0000\u00f5\u00dd\u0001\u0000\u0000"+ - "\u0000\u00f5\u00de\u0001\u0000\u0000\u0000\u00f5\u00df\u0001\u0000\u0000"+ - "\u0000\u00f5\u00e0\u0001\u0000\u0000\u0000\u00f5\u00e1\u0001\u0000\u0000"+ - "\u0000\u00f5\u00e2\u0001\u0000\u0000\u0000\u00f5\u00e3\u0001\u0000\u0000"+ - "\u0000\u00f5\u00e4\u0001\u0000\u0000\u0000\u00f5\u00e5\u0001\u0000\u0000"+ - "\u0000\u00f5\u00e6\u0001\u0000\u0000\u0000\u00f5\u00e7\u0001\u0000\u0000"+ - "\u0000\u00f5\u00e8\u0001\u0000\u0000\u0000\u00f5\u00e9\u0001\u0000\u0000"+ - "\u0000\u00f5\u00ea\u0001\u0000\u0000\u0000\u00f5\u00eb\u0001\u0000\u0000"+ - "\u0000\u00f5\u00ec\u0001\u0000\u0000\u0000\u00f5\u00ed\u0001\u0000\u0000"+ - "\u0000\u00f5\u00ef\u0001\u0000\u0000\u0000\u00f5\u00f1\u0001\u0000\u0000"+ - "\u0000\u00f5\u00f3\u0001\u0000\u0000\u0000\u00f6\t\u0001\u0000\u0000\u0000"+ - "\u00f7\u00f8\u0005\u0011\u0000\u0000\u00f8\u00f9\u0003\u008aE\u0000\u00f9"+ - "\u000b\u0001\u0000\u0000\u0000\u00fa\u00fb\u0003<\u001e\u0000\u00fb\r"+ - "\u0001\u0000\u0000\u0000\u00fc\u00fd\u0005\r\u0000\u0000\u00fd\u00fe\u0003"+ - "\u0010\b\u0000\u00fe\u000f\u0001\u0000\u0000\u0000\u00ff\u0104\u0003\u0012"+ - "\t\u0000\u0100\u0101\u0005<\u0000\u0000\u0101\u0103\u0003\u0012\t\u0000"+ - "\u0102\u0100\u0001\u0000\u0000\u0000\u0103\u0106\u0001\u0000\u0000\u0000"+ - "\u0104\u0102\u0001\u0000\u0000\u0000\u0104\u0105\u0001\u0000\u0000\u0000"+ - "\u0105\u0011\u0001\u0000\u0000\u0000\u0106\u0104\u0001\u0000\u0000\u0000"+ - "\u0107\u0108\u00032\u0019\u0000\u0108\u0109\u00057\u0000\u0000\u0109\u010b"+ - "\u0001\u0000\u0000\u0000\u010a\u0107\u0001\u0000\u0000\u0000\u010a\u010b"+ - "\u0001\u0000\u0000\u0000\u010b\u010c\u0001\u0000\u0000\u0000\u010c\u010d"+ - "\u0003\u008aE\u0000\u010d\u0013\u0001\u0000\u0000\u0000\u010e\u0113\u0003"+ - "\u0016\u000b\u0000\u010f\u0110\u0005<\u0000\u0000\u0110\u0112\u0003\u0016"+ - "\u000b\u0000\u0111\u010f\u0001\u0000\u0000\u0000\u0112\u0115\u0001\u0000"+ - "\u0000\u0000\u0113\u0111\u0001\u0000\u0000\u0000\u0113\u0114\u0001\u0000"+ - "\u0000\u0000\u0114\u0015\u0001\u0000\u0000\u0000\u0115\u0113\u0001\u0000"+ - "\u0000\u0000\u0116\u0119\u00032\u0019\u0000\u0117\u0118\u00057\u0000\u0000"+ - "\u0118\u011a\u0003\u008aE\u0000\u0119\u0117\u0001\u0000\u0000\u0000\u0119"+ - "\u011a\u0001\u0000\u0000\u0000\u011a\u0017\u0001\u0000\u0000\u0000\u011b"+ - "\u011c\u0005\u0013\u0000\u0000\u011c\u011d\u0003\u001c\u000e\u0000\u011d"+ - "\u0019\u0001\u0000\u0000\u0000\u011e\u011f\u0005\u0014\u0000\u0000\u011f"+ - "\u0120\u0003\u001c\u000e\u0000\u0120\u001b\u0001\u0000\u0000\u0000\u0121"+ - "\u0126\u0003\u001e\u000f\u0000\u0122\u0123\u0005<\u0000\u0000\u0123\u0125"+ - "\u0003\u001e\u000f\u0000\u0124\u0122\u0001\u0000\u0000\u0000\u0125\u0128"+ - "\u0001\u0000\u0000\u0000\u0126\u0124\u0001\u0000\u0000\u0000\u0126\u0127"+ - "\u0001\u0000\u0000\u0000\u0127\u012a\u0001\u0000\u0000\u0000\u0128\u0126"+ - "\u0001\u0000\u0000\u0000\u0129\u012b\u0003(\u0014\u0000\u012a\u0129\u0001"+ - "\u0000\u0000\u0000\u012a\u012b\u0001\u0000\u0000\u0000\u012b\u001d\u0001"+ - "\u0000\u0000\u0000\u012c\u012d\u0003 \u0010\u0000\u012d\u012e\u0005:\u0000"+ - "\u0000\u012e\u012f\u0003$\u0012\u0000\u012f\u0136\u0001\u0000\u0000\u0000"+ - "\u0130\u0131\u0003$\u0012\u0000\u0131\u0132\u00059\u0000\u0000\u0132\u0133"+ - "\u0003\"\u0011\u0000\u0133\u0136\u0001\u0000\u0000\u0000\u0134\u0136\u0003"+ - "&\u0013\u0000\u0135\u012c\u0001\u0000\u0000\u0000\u0135\u0130\u0001\u0000"+ - "\u0000\u0000\u0135\u0134\u0001\u0000\u0000\u0000\u0136\u001f\u0001\u0000"+ - "\u0000\u0000\u0137\u0138\u0005i\u0000\u0000\u0138!\u0001\u0000\u0000\u0000"+ - "\u0139\u013a\u0005i\u0000\u0000\u013a#\u0001\u0000\u0000\u0000\u013b\u013c"+ - "\u0005i\u0000\u0000\u013c%\u0001\u0000\u0000\u0000\u013d\u013e\u0007\u0000"+ - "\u0000\u0000\u013e\'\u0001\u0000\u0000\u0000\u013f\u0140\u0005h\u0000"+ - "\u0000\u0140\u0145\u0005i\u0000\u0000\u0141\u0142\u0005<\u0000\u0000\u0142"+ - "\u0144\u0005i\u0000\u0000\u0143\u0141\u0001\u0000\u0000\u0000\u0144\u0147"+ - "\u0001\u0000\u0000\u0000\u0145\u0143\u0001\u0000\u0000\u0000\u0145\u0146"+ - "\u0001\u0000\u0000\u0000\u0146)\u0001\u0000\u0000\u0000\u0147\u0145\u0001"+ - "\u0000\u0000\u0000\u0148\u0149\u0005\t\u0000\u0000\u0149\u014a\u0003\u0010"+ - "\b\u0000\u014a+\u0001\u0000\u0000\u0000\u014b\u014d\u0005\u0010\u0000"+ - "\u0000\u014c\u014e\u0003.\u0017\u0000\u014d\u014c\u0001\u0000\u0000\u0000"+ - "\u014d\u014e\u0001\u0000\u0000\u0000\u014e\u0151\u0001\u0000\u0000\u0000"+ - "\u014f\u0150\u00058\u0000\u0000\u0150\u0152\u0003\u0010\b\u0000\u0151"+ - "\u014f\u0001\u0000\u0000\u0000\u0151\u0152\u0001\u0000\u0000\u0000\u0152"+ - "-\u0001\u0000\u0000\u0000\u0153\u0158\u00030\u0018\u0000\u0154\u0155\u0005"+ - "<\u0000\u0000\u0155\u0157\u00030\u0018\u0000\u0156\u0154\u0001\u0000\u0000"+ - "\u0000\u0157\u015a\u0001\u0000\u0000\u0000\u0158\u0156\u0001\u0000\u0000"+ - "\u0000\u0158\u0159\u0001\u0000\u0000\u0000\u0159/\u0001\u0000\u0000\u0000"+ - "\u015a\u0158\u0001\u0000\u0000\u0000\u015b\u015e\u0003\u0012\t\u0000\u015c"+ - "\u015d\u0005\u0011\u0000\u0000\u015d\u015f\u0003\u008aE\u0000\u015e\u015c"+ - "\u0001\u0000\u0000\u0000\u015e\u015f\u0001\u0000\u0000\u0000\u015f1\u0001"+ - "\u0000\u0000\u0000\u0160\u0161\u0004\u0019\b\u0000\u0161\u0163\u0005_"+ - "\u0000\u0000\u0162\u0164\u0005c\u0000\u0000\u0163\u0162\u0001\u0000\u0000"+ - "\u0000\u0163\u0164\u0001\u0000\u0000\u0000\u0164\u0165\u0001\u0000\u0000"+ - "\u0000\u0165\u0166\u0005`\u0000\u0000\u0166\u0167\u0005>\u0000\u0000\u0167"+ - "\u0168\u0005_\u0000\u0000\u0168\u0169\u00034\u001a\u0000\u0169\u016a\u0005"+ - "`\u0000\u0000\u016a\u016d\u0001\u0000\u0000\u0000\u016b\u016d\u00034\u001a"+ - "\u0000\u016c\u0160\u0001\u0000\u0000\u0000\u016c\u016b\u0001\u0000\u0000"+ - "\u0000\u016d3\u0001\u0000\u0000\u0000\u016e\u0173\u0003D\"\u0000\u016f"+ - "\u0170\u0005>\u0000\u0000\u0170\u0172\u0003D\"\u0000\u0171\u016f\u0001"+ - "\u0000\u0000\u0000\u0172\u0175\u0001\u0000\u0000\u0000\u0173\u0171\u0001"+ - "\u0000\u0000\u0000\u0173\u0174\u0001\u0000\u0000\u0000\u01745\u0001\u0000"+ - "\u0000\u0000\u0175\u0173\u0001\u0000\u0000\u0000\u0176\u0177\u0004\u001b"+ - "\t\u0000\u0177\u0179\u0005_\u0000\u0000\u0178\u017a\u0005\u0081\u0000"+ - "\u0000\u0179\u0178\u0001\u0000\u0000\u0000\u0179\u017a\u0001\u0000\u0000"+ - "\u0000\u017a\u017b\u0001\u0000\u0000\u0000\u017b\u017c\u0005`\u0000\u0000"+ - "\u017c\u017d\u0005>\u0000\u0000\u017d\u017e\u0005_\u0000\u0000\u017e\u017f"+ - "\u00038\u001c\u0000\u017f\u0180\u0005`\u0000\u0000\u0180\u0183\u0001\u0000"+ - "\u0000\u0000\u0181\u0183\u00038\u001c\u0000\u0182\u0176\u0001\u0000\u0000"+ - "\u0000\u0182\u0181\u0001\u0000\u0000\u0000\u01837\u0001\u0000\u0000\u0000"+ - "\u0184\u0189\u0003>\u001f\u0000\u0185\u0186\u0005>\u0000\u0000\u0186\u0188"+ - "\u0003>\u001f\u0000\u0187\u0185\u0001\u0000\u0000\u0000\u0188\u018b\u0001"+ - "\u0000\u0000\u0000\u0189\u0187\u0001\u0000\u0000\u0000\u0189\u018a\u0001"+ - "\u0000\u0000\u0000\u018a9\u0001\u0000\u0000\u0000\u018b\u0189\u0001\u0000"+ - "\u0000\u0000\u018c\u0191\u00036\u001b\u0000\u018d\u018e\u0005<\u0000\u0000"+ - "\u018e\u0190\u00036\u001b\u0000\u018f\u018d\u0001\u0000\u0000\u0000\u0190"+ - "\u0193\u0001\u0000\u0000\u0000\u0191\u018f\u0001\u0000\u0000\u0000\u0191"+ - "\u0192\u0001\u0000\u0000\u0000\u0192;\u0001\u0000\u0000\u0000\u0193\u0191"+ - "\u0001\u0000\u0000\u0000\u0194\u0195\u0007\u0001\u0000\u0000\u0195=\u0001"+ - "\u0000\u0000\u0000\u0196\u019a\u0005\u0081\u0000\u0000\u0197\u019a\u0003"+ - "@ \u0000\u0198\u019a\u0003B!\u0000\u0199\u0196\u0001\u0000\u0000\u0000"+ - "\u0199\u0197\u0001\u0000\u0000\u0000\u0199\u0198\u0001\u0000\u0000\u0000"+ - "\u019a?\u0001\u0000\u0000\u0000\u019b\u019e\u0005J\u0000\u0000\u019c\u019e"+ - "\u0005]\u0000\u0000\u019d\u019b\u0001\u0000\u0000\u0000\u019d\u019c\u0001"+ - "\u0000\u0000\u0000\u019eA\u0001\u0000\u0000\u0000\u019f\u01a2\u0005\\"+ - "\u0000\u0000\u01a0\u01a2\u0005^\u0000\u0000\u01a1\u019f\u0001\u0000\u0000"+ - "\u0000\u01a1\u01a0\u0001\u0000\u0000\u0000\u01a2C\u0001\u0000\u0000\u0000"+ - "\u01a3\u01a7\u0003<\u001e\u0000\u01a4\u01a7\u0003@ \u0000\u01a5\u01a7"+ - "\u0003B!\u0000\u01a6\u01a3\u0001\u0000\u0000\u0000\u01a6\u01a4\u0001\u0000"+ - "\u0000\u0000\u01a6\u01a5\u0001\u0000\u0000\u0000\u01a7E\u0001\u0000\u0000"+ - "\u0000\u01a8\u01a9\u0005\u000b\u0000\u0000\u01a9\u01aa\u0003\u00a0P\u0000"+ - "\u01aaG\u0001\u0000\u0000\u0000\u01ab\u01ac\u0005\u000f\u0000\u0000\u01ac"+ - "\u01b1\u0003J%\u0000\u01ad\u01ae\u0005<\u0000\u0000\u01ae\u01b0\u0003"+ - "J%\u0000\u01af\u01ad\u0001\u0000\u0000\u0000\u01b0\u01b3\u0001\u0000\u0000"+ - "\u0000\u01b1\u01af\u0001\u0000\u0000\u0000\u01b1\u01b2\u0001\u0000\u0000"+ - "\u0000\u01b2I\u0001\u0000\u0000\u0000\u01b3\u01b1\u0001\u0000\u0000\u0000"+ - "\u01b4\u01b6\u0003\u008aE\u0000\u01b5\u01b7\u0007\u0002\u0000\u0000\u01b6"+ - "\u01b5\u0001\u0000\u0000\u0000\u01b6\u01b7\u0001\u0000\u0000\u0000\u01b7"+ - "\u01ba\u0001\u0000\u0000\u0000\u01b8\u01b9\u0005G\u0000\u0000\u01b9\u01bb"+ - "\u0007\u0003\u0000\u0000\u01ba\u01b8\u0001\u0000\u0000\u0000\u01ba\u01bb"+ - "\u0001\u0000\u0000\u0000\u01bbK\u0001\u0000\u0000\u0000\u01bc\u01bd\u0005"+ - "\u001e\u0000\u0000\u01bd\u01be\u0003:\u001d\u0000\u01beM\u0001\u0000\u0000"+ - "\u0000\u01bf\u01c0\u0005\u001d\u0000\u0000\u01c0\u01c1\u0003:\u001d\u0000"+ - "\u01c1O\u0001\u0000\u0000\u0000\u01c2\u01c3\u0005 \u0000\u0000\u01c3\u01c8"+ - "\u0003R)\u0000\u01c4\u01c5\u0005<\u0000\u0000\u01c5\u01c7\u0003R)\u0000"+ - "\u01c6\u01c4\u0001\u0000\u0000\u0000\u01c7\u01ca\u0001\u0000\u0000\u0000"+ - "\u01c8\u01c6\u0001\u0000\u0000\u0000\u01c8\u01c9\u0001\u0000\u0000\u0000"+ - "\u01c9Q\u0001\u0000\u0000\u0000\u01ca\u01c8\u0001\u0000\u0000\u0000\u01cb"+ - "\u01cc\u00036\u001b\u0000\u01cc\u01cd\u0005\u0085\u0000\u0000\u01cd\u01ce"+ - "\u00036\u001b\u0000\u01ce\u01d4\u0001\u0000\u0000\u0000\u01cf\u01d0\u0003"+ - "6\u001b\u0000\u01d0\u01d1\u00057\u0000\u0000\u01d1\u01d2\u00036\u001b"+ - "\u0000\u01d2\u01d4\u0001\u0000\u0000\u0000\u01d3\u01cb\u0001\u0000\u0000"+ - "\u0000\u01d3\u01cf\u0001\u0000\u0000\u0000\u01d4S\u0001\u0000\u0000\u0000"+ - "\u01d5\u01d6\u0005\b\u0000\u0000\u01d6\u01d7\u0003\u0094J\u0000\u01d7"+ - "\u01d9\u0003\u00aaU\u0000\u01d8\u01da\u0003V+\u0000\u01d9\u01d8\u0001"+ - "\u0000\u0000\u0000\u01d9\u01da\u0001\u0000\u0000\u0000\u01daU\u0001\u0000"+ - "\u0000\u0000\u01db\u01e0\u0003X,\u0000\u01dc\u01dd\u0005<\u0000\u0000"+ - "\u01dd\u01df\u0003X,\u0000\u01de\u01dc\u0001\u0000\u0000\u0000\u01df\u01e2"+ - "\u0001\u0000\u0000\u0000\u01e0\u01de\u0001\u0000\u0000\u0000\u01e0\u01e1"+ - "\u0001\u0000\u0000\u0000\u01e1W\u0001\u0000\u0000\u0000\u01e2\u01e0\u0001"+ - "\u0000\u0000\u0000\u01e3\u01e4\u0003<\u001e\u0000\u01e4\u01e5\u00057\u0000"+ - "\u0000\u01e5\u01e6\u0003\u00a0P\u0000\u01e6Y\u0001\u0000\u0000\u0000\u01e7"+ - "\u01e8\u0005M\u0000\u0000\u01e8\u01ea\u0003\u009aM\u0000\u01e9\u01e7\u0001"+ - "\u0000\u0000\u0000\u01e9\u01ea\u0001\u0000\u0000\u0000\u01ea[\u0001\u0000"+ - "\u0000\u0000\u01eb\u01ec\u0005\n\u0000\u0000\u01ec\u01ed\u0003\u0094J"+ - "\u0000\u01ed\u01ee\u0003\u00aaU\u0000\u01ee]\u0001\u0000\u0000\u0000\u01ef"+ - "\u01f0\u0005\u001c\u0000\u0000\u01f0\u01f1\u00032\u0019\u0000\u01f1_\u0001"+ - "\u0000\u0000\u0000\u01f2\u01f3\u0005\u0006\u0000\u0000\u01f3\u01f4\u0003"+ - "b1\u0000\u01f4a\u0001\u0000\u0000\u0000\u01f5\u01f6\u0005a\u0000\u0000"+ - "\u01f6\u01f7\u0003\u0004\u0002\u0000\u01f7\u01f8\u0005b\u0000\u0000\u01f8"+ - "c\u0001\u0000\u0000\u0000\u01f9\u01fa\u0005\"\u0000\u0000\u01fa\u01fb"+ - "\u0005\u008c\u0000\u0000\u01fbe\u0001\u0000\u0000\u0000\u01fc\u01fd\u0005"+ - "\u0005\u0000\u0000\u01fd\u0200\u0003h4\u0000\u01fe\u01ff\u0005H\u0000"+ - "\u0000\u01ff\u0201\u00036\u001b\u0000\u0200\u01fe\u0001\u0000\u0000\u0000"+ - "\u0200\u0201\u0001\u0000\u0000\u0000\u0201\u020b\u0001\u0000\u0000\u0000"+ - "\u0202\u0203\u0005M\u0000\u0000\u0203\u0208\u0003j5\u0000\u0204\u0205"+ - "\u0005<\u0000\u0000\u0205\u0207\u0003j5\u0000\u0206\u0204\u0001\u0000"+ - "\u0000\u0000\u0207\u020a\u0001\u0000\u0000\u0000\u0208\u0206\u0001\u0000"+ - "\u0000\u0000\u0208\u0209\u0001\u0000\u0000\u0000\u0209\u020c\u0001\u0000"+ - "\u0000\u0000\u020a\u0208\u0001\u0000\u0000\u0000\u020b\u0202\u0001\u0000"+ - "\u0000\u0000\u020b\u020c\u0001\u0000\u0000\u0000\u020cg\u0001\u0000\u0000"+ - "\u0000\u020d\u020e\u0007\u0004\u0000\u0000\u020ei\u0001\u0000\u0000\u0000"+ - "\u020f\u0210\u00036\u001b\u0000\u0210\u0211\u00057\u0000\u0000\u0211\u0213"+ - "\u0001\u0000\u0000\u0000\u0212\u020f\u0001\u0000\u0000\u0000\u0212\u0213"+ - "\u0001\u0000\u0000\u0000\u0213\u0214\u0001\u0000\u0000\u0000\u0214\u0215"+ - "\u00036\u001b\u0000\u0215k\u0001\u0000\u0000\u0000\u0216\u0217\u0005\u000e"+ - "\u0000\u0000\u0217\u0218\u0003\u00a0P\u0000\u0218m\u0001\u0000\u0000\u0000"+ - "\u0219\u021a\u0005\u0004\u0000\u0000\u021a\u021d\u00032\u0019\u0000\u021b"+ - "\u021c\u0005H\u0000\u0000\u021c\u021e\u00032\u0019\u0000\u021d\u021b\u0001"+ - "\u0000\u0000\u0000\u021d\u021e\u0001\u0000\u0000\u0000\u021e\u0224\u0001"+ - "\u0000\u0000\u0000\u021f\u0220\u0005\u0085\u0000\u0000\u0220\u0221\u0003"+ - "2\u0019\u0000\u0221\u0222\u0005<\u0000\u0000\u0222\u0223\u00032\u0019"+ - "\u0000\u0223\u0225\u0001\u0000\u0000\u0000\u0224\u021f\u0001\u0000\u0000"+ - "\u0000\u0224\u0225\u0001\u0000\u0000\u0000\u0225o\u0001\u0000\u0000\u0000"+ - "\u0226\u0227\u0005\u0015\u0000\u0000\u0227\u0228\u0003r9\u0000\u0228q"+ - "\u0001\u0000\u0000\u0000\u0229\u022b\u0003t:\u0000\u022a\u0229\u0001\u0000"+ - "\u0000\u0000\u022b\u022c\u0001\u0000\u0000\u0000\u022c\u022a\u0001\u0000"+ - "\u0000\u0000\u022c\u022d\u0001\u0000\u0000\u0000\u022ds\u0001\u0000\u0000"+ - "\u0000\u022e\u022f\u0005a\u0000\u0000\u022f\u0230\u0003v;\u0000\u0230"+ - "\u0231\u0005b\u0000\u0000\u0231u\u0001\u0000\u0000\u0000\u0232\u0233\u0006"+ - ";\uffff\uffff\u0000\u0233\u0234\u0003x<\u0000\u0234\u023a\u0001\u0000"+ - "\u0000\u0000\u0235\u0236\n\u0001\u0000\u0000\u0236\u0237\u00051\u0000"+ - "\u0000\u0237\u0239\u0003x<\u0000\u0238\u0235\u0001\u0000\u0000\u0000\u0239"+ - "\u023c\u0001\u0000\u0000\u0000\u023a\u0238\u0001\u0000\u0000\u0000\u023a"+ - "\u023b\u0001\u0000\u0000\u0000\u023bw\u0001\u0000\u0000\u0000\u023c\u023a"+ - "\u0001\u0000\u0000\u0000\u023d\u023e\u0003\b\u0004\u0000\u023ey\u0001"+ - "\u0000\u0000\u0000\u023f\u0243\u0005\f\u0000\u0000\u0240\u0241\u00032"+ - "\u0019\u0000\u0241\u0242\u00057\u0000\u0000\u0242\u0244\u0001\u0000\u0000"+ - "\u0000\u0243\u0240\u0001\u0000\u0000\u0000\u0243\u0244\u0001\u0000\u0000"+ - "\u0000\u0244\u0245\u0001\u0000\u0000\u0000\u0245\u0246\u0003\u00a0P\u0000"+ - "\u0246\u0247\u0005H\u0000\u0000\u0247\u0248\u0003\u0014\n\u0000\u0248"+ - "\u0249\u0003Z-\u0000\u0249{\u0001\u0000\u0000\u0000\u024a\u024e\u0005"+ - "\u0007\u0000\u0000\u024b\u024c\u00032\u0019\u0000\u024c\u024d\u00057\u0000"+ - "\u0000\u024d\u024f\u0001\u0000\u0000\u0000\u024e\u024b\u0001\u0000\u0000"+ - "\u0000\u024e\u024f\u0001\u0000\u0000\u0000\u024f\u0250\u0001\u0000\u0000"+ - "\u0000\u0250\u0251\u0003\u0094J\u0000\u0251\u0252\u0003Z-\u0000\u0252"+ - "}\u0001\u0000\u0000\u0000\u0253\u0254\u0005\u001b\u0000\u0000\u0254\u0255"+ - "\u0003\u001e\u000f\u0000\u0255\u0256\u0005H\u0000\u0000\u0256\u0257\u0003"+ - ":\u001d\u0000\u0257\u007f\u0001\u0000\u0000\u0000\u0258\u0259\u0005\u0012"+ - "\u0000\u0000\u0259\u025c\u0003.\u0017\u0000\u025a\u025b\u00058\u0000\u0000"+ - "\u025b\u025d\u0003\u0010\b\u0000\u025c\u025a\u0001\u0000\u0000\u0000\u025c"+ - "\u025d\u0001\u0000\u0000\u0000\u025d\u0081\u0001\u0000\u0000\u0000\u025e"+ - "\u025f\u0005\u001f\u0000\u0000\u025f\u0260\u0003:\u001d\u0000\u0260\u0083"+ - "\u0001\u0000\u0000\u0000\u0261\u0263\u0005\u0016\u0000\u0000\u0262\u0264"+ - "\u0003<\u001e\u0000\u0263\u0262\u0001\u0000\u0000\u0000\u0263\u0264\u0001"+ - "\u0000\u0000\u0000\u0264\u0265\u0001\u0000\u0000\u0000\u0265\u0266\u0003"+ - "Z-\u0000\u0266\u0085\u0001\u0000\u0000\u0000\u0267\u0268\u0005!\u0000"+ - "\u0000\u0268\u0269\u0003\u0088D\u0000\u0269\u026a\u0005;\u0000\u0000\u026a"+ - "\u0087\u0001\u0000\u0000\u0000\u026b\u026c\u0003<\u001e\u0000\u026c\u026d"+ - "\u00057\u0000\u0000\u026d\u026e\u0003\u00a0P\u0000\u026e\u0089\u0001\u0000"+ - "\u0000\u0000\u026f\u0270\u0006E\uffff\uffff\u0000\u0270\u0271\u0005E\u0000"+ - "\u0000\u0271\u028d\u0003\u008aE\b\u0272\u028d\u0003\u0090H\u0000\u0273"+ - "\u028d\u0003\u008cF\u0000\u0274\u0276\u0003\u0090H\u0000\u0275\u0277\u0005"+ - "E\u0000\u0000\u0276\u0275\u0001\u0000\u0000\u0000\u0276\u0277\u0001\u0000"+ - "\u0000\u0000\u0277\u0278\u0001\u0000\u0000\u0000\u0278\u0279\u0005A\u0000"+ - "\u0000\u0279\u027a\u0005a\u0000\u0000\u027a\u027f\u0003\u0090H\u0000\u027b"+ - "\u027c\u0005<\u0000\u0000\u027c\u027e\u0003\u0090H\u0000\u027d\u027b\u0001"+ - "\u0000\u0000\u0000\u027e\u0281\u0001\u0000\u0000\u0000\u027f\u027d\u0001"+ - "\u0000\u0000\u0000\u027f\u0280\u0001\u0000\u0000\u0000\u0280\u0282\u0001"+ - "\u0000\u0000\u0000\u0281\u027f\u0001\u0000\u0000\u0000\u0282\u0283\u0005"+ - "b\u0000\u0000\u0283\u028d\u0001\u0000\u0000\u0000\u0284\u0285\u0003\u0090"+ - "H\u0000\u0285\u0287\u0005B\u0000\u0000\u0286\u0288\u0005E\u0000\u0000"+ - "\u0287\u0286\u0001\u0000\u0000\u0000\u0287\u0288\u0001\u0000\u0000\u0000"+ - "\u0288\u0289\u0001\u0000\u0000\u0000\u0289\u028a\u0005F\u0000\u0000\u028a"+ - "\u028d\u0001\u0000\u0000\u0000\u028b\u028d\u0003\u008eG\u0000\u028c\u026f"+ - "\u0001\u0000\u0000\u0000\u028c\u0272\u0001\u0000\u0000\u0000\u028c\u0273"+ - "\u0001\u0000\u0000\u0000\u028c\u0274\u0001\u0000\u0000\u0000\u028c\u0284"+ - "\u0001\u0000\u0000\u0000\u028c\u028b\u0001\u0000\u0000\u0000\u028d\u0296"+ - "\u0001\u0000\u0000\u0000\u028e\u028f\n\u0005\u0000\u0000\u028f\u0290\u0005"+ - "5\u0000\u0000\u0290\u0295\u0003\u008aE\u0006\u0291\u0292\n\u0004\u0000"+ - "\u0000\u0292\u0293\u0005I\u0000\u0000\u0293\u0295\u0003\u008aE\u0005\u0294"+ - "\u028e\u0001\u0000\u0000\u0000\u0294\u0291\u0001\u0000\u0000\u0000\u0295"+ - "\u0298\u0001\u0000\u0000\u0000\u0296\u0294\u0001\u0000\u0000\u0000\u0296"+ - "\u0297\u0001\u0000\u0000\u0000\u0297\u008b\u0001\u0000\u0000\u0000\u0298"+ - "\u0296\u0001\u0000\u0000\u0000\u0299\u029b\u0003\u0090H\u0000\u029a\u029c"+ - "\u0005E\u0000\u0000\u029b\u029a\u0001\u0000\u0000\u0000\u029b\u029c\u0001"+ - "\u0000\u0000\u0000\u029c\u029d\u0001\u0000\u0000\u0000\u029d\u029e\u0005"+ - "D\u0000\u0000\u029e\u029f\u0003\u00aaU\u0000\u029f\u02c8\u0001\u0000\u0000"+ - "\u0000\u02a0\u02a2\u0003\u0090H\u0000\u02a1\u02a3\u0005E\u0000\u0000\u02a2"+ - "\u02a1\u0001\u0000\u0000\u0000\u02a2\u02a3\u0001\u0000\u0000\u0000\u02a3"+ - "\u02a4\u0001\u0000\u0000\u0000\u02a4\u02a5\u0005K\u0000\u0000\u02a5\u02a6"+ - "\u0003\u00aaU\u0000\u02a6\u02c8\u0001\u0000\u0000\u0000\u02a7\u02a9\u0003"+ - "\u0090H\u0000\u02a8\u02aa\u0005E\u0000\u0000\u02a9\u02a8\u0001\u0000\u0000"+ - "\u0000\u02a9\u02aa\u0001\u0000\u0000\u0000\u02aa\u02ab\u0001\u0000\u0000"+ - "\u0000\u02ab\u02ac\u0005D\u0000\u0000\u02ac\u02ad\u0005a\u0000\u0000\u02ad"+ - "\u02b2\u0003\u00aaU\u0000\u02ae\u02af\u0005<\u0000\u0000\u02af\u02b1\u0003"+ - "\u00aaU\u0000\u02b0\u02ae\u0001\u0000\u0000\u0000\u02b1\u02b4\u0001\u0000"+ - "\u0000\u0000\u02b2\u02b0\u0001\u0000\u0000\u0000\u02b2\u02b3\u0001\u0000"+ - "\u0000\u0000\u02b3\u02b5\u0001\u0000\u0000\u0000\u02b4\u02b2\u0001\u0000"+ - "\u0000\u0000\u02b5\u02b6\u0005b\u0000\u0000\u02b6\u02c8\u0001\u0000\u0000"+ - "\u0000\u02b7\u02b9\u0003\u0090H\u0000\u02b8\u02ba\u0005E\u0000\u0000\u02b9"+ - "\u02b8\u0001\u0000\u0000\u0000\u02b9\u02ba\u0001\u0000\u0000\u0000\u02ba"+ - "\u02bb\u0001\u0000\u0000\u0000\u02bb\u02bc\u0005K\u0000\u0000\u02bc\u02bd"+ - "\u0005a\u0000\u0000\u02bd\u02c2\u0003\u00aaU\u0000\u02be\u02bf\u0005<"+ - "\u0000\u0000\u02bf\u02c1\u0003\u00aaU\u0000\u02c0\u02be\u0001\u0000\u0000"+ - "\u0000\u02c1\u02c4\u0001\u0000\u0000\u0000\u02c2\u02c0\u0001\u0000\u0000"+ - "\u0000\u02c2\u02c3\u0001\u0000\u0000\u0000\u02c3\u02c5\u0001\u0000\u0000"+ - "\u0000\u02c4\u02c2\u0001\u0000\u0000\u0000\u02c5\u02c6\u0005b\u0000\u0000"+ - "\u02c6\u02c8\u0001\u0000\u0000\u0000\u02c7\u0299\u0001\u0000\u0000\u0000"+ - "\u02c7\u02a0\u0001\u0000\u0000\u0000\u02c7\u02a7\u0001\u0000\u0000\u0000"+ - "\u02c7\u02b7\u0001\u0000\u0000\u0000\u02c8\u008d\u0001\u0000\u0000\u0000"+ - "\u02c9\u02cc\u00032\u0019\u0000\u02ca\u02cb\u00059\u0000\u0000\u02cb\u02cd"+ - "\u0003\f\u0006\u0000\u02cc\u02ca\u0001\u0000\u0000\u0000\u02cc\u02cd\u0001"+ - "\u0000\u0000\u0000\u02cd\u02ce\u0001\u0000\u0000\u0000\u02ce\u02cf\u0005"+ - ":\u0000\u0000\u02cf\u02d0\u0003\u00a0P\u0000\u02d0\u008f\u0001\u0000\u0000"+ - "\u0000\u02d1\u02d7\u0003\u0092I\u0000\u02d2\u02d3\u0003\u0092I\u0000\u02d3"+ - "\u02d4\u0003\u00acV\u0000\u02d4\u02d5\u0003\u0092I\u0000\u02d5\u02d7\u0001"+ - "\u0000\u0000\u0000\u02d6\u02d1\u0001\u0000\u0000\u0000\u02d6\u02d2\u0001"+ - "\u0000\u0000\u0000\u02d7\u0091\u0001\u0000\u0000\u0000\u02d8\u02d9\u0006"+ - "I\uffff\uffff\u0000\u02d9\u02dd\u0003\u0094J\u0000\u02da\u02db\u0007\u0005"+ - "\u0000\u0000\u02db\u02dd\u0003\u0092I\u0003\u02dc\u02d8\u0001\u0000\u0000"+ - "\u0000\u02dc\u02da\u0001\u0000\u0000\u0000\u02dd\u02e6\u0001\u0000\u0000"+ - "\u0000\u02de\u02df\n\u0002\u0000\u0000\u02df\u02e0\u0007\u0006\u0000\u0000"+ - "\u02e0\u02e5\u0003\u0092I\u0003\u02e1\u02e2\n\u0001\u0000\u0000\u02e2"+ - "\u02e3\u0007\u0005\u0000\u0000\u02e3\u02e5\u0003\u0092I\u0002\u02e4\u02de"+ - "\u0001\u0000\u0000\u0000\u02e4\u02e1\u0001\u0000\u0000\u0000\u02e5\u02e8"+ - "\u0001\u0000\u0000\u0000\u02e6\u02e4\u0001\u0000\u0000\u0000\u02e6\u02e7"+ - "\u0001\u0000\u0000\u0000\u02e7\u0093\u0001\u0000\u0000\u0000\u02e8\u02e6"+ - "\u0001\u0000\u0000\u0000\u02e9\u02ea\u0006J\uffff\uffff\u0000\u02ea\u02f2"+ - "\u0003\u00a0P\u0000\u02eb\u02f2\u00032\u0019\u0000\u02ec\u02f2\u0003\u0096"+ - "K\u0000\u02ed\u02ee\u0005a\u0000\u0000\u02ee\u02ef\u0003\u008aE\u0000"+ - "\u02ef\u02f0\u0005b\u0000\u0000\u02f0\u02f2\u0001\u0000\u0000\u0000\u02f1"+ - "\u02e9\u0001\u0000\u0000\u0000\u02f1\u02eb\u0001\u0000\u0000\u0000\u02f1"+ - "\u02ec\u0001\u0000\u0000\u0000\u02f1\u02ed\u0001\u0000\u0000\u0000\u02f2"+ - "\u02f8\u0001\u0000\u0000\u0000\u02f3\u02f4\n\u0001\u0000\u0000\u02f4\u02f5"+ - "\u00059\u0000\u0000\u02f5\u02f7\u0003\f\u0006\u0000\u02f6\u02f3\u0001"+ - "\u0000\u0000\u0000\u02f7\u02fa\u0001\u0000\u0000\u0000\u02f8\u02f6\u0001"+ - "\u0000\u0000\u0000\u02f8\u02f9\u0001\u0000\u0000\u0000\u02f9\u0095\u0001"+ - "\u0000\u0000\u0000\u02fa\u02f8\u0001\u0000\u0000\u0000\u02fb\u02fc\u0003"+ - "\u0098L\u0000\u02fc\u030a\u0005a\u0000\u0000\u02fd\u030b\u0005W\u0000"+ - "\u0000\u02fe\u0303\u0003\u008aE\u0000\u02ff\u0300\u0005<\u0000\u0000\u0300"+ - "\u0302\u0003\u008aE\u0000\u0301\u02ff\u0001\u0000\u0000\u0000\u0302\u0305"+ - "\u0001\u0000\u0000\u0000\u0303\u0301\u0001\u0000\u0000\u0000\u0303\u0304"+ - "\u0001\u0000\u0000\u0000\u0304\u0308\u0001\u0000\u0000\u0000\u0305\u0303"+ - "\u0001\u0000\u0000\u0000\u0306\u0307\u0005<\u0000\u0000\u0307\u0309\u0003"+ - "\u009aM\u0000\u0308\u0306\u0001\u0000\u0000\u0000\u0308\u0309\u0001\u0000"+ - "\u0000\u0000\u0309\u030b\u0001\u0000\u0000\u0000\u030a\u02fd\u0001\u0000"+ - "\u0000\u0000\u030a\u02fe\u0001\u0000\u0000\u0000\u030a\u030b\u0001\u0000"+ - "\u0000\u0000\u030b\u030c\u0001\u0000\u0000\u0000\u030c\u030d\u0005b\u0000"+ - "\u0000\u030d\u0097\u0001\u0000\u0000\u0000\u030e\u0312\u0003D\"\u0000"+ - "\u030f\u0312\u0005@\u0000\u0000\u0310\u0312\u0005C\u0000\u0000\u0311\u030e"+ - "\u0001\u0000\u0000\u0000\u0311\u030f\u0001\u0000\u0000\u0000\u0311\u0310"+ - "\u0001\u0000\u0000\u0000\u0312\u0099\u0001\u0000\u0000\u0000\u0313\u031c"+ - "\u0005Z\u0000\u0000\u0314\u0319\u0003\u009cN\u0000\u0315\u0316\u0005<"+ - "\u0000\u0000\u0316\u0318\u0003\u009cN\u0000\u0317\u0315\u0001\u0000\u0000"+ - "\u0000\u0318\u031b\u0001\u0000\u0000\u0000\u0319\u0317\u0001\u0000\u0000"+ - "\u0000\u0319\u031a\u0001\u0000\u0000\u0000\u031a\u031d\u0001\u0000\u0000"+ - "\u0000\u031b\u0319\u0001\u0000\u0000\u0000\u031c\u0314\u0001\u0000\u0000"+ - "\u0000\u031c\u031d\u0001\u0000\u0000\u0000\u031d\u031e\u0001\u0000\u0000"+ - "\u0000\u031e\u031f\u0005[\u0000\u0000\u031f\u009b\u0001\u0000\u0000\u0000"+ - "\u0320\u0321\u0003\u00aaU\u0000\u0321\u0322\u0005:\u0000\u0000\u0322\u0323"+ - "\u0003\u009eO\u0000\u0323\u009d\u0001\u0000\u0000\u0000\u0324\u0327\u0003"+ - "\u00a0P\u0000\u0325\u0327\u0003\u009aM\u0000\u0326\u0324\u0001\u0000\u0000"+ - "\u0000\u0326\u0325\u0001\u0000\u0000\u0000\u0327\u009f\u0001\u0000\u0000"+ - "\u0000\u0328\u0353\u0005F\u0000\u0000\u0329\u032a\u0003\u00a8T\u0000\u032a"+ - "\u032b\u0005c\u0000\u0000\u032b\u0353\u0001\u0000\u0000\u0000\u032c\u0353"+ - "\u0003\u00a6S\u0000\u032d\u0353\u0003\u00a8T\u0000\u032e\u0353\u0003\u00a2"+ - "Q\u0000\u032f\u0353\u0003@ \u0000\u0330\u0353\u0003\u00aaU\u0000\u0331"+ - "\u0332\u0005_\u0000\u0000\u0332\u0337\u0003\u00a4R\u0000\u0333\u0334\u0005"+ - "<\u0000\u0000\u0334\u0336\u0003\u00a4R\u0000\u0335\u0333\u0001\u0000\u0000"+ - "\u0000\u0336\u0339\u0001\u0000\u0000\u0000\u0337\u0335\u0001\u0000\u0000"+ - "\u0000\u0337\u0338\u0001\u0000\u0000\u0000\u0338\u033a\u0001\u0000\u0000"+ - "\u0000\u0339\u0337\u0001\u0000\u0000\u0000\u033a\u033b\u0005`\u0000\u0000"+ - "\u033b\u0353\u0001\u0000\u0000\u0000\u033c\u033d\u0005_\u0000\u0000\u033d"+ - "\u0342\u0003\u00a2Q\u0000\u033e\u033f\u0005<\u0000\u0000\u033f\u0341\u0003"+ - "\u00a2Q\u0000\u0340\u033e\u0001\u0000\u0000\u0000\u0341\u0344\u0001\u0000"+ - "\u0000\u0000\u0342\u0340\u0001\u0000\u0000\u0000\u0342\u0343\u0001\u0000"+ - "\u0000\u0000\u0343\u0345\u0001\u0000\u0000\u0000\u0344\u0342\u0001\u0000"+ - "\u0000\u0000\u0345\u0346\u0005`\u0000\u0000\u0346\u0353\u0001\u0000\u0000"+ - "\u0000\u0347\u0348\u0005_\u0000\u0000\u0348\u034d\u0003\u00aaU\u0000\u0349"+ - "\u034a\u0005<\u0000\u0000\u034a\u034c\u0003\u00aaU\u0000\u034b\u0349\u0001"+ - "\u0000\u0000\u0000\u034c\u034f\u0001\u0000\u0000\u0000\u034d\u034b\u0001"+ - "\u0000\u0000\u0000\u034d\u034e\u0001\u0000\u0000\u0000\u034e\u0350\u0001"+ - "\u0000\u0000\u0000\u034f\u034d\u0001\u0000\u0000\u0000\u0350\u0351\u0005"+ - "`\u0000\u0000\u0351\u0353\u0001\u0000\u0000\u0000\u0352\u0328\u0001\u0000"+ - "\u0000\u0000\u0352\u0329\u0001\u0000\u0000\u0000\u0352\u032c\u0001\u0000"+ - "\u0000\u0000\u0352\u032d\u0001\u0000\u0000\u0000\u0352\u032e\u0001\u0000"+ - "\u0000\u0000\u0352\u032f\u0001\u0000\u0000\u0000\u0352\u0330\u0001\u0000"+ - "\u0000\u0000\u0352\u0331\u0001\u0000\u0000\u0000\u0352\u033c\u0001\u0000"+ - "\u0000\u0000\u0352\u0347\u0001\u0000\u0000\u0000\u0353\u00a1\u0001\u0000"+ - "\u0000\u0000\u0354\u0355\u0007\u0007\u0000\u0000\u0355\u00a3\u0001\u0000"+ - "\u0000\u0000\u0356\u0359\u0003\u00a6S\u0000\u0357\u0359\u0003\u00a8T\u0000"+ - "\u0358\u0356\u0001\u0000\u0000\u0000\u0358\u0357\u0001\u0000\u0000\u0000"+ - "\u0359\u00a5\u0001\u0000\u0000\u0000\u035a\u035c\u0007\u0005\u0000\u0000"+ - "\u035b\u035a\u0001\u0000\u0000\u0000\u035b\u035c\u0001\u0000\u0000\u0000"+ - "\u035c\u035d\u0001\u0000\u0000\u0000\u035d\u035e\u00054\u0000\u0000\u035e"+ - "\u00a7\u0001\u0000\u0000\u0000\u035f\u0361\u0007\u0005\u0000\u0000\u0360"+ - "\u035f\u0001\u0000\u0000\u0000\u0360\u0361\u0001\u0000\u0000\u0000\u0361"+ - "\u0362\u0001\u0000\u0000\u0000\u0362\u0363\u00053\u0000\u0000\u0363\u00a9"+ - "\u0001\u0000\u0000\u0000\u0364\u0365\u00052\u0000\u0000\u0365\u00ab\u0001"+ - "\u0000\u0000\u0000\u0366\u0367\u0007\b\u0000\u0000\u0367\u00ad\u0001\u0000"+ - "\u0000\u0000\u0368\u0369\u0007\t\u0000\u0000\u0369\u036a\u0005s\u0000"+ - "\u0000\u036a\u036b\u0003\u00b0X\u0000\u036b\u036c\u0003\u00b2Y\u0000\u036c"+ - "\u00af\u0001\u0000\u0000\u0000\u036d\u036e\u0004X\u0010\u0000\u036e\u0370"+ - "\u0003\u001e\u000f\u0000\u036f\u0371\u0005\u0085\u0000\u0000\u0370\u036f"+ - "\u0001\u0000\u0000\u0000\u0370\u0371\u0001\u0000\u0000\u0000\u0371\u0372"+ - "\u0001\u0000\u0000\u0000\u0372\u0373\u0005i\u0000\u0000\u0373\u0376\u0001"+ - "\u0000\u0000\u0000\u0374\u0376\u0003\u001e\u000f\u0000\u0375\u036d\u0001"+ - "\u0000\u0000\u0000\u0375\u0374\u0001\u0000\u0000\u0000\u0376\u00b1\u0001"+ - "\u0000\u0000\u0000\u0377\u0378\u0005H\u0000\u0000\u0378\u037d\u0003\u00b4"+ - "Z\u0000\u0379\u037a\u0005<\u0000\u0000\u037a\u037c\u0003\u00b4Z\u0000"+ - "\u037b\u0379\u0001\u0000\u0000\u0000\u037c\u037f\u0001\u0000\u0000\u0000"+ - "\u037d\u037b\u0001\u0000\u0000\u0000\u037d\u037e\u0001\u0000\u0000\u0000"+ - "\u037e\u00b3\u0001\u0000\u0000\u0000\u037f\u037d\u0001\u0000\u0000\u0000"+ - "\u0380\u0381\u0003\u0090H\u0000\u0381\u00b5\u0001\u0000\u0000\u0000U\u00ba"+ - "\u00c2\u00cf\u00d9\u00f5\u0104\u010a\u0113\u0119\u0126\u012a\u0135\u0145"+ - "\u014d\u0151\u0158\u015e\u0163\u016c\u0173\u0179\u0182\u0189\u0191\u0199"+ - "\u019d\u01a1\u01a6\u01b1\u01b6\u01ba\u01c8\u01d3\u01d9\u01e0\u01e9\u0200"+ - "\u0208\u020b\u0212\u021d\u0224\u022c\u023a\u0243\u024e\u025c\u0263\u0276"+ - "\u027f\u0287\u028c\u0294\u0296\u029b\u02a2\u02a9\u02b2\u02b9\u02c2\u02c7"+ - "\u02cc\u02d6\u02dc\u02e4\u02e6\u02f1\u02f8\u0303\u0308\u030a\u0311\u0319"+ - "\u031c\u0326\u0337\u0342\u034d\u0352\u0358\u035b\u0360\u0370\u0375\u037d"; + "E\u0003E\u0275\bE\u0001E\u0001E\u0001E\u0001E\u0001E\u0005E\u027c\bE\n"+ + "E\fE\u027f\tE\u0001E\u0001E\u0001E\u0001E\u0001E\u0003E\u0286\bE\u0001"+ + "E\u0001E\u0001E\u0003E\u028b\bE\u0001E\u0001E\u0001E\u0001E\u0001E\u0001"+ + "E\u0005E\u0293\bE\nE\fE\u0296\tE\u0001F\u0001F\u0003F\u029a\bF\u0001F"+ + "\u0001F\u0001F\u0001F\u0001F\u0003F\u02a1\bF\u0001F\u0001F\u0001F\u0001"+ + "F\u0001F\u0003F\u02a8\bF\u0001F\u0001F\u0001F\u0001F\u0001F\u0005F\u02af"+ + "\bF\nF\fF\u02b2\tF\u0001F\u0001F\u0001F\u0001F\u0003F\u02b8\bF\u0001F"+ + "\u0001F\u0001F\u0001F\u0001F\u0005F\u02bf\bF\nF\fF\u02c2\tF\u0001F\u0001"+ + "F\u0003F\u02c6\bF\u0001G\u0001G\u0001G\u0003G\u02cb\bG\u0001G\u0001G\u0001"+ + "G\u0001H\u0001H\u0001H\u0001H\u0001H\u0003H\u02d5\bH\u0001I\u0001I\u0001"+ + "I\u0001I\u0003I\u02db\bI\u0001I\u0001I\u0001I\u0001I\u0001I\u0001I\u0005"+ + "I\u02e3\bI\nI\fI\u02e6\tI\u0001J\u0001J\u0001J\u0001J\u0001J\u0001J\u0001"+ + "J\u0001J\u0003J\u02f0\bJ\u0001J\u0001J\u0001J\u0005J\u02f5\bJ\nJ\fJ\u02f8"+ + "\tJ\u0001K\u0001K\u0001K\u0001K\u0001K\u0001K\u0005K\u0300\bK\nK\fK\u0303"+ + "\tK\u0001K\u0001K\u0003K\u0307\bK\u0003K\u0309\bK\u0001K\u0001K\u0001"+ + "L\u0001L\u0001L\u0003L\u0310\bL\u0001M\u0001M\u0001M\u0001M\u0005M\u0316"+ + "\bM\nM\fM\u0319\tM\u0003M\u031b\bM\u0001M\u0001M\u0001N\u0001N\u0001N"+ + "\u0001N\u0001O\u0001O\u0003O\u0325\bO\u0001P\u0001P\u0001P\u0001P\u0001"+ + "P\u0001P\u0001P\u0001P\u0001P\u0001P\u0001P\u0001P\u0001P\u0005P\u0334"+ + "\bP\nP\fP\u0337\tP\u0001P\u0001P\u0001P\u0001P\u0001P\u0001P\u0005P\u033f"+ + "\bP\nP\fP\u0342\tP\u0001P\u0001P\u0001P\u0001P\u0001P\u0001P\u0005P\u034a"+ + "\bP\nP\fP\u034d\tP\u0001P\u0001P\u0003P\u0351\bP\u0001Q\u0001Q\u0001R"+ + "\u0001R\u0003R\u0357\bR\u0001S\u0003S\u035a\bS\u0001S\u0001S\u0001T\u0003"+ + "T\u035f\bT\u0001T\u0001T\u0001U\u0001U\u0001V\u0001V\u0001W\u0001W\u0001"+ + "W\u0001W\u0001W\u0001X\u0001X\u0001X\u0003X\u036f\bX\u0001X\u0001X\u0001"+ + "X\u0003X\u0374\bX\u0001Y\u0001Y\u0001Y\u0001Y\u0005Y\u037a\bY\nY\fY\u037d"+ + "\tY\u0001Y\u0000\u0005\u0004v\u008a\u0092\u0094Z\u0000\u0002\u0004\u0006"+ + "\b\n\f\u000e\u0010\u0012\u0014\u0016\u0018\u001a\u001c\u001e \"$&(*,."+ + "02468:<>@BDFHJLNPRTVXZ\\^`bdfhjlnprtvxz|~\u0080\u0082\u0084\u0086\u0088"+ + "\u008a\u008c\u008e\u0090\u0092\u0094\u0096\u0098\u009a\u009c\u009e\u00a0"+ + "\u00a2\u00a4\u00a6\u00a8\u00aa\u00ac\u00ae\u00b0\u00b2\u0000\n\u0002\u0000"+ + "22ii\u0001\u0000cd\u0002\u000066==\u0002\u0000@@CC\u0002\u0000\'\'22\u0001"+ + "\u0000UV\u0001\u0000WY\u0002\u0000??LL\u0002\u0000NNPT\u0002\u0000\u0017"+ + "\u0017\u0019\u001a\u03a5\u0000\u00c0\u0001\u0000\u0000\u0000\u0002\u00c2"+ + "\u0001\u0000\u0000\u0000\u0004\u00c5\u0001\u0000\u0000\u0000\u0006\u00d7"+ + "\u0001\u0000\u0000\u0000\b\u00f3\u0001\u0000\u0000\u0000\n\u00f5\u0001"+ + "\u0000\u0000\u0000\f\u00f8\u0001\u0000\u0000\u0000\u000e\u00fa\u0001\u0000"+ + "\u0000\u0000\u0010\u00fd\u0001\u0000\u0000\u0000\u0012\u0108\u0001\u0000"+ + "\u0000\u0000\u0014\u010c\u0001\u0000\u0000\u0000\u0016\u0114\u0001\u0000"+ + "\u0000\u0000\u0018\u0119\u0001\u0000\u0000\u0000\u001a\u011c\u0001\u0000"+ + "\u0000\u0000\u001c\u011f\u0001\u0000\u0000\u0000\u001e\u0133\u0001\u0000"+ + "\u0000\u0000 \u0135\u0001\u0000\u0000\u0000\"\u0137\u0001\u0000\u0000"+ + "\u0000$\u0139\u0001\u0000\u0000\u0000&\u013b\u0001\u0000\u0000\u0000("+ + "\u013d\u0001\u0000\u0000\u0000*\u0146\u0001\u0000\u0000\u0000,\u0149\u0001"+ + "\u0000\u0000\u0000.\u0151\u0001\u0000\u0000\u00000\u0159\u0001\u0000\u0000"+ + "\u00002\u016a\u0001\u0000\u0000\u00004\u016c\u0001\u0000\u0000\u00006"+ + "\u0180\u0001\u0000\u0000\u00008\u0182\u0001\u0000\u0000\u0000:\u018a\u0001"+ + "\u0000\u0000\u0000<\u0192\u0001\u0000\u0000\u0000>\u0197\u0001\u0000\u0000"+ + "\u0000@\u019b\u0001\u0000\u0000\u0000B\u019f\u0001\u0000\u0000\u0000D"+ + "\u01a4\u0001\u0000\u0000\u0000F\u01a6\u0001\u0000\u0000\u0000H\u01a9\u0001"+ + "\u0000\u0000\u0000J\u01b2\u0001\u0000\u0000\u0000L\u01ba\u0001\u0000\u0000"+ + "\u0000N\u01bd\u0001\u0000\u0000\u0000P\u01c0\u0001\u0000\u0000\u0000R"+ + "\u01d1\u0001\u0000\u0000\u0000T\u01d3\u0001\u0000\u0000\u0000V\u01d9\u0001"+ + "\u0000\u0000\u0000X\u01e1\u0001\u0000\u0000\u0000Z\u01e7\u0001\u0000\u0000"+ + "\u0000\\\u01e9\u0001\u0000\u0000\u0000^\u01ed\u0001\u0000\u0000\u0000"+ + "`\u01f0\u0001\u0000\u0000\u0000b\u01f3\u0001\u0000\u0000\u0000d\u01f7"+ + "\u0001\u0000\u0000\u0000f\u01fa\u0001\u0000\u0000\u0000h\u020b\u0001\u0000"+ + "\u0000\u0000j\u0210\u0001\u0000\u0000\u0000l\u0214\u0001\u0000\u0000\u0000"+ + "n\u0217\u0001\u0000\u0000\u0000p\u0224\u0001\u0000\u0000\u0000r\u0228"+ + "\u0001\u0000\u0000\u0000t\u022c\u0001\u0000\u0000\u0000v\u0230\u0001\u0000"+ + "\u0000\u0000x\u023b\u0001\u0000\u0000\u0000z\u023d\u0001\u0000\u0000\u0000"+ + "|\u0248\u0001\u0000\u0000\u0000~\u0251\u0001\u0000\u0000\u0000\u0080\u0256"+ + "\u0001\u0000\u0000\u0000\u0082\u025c\u0001\u0000\u0000\u0000\u0084\u025f"+ + "\u0001\u0000\u0000\u0000\u0086\u0265\u0001\u0000\u0000\u0000\u0088\u0269"+ + "\u0001\u0000\u0000\u0000\u008a\u028a\u0001\u0000\u0000\u0000\u008c\u02c5"+ + "\u0001\u0000\u0000\u0000\u008e\u02c7\u0001\u0000\u0000\u0000\u0090\u02d4"+ + "\u0001\u0000\u0000\u0000\u0092\u02da\u0001\u0000\u0000\u0000\u0094\u02ef"+ + "\u0001\u0000\u0000\u0000\u0096\u02f9\u0001\u0000\u0000\u0000\u0098\u030f"+ + "\u0001\u0000\u0000\u0000\u009a\u0311\u0001\u0000\u0000\u0000\u009c\u031e"+ + "\u0001\u0000\u0000\u0000\u009e\u0324\u0001\u0000\u0000\u0000\u00a0\u0350"+ + "\u0001\u0000\u0000\u0000\u00a2\u0352\u0001\u0000\u0000\u0000\u00a4\u0356"+ + "\u0001\u0000\u0000\u0000\u00a6\u0359\u0001\u0000\u0000\u0000\u00a8\u035e"+ + "\u0001\u0000\u0000\u0000\u00aa\u0362\u0001\u0000\u0000\u0000\u00ac\u0364"+ + "\u0001\u0000\u0000\u0000\u00ae\u0366\u0001\u0000\u0000\u0000\u00b0\u0373"+ + "\u0001\u0000\u0000\u0000\u00b2\u0375\u0001\u0000\u0000\u0000\u00b4\u00b6"+ + "\u0004\u0000\u0000\u0000\u00b5\u00b7\u0003\u0086C\u0000\u00b6\u00b5\u0001"+ + "\u0000\u0000\u0000\u00b7\u00b8\u0001\u0000\u0000\u0000\u00b8\u00b6\u0001"+ + "\u0000\u0000\u0000\u00b8\u00b9\u0001\u0000\u0000\u0000\u00b9\u00ba\u0001"+ + "\u0000\u0000\u0000\u00ba\u00bb\u0003\u0002\u0001\u0000\u00bb\u00bc\u0005"+ + "\u0000\u0000\u0001\u00bc\u00c1\u0001\u0000\u0000\u0000\u00bd\u00be\u0003"+ + "\u0002\u0001\u0000\u00be\u00bf\u0005\u0000\u0000\u0001\u00bf\u00c1\u0001"+ + "\u0000\u0000\u0000\u00c0\u00b4\u0001\u0000\u0000\u0000\u00c0\u00bd\u0001"+ + "\u0000\u0000\u0000\u00c1\u0001\u0001\u0000\u0000\u0000\u00c2\u00c3\u0003"+ + "\u0004\u0002\u0000\u00c3\u00c4\u0005\u0000\u0000\u0001\u00c4\u0003\u0001"+ + "\u0000\u0000\u0000\u00c5\u00c6\u0006\u0002\uffff\uffff\u0000\u00c6\u00c7"+ + "\u0003\u0006\u0003\u0000\u00c7\u00cd\u0001\u0000\u0000\u0000\u00c8\u00c9"+ + "\n\u0001\u0000\u0000\u00c9\u00ca\u00051\u0000\u0000\u00ca\u00cc\u0003"+ + "\b\u0004\u0000\u00cb\u00c8\u0001\u0000\u0000\u0000\u00cc\u00cf\u0001\u0000"+ + "\u0000\u0000\u00cd\u00cb\u0001\u0000\u0000\u0000\u00cd\u00ce\u0001\u0000"+ + "\u0000\u0000\u00ce\u0005\u0001\u0000\u0000\u0000\u00cf\u00cd\u0001\u0000"+ + "\u0000\u0000\u00d0\u00d8\u0003\u0018\f\u0000\u00d1\u00d8\u0003\u000e\u0007"+ + "\u0000\u00d2\u00d8\u0003d2\u0000\u00d3\u00d4\u0004\u0003\u0002\u0000\u00d4"+ + "\u00d8\u0003\u001a\r\u0000\u00d5\u00d6\u0004\u0003\u0003\u0000\u00d6\u00d8"+ + "\u0003`0\u0000\u00d7\u00d0\u0001\u0000\u0000\u0000\u00d7\u00d1\u0001\u0000"+ + "\u0000\u0000\u00d7\u00d2\u0001\u0000\u0000\u0000\u00d7\u00d3\u0001\u0000"+ + "\u0000\u0000\u00d7\u00d5\u0001\u0000\u0000\u0000\u00d8\u0007\u0001\u0000"+ + "\u0000\u0000\u00d9\u00f4\u0003*\u0015\u0000\u00da\u00f4\u0003\n\u0005"+ + "\u0000\u00db\u00f4\u0003L&\u0000\u00dc\u00f4\u0003F#\u0000\u00dd\u00f4"+ + "\u0003,\u0016\u0000\u00de\u00f4\u0003H$\u0000\u00df\u00f4\u0003N\'\u0000"+ + "\u00e0\u00f4\u0003P(\u0000\u00e1\u00f4\u0003T*\u0000\u00e2\u00f4\u0003"+ + "\\.\u0000\u00e3\u00f4\u0003f3\u0000\u00e4\u00f4\u0003^/\u0000\u00e5\u00f4"+ + "\u0003\u00aeW\u0000\u00e6\u00f4\u0003n7\u0000\u00e7\u00f4\u0003|>\u0000"+ + "\u00e8\u00f4\u0003l6\u0000\u00e9\u00f4\u0003p8\u0000\u00ea\u00f4\u0003"+ + "z=\u0000\u00eb\u00ec\u0004\u0004\u0004\u0000\u00ec\u00f4\u0003\u0080@"+ + "\u0000\u00ed\u00ee\u0004\u0004\u0005\u0000\u00ee\u00f4\u0003~?\u0000\u00ef"+ + "\u00f0\u0004\u0004\u0006\u0000\u00f0\u00f4\u0003\u0082A\u0000\u00f1\u00f2"+ + "\u0004\u0004\u0007\u0000\u00f2\u00f4\u0003\u0084B\u0000\u00f3\u00d9\u0001"+ + "\u0000\u0000\u0000\u00f3\u00da\u0001\u0000\u0000\u0000\u00f3\u00db\u0001"+ + "\u0000\u0000\u0000\u00f3\u00dc\u0001\u0000\u0000\u0000\u00f3\u00dd\u0001"+ + "\u0000\u0000\u0000\u00f3\u00de\u0001\u0000\u0000\u0000\u00f3\u00df\u0001"+ + "\u0000\u0000\u0000\u00f3\u00e0\u0001\u0000\u0000\u0000\u00f3\u00e1\u0001"+ + "\u0000\u0000\u0000\u00f3\u00e2\u0001\u0000\u0000\u0000\u00f3\u00e3\u0001"+ + "\u0000\u0000\u0000\u00f3\u00e4\u0001\u0000\u0000\u0000\u00f3\u00e5\u0001"+ + "\u0000\u0000\u0000\u00f3\u00e6\u0001\u0000\u0000\u0000\u00f3\u00e7\u0001"+ + "\u0000\u0000\u0000\u00f3\u00e8\u0001\u0000\u0000\u0000\u00f3\u00e9\u0001"+ + "\u0000\u0000\u0000\u00f3\u00ea\u0001\u0000\u0000\u0000\u00f3\u00eb\u0001"+ + "\u0000\u0000\u0000\u00f3\u00ed\u0001\u0000\u0000\u0000\u00f3\u00ef\u0001"+ + "\u0000\u0000\u0000\u00f3\u00f1\u0001\u0000\u0000\u0000\u00f4\t\u0001\u0000"+ + "\u0000\u0000\u00f5\u00f6\u0005\u0011\u0000\u0000\u00f6\u00f7\u0003\u008a"+ + "E\u0000\u00f7\u000b\u0001\u0000\u0000\u0000\u00f8\u00f9\u0003<\u001e\u0000"+ + "\u00f9\r\u0001\u0000\u0000\u0000\u00fa\u00fb\u0005\r\u0000\u0000\u00fb"+ + "\u00fc\u0003\u0010\b\u0000\u00fc\u000f\u0001\u0000\u0000\u0000\u00fd\u0102"+ + "\u0003\u0012\t\u0000\u00fe\u00ff\u0005<\u0000\u0000\u00ff\u0101\u0003"+ + "\u0012\t\u0000\u0100\u00fe\u0001\u0000\u0000\u0000\u0101\u0104\u0001\u0000"+ + "\u0000\u0000\u0102\u0100\u0001\u0000\u0000\u0000\u0102\u0103\u0001\u0000"+ + "\u0000\u0000\u0103\u0011\u0001\u0000\u0000\u0000\u0104\u0102\u0001\u0000"+ + "\u0000\u0000\u0105\u0106\u00032\u0019\u0000\u0106\u0107\u00057\u0000\u0000"+ + "\u0107\u0109\u0001\u0000\u0000\u0000\u0108\u0105\u0001\u0000\u0000\u0000"+ + "\u0108\u0109\u0001\u0000\u0000\u0000\u0109\u010a\u0001\u0000\u0000\u0000"+ + "\u010a\u010b\u0003\u008aE\u0000\u010b\u0013\u0001\u0000\u0000\u0000\u010c"+ + "\u0111\u0003\u0016\u000b\u0000\u010d\u010e\u0005<\u0000\u0000\u010e\u0110"+ + "\u0003\u0016\u000b\u0000\u010f\u010d\u0001\u0000\u0000\u0000\u0110\u0113"+ + "\u0001\u0000\u0000\u0000\u0111\u010f\u0001\u0000\u0000\u0000\u0111\u0112"+ + "\u0001\u0000\u0000\u0000\u0112\u0015\u0001\u0000\u0000\u0000\u0113\u0111"+ + "\u0001\u0000\u0000\u0000\u0114\u0117\u00032\u0019\u0000\u0115\u0116\u0005"+ + "7\u0000\u0000\u0116\u0118\u0003\u008aE\u0000\u0117\u0115\u0001\u0000\u0000"+ + "\u0000\u0117\u0118\u0001\u0000\u0000\u0000\u0118\u0017\u0001\u0000\u0000"+ + "\u0000\u0119\u011a\u0005\u0013\u0000\u0000\u011a\u011b\u0003\u001c\u000e"+ + "\u0000\u011b\u0019\u0001\u0000\u0000\u0000\u011c\u011d\u0005\u0014\u0000"+ + "\u0000\u011d\u011e\u0003\u001c\u000e\u0000\u011e\u001b\u0001\u0000\u0000"+ + "\u0000\u011f\u0124\u0003\u001e\u000f\u0000\u0120\u0121\u0005<\u0000\u0000"+ + "\u0121\u0123\u0003\u001e\u000f\u0000\u0122\u0120\u0001\u0000\u0000\u0000"+ + "\u0123\u0126\u0001\u0000\u0000\u0000\u0124\u0122\u0001\u0000\u0000\u0000"+ + "\u0124\u0125\u0001\u0000\u0000\u0000\u0125\u0128\u0001\u0000\u0000\u0000"+ + "\u0126\u0124\u0001\u0000\u0000\u0000\u0127\u0129\u0003(\u0014\u0000\u0128"+ + "\u0127\u0001\u0000\u0000\u0000\u0128\u0129\u0001\u0000\u0000\u0000\u0129"+ + "\u001d\u0001\u0000\u0000\u0000\u012a\u012b\u0003 \u0010\u0000\u012b\u012c"+ + "\u0005:\u0000\u0000\u012c\u012d\u0003$\u0012\u0000\u012d\u0134\u0001\u0000"+ + "\u0000\u0000\u012e\u012f\u0003$\u0012\u0000\u012f\u0130\u00059\u0000\u0000"+ + "\u0130\u0131\u0003\"\u0011\u0000\u0131\u0134\u0001\u0000\u0000\u0000\u0132"+ + "\u0134\u0003&\u0013\u0000\u0133\u012a\u0001\u0000\u0000\u0000\u0133\u012e"+ + "\u0001\u0000\u0000\u0000\u0133\u0132\u0001\u0000\u0000\u0000\u0134\u001f"+ + "\u0001\u0000\u0000\u0000\u0135\u0136\u0005i\u0000\u0000\u0136!\u0001\u0000"+ + "\u0000\u0000\u0137\u0138\u0005i\u0000\u0000\u0138#\u0001\u0000\u0000\u0000"+ + "\u0139\u013a\u0005i\u0000\u0000\u013a%\u0001\u0000\u0000\u0000\u013b\u013c"+ + "\u0007\u0000\u0000\u0000\u013c\'\u0001\u0000\u0000\u0000\u013d\u013e\u0005"+ + "h\u0000\u0000\u013e\u0143\u0005i\u0000\u0000\u013f\u0140\u0005<\u0000"+ + "\u0000\u0140\u0142\u0005i\u0000\u0000\u0141\u013f\u0001\u0000\u0000\u0000"+ + "\u0142\u0145\u0001\u0000\u0000\u0000\u0143\u0141\u0001\u0000\u0000\u0000"+ + "\u0143\u0144\u0001\u0000\u0000\u0000\u0144)\u0001\u0000\u0000\u0000\u0145"+ + "\u0143\u0001\u0000\u0000\u0000\u0146\u0147\u0005\t\u0000\u0000\u0147\u0148"+ + "\u0003\u0010\b\u0000\u0148+\u0001\u0000\u0000\u0000\u0149\u014b\u0005"+ + "\u0010\u0000\u0000\u014a\u014c\u0003.\u0017\u0000\u014b\u014a\u0001\u0000"+ + "\u0000\u0000\u014b\u014c\u0001\u0000\u0000\u0000\u014c\u014f\u0001\u0000"+ + "\u0000\u0000\u014d\u014e\u00058\u0000\u0000\u014e\u0150\u0003\u0010\b"+ + "\u0000\u014f\u014d\u0001\u0000\u0000\u0000\u014f\u0150\u0001\u0000\u0000"+ + "\u0000\u0150-\u0001\u0000\u0000\u0000\u0151\u0156\u00030\u0018\u0000\u0152"+ + "\u0153\u0005<\u0000\u0000\u0153\u0155\u00030\u0018\u0000\u0154\u0152\u0001"+ + "\u0000\u0000\u0000\u0155\u0158\u0001\u0000\u0000\u0000\u0156\u0154\u0001"+ + "\u0000\u0000\u0000\u0156\u0157\u0001\u0000\u0000\u0000\u0157/\u0001\u0000"+ + "\u0000\u0000\u0158\u0156\u0001\u0000\u0000\u0000\u0159\u015c\u0003\u0012"+ + "\t\u0000\u015a\u015b\u0005\u0011\u0000\u0000\u015b\u015d\u0003\u008aE"+ + "\u0000\u015c\u015a\u0001\u0000\u0000\u0000\u015c\u015d\u0001\u0000\u0000"+ + "\u0000\u015d1\u0001\u0000\u0000\u0000\u015e\u015f\u0004\u0019\b\u0000"+ + "\u015f\u0161\u0005_\u0000\u0000\u0160\u0162\u0005c\u0000\u0000\u0161\u0160"+ + "\u0001\u0000\u0000\u0000\u0161\u0162\u0001\u0000\u0000\u0000\u0162\u0163"+ + "\u0001\u0000\u0000\u0000\u0163\u0164\u0005`\u0000\u0000\u0164\u0165\u0005"+ + ">\u0000\u0000\u0165\u0166\u0005_\u0000\u0000\u0166\u0167\u00034\u001a"+ + "\u0000\u0167\u0168\u0005`\u0000\u0000\u0168\u016b\u0001\u0000\u0000\u0000"+ + "\u0169\u016b\u00034\u001a\u0000\u016a\u015e\u0001\u0000\u0000\u0000\u016a"+ + "\u0169\u0001\u0000\u0000\u0000\u016b3\u0001\u0000\u0000\u0000\u016c\u0171"+ + "\u0003D\"\u0000\u016d\u016e\u0005>\u0000\u0000\u016e\u0170\u0003D\"\u0000"+ + "\u016f\u016d\u0001\u0000\u0000\u0000\u0170\u0173\u0001\u0000\u0000\u0000"+ + "\u0171\u016f\u0001\u0000\u0000\u0000\u0171\u0172\u0001\u0000\u0000\u0000"+ + "\u01725\u0001\u0000\u0000\u0000\u0173\u0171\u0001\u0000\u0000\u0000\u0174"+ + "\u0175\u0004\u001b\t\u0000\u0175\u0177\u0005_\u0000\u0000\u0176\u0178"+ + "\u0005\u0081\u0000\u0000\u0177\u0176\u0001\u0000\u0000\u0000\u0177\u0178"+ + "\u0001\u0000\u0000\u0000\u0178\u0179\u0001\u0000\u0000\u0000\u0179\u017a"+ + "\u0005`\u0000\u0000\u017a\u017b\u0005>\u0000\u0000\u017b\u017c\u0005_"+ + "\u0000\u0000\u017c\u017d\u00038\u001c\u0000\u017d\u017e\u0005`\u0000\u0000"+ + "\u017e\u0181\u0001\u0000\u0000\u0000\u017f\u0181\u00038\u001c\u0000\u0180"+ + "\u0174\u0001\u0000\u0000\u0000\u0180\u017f\u0001\u0000\u0000\u0000\u0181"+ + "7\u0001\u0000\u0000\u0000\u0182\u0187\u0003>\u001f\u0000\u0183\u0184\u0005"+ + ">\u0000\u0000\u0184\u0186\u0003>\u001f\u0000\u0185\u0183\u0001\u0000\u0000"+ + "\u0000\u0186\u0189\u0001\u0000\u0000\u0000\u0187\u0185\u0001\u0000\u0000"+ + "\u0000\u0187\u0188\u0001\u0000\u0000\u0000\u01889\u0001\u0000\u0000\u0000"+ + "\u0189\u0187\u0001\u0000\u0000\u0000\u018a\u018f\u00036\u001b\u0000\u018b"+ + "\u018c\u0005<\u0000\u0000\u018c\u018e\u00036\u001b\u0000\u018d\u018b\u0001"+ + "\u0000\u0000\u0000\u018e\u0191\u0001\u0000\u0000\u0000\u018f\u018d\u0001"+ + "\u0000\u0000\u0000\u018f\u0190\u0001\u0000\u0000\u0000\u0190;\u0001\u0000"+ + "\u0000\u0000\u0191\u018f\u0001\u0000\u0000\u0000\u0192\u0193\u0007\u0001"+ + "\u0000\u0000\u0193=\u0001\u0000\u0000\u0000\u0194\u0198\u0005\u0081\u0000"+ + "\u0000\u0195\u0198\u0003@ \u0000\u0196\u0198\u0003B!\u0000\u0197\u0194"+ + "\u0001\u0000\u0000\u0000\u0197\u0195\u0001\u0000\u0000\u0000\u0197\u0196"+ + "\u0001\u0000\u0000\u0000\u0198?\u0001\u0000\u0000\u0000\u0199\u019c\u0005"+ + "J\u0000\u0000\u019a\u019c\u0005]\u0000\u0000\u019b\u0199\u0001\u0000\u0000"+ + "\u0000\u019b\u019a\u0001\u0000\u0000\u0000\u019cA\u0001\u0000\u0000\u0000"+ + "\u019d\u01a0\u0005\\\u0000\u0000\u019e\u01a0\u0005^\u0000\u0000\u019f"+ + "\u019d\u0001\u0000\u0000\u0000\u019f\u019e\u0001\u0000\u0000\u0000\u01a0"+ + "C\u0001\u0000\u0000\u0000\u01a1\u01a5\u0003<\u001e\u0000\u01a2\u01a5\u0003"+ + "@ \u0000\u01a3\u01a5\u0003B!\u0000\u01a4\u01a1\u0001\u0000\u0000\u0000"+ + "\u01a4\u01a2\u0001\u0000\u0000\u0000\u01a4\u01a3\u0001\u0000\u0000\u0000"+ + "\u01a5E\u0001\u0000\u0000\u0000\u01a6\u01a7\u0005\u000b\u0000\u0000\u01a7"+ + "\u01a8\u0003\u00a0P\u0000\u01a8G\u0001\u0000\u0000\u0000\u01a9\u01aa\u0005"+ + "\u000f\u0000\u0000\u01aa\u01af\u0003J%\u0000\u01ab\u01ac\u0005<\u0000"+ + "\u0000\u01ac\u01ae\u0003J%\u0000\u01ad\u01ab\u0001\u0000\u0000\u0000\u01ae"+ + "\u01b1\u0001\u0000\u0000\u0000\u01af\u01ad\u0001\u0000\u0000\u0000\u01af"+ + "\u01b0\u0001\u0000\u0000\u0000\u01b0I\u0001\u0000\u0000\u0000\u01b1\u01af"+ + "\u0001\u0000\u0000\u0000\u01b2\u01b4\u0003\u008aE\u0000\u01b3\u01b5\u0007"+ + "\u0002\u0000\u0000\u01b4\u01b3\u0001\u0000\u0000\u0000\u01b4\u01b5\u0001"+ + "\u0000\u0000\u0000\u01b5\u01b8\u0001\u0000\u0000\u0000\u01b6\u01b7\u0005"+ + "G\u0000\u0000\u01b7\u01b9\u0007\u0003\u0000\u0000\u01b8\u01b6\u0001\u0000"+ + "\u0000\u0000\u01b8\u01b9\u0001\u0000\u0000\u0000\u01b9K\u0001\u0000\u0000"+ + "\u0000\u01ba\u01bb\u0005\u001e\u0000\u0000\u01bb\u01bc\u0003:\u001d\u0000"+ + "\u01bcM\u0001\u0000\u0000\u0000\u01bd\u01be\u0005\u001d\u0000\u0000\u01be"+ + "\u01bf\u0003:\u001d\u0000\u01bfO\u0001\u0000\u0000\u0000\u01c0\u01c1\u0005"+ + " \u0000\u0000\u01c1\u01c6\u0003R)\u0000\u01c2\u01c3\u0005<\u0000\u0000"+ + "\u01c3\u01c5\u0003R)\u0000\u01c4\u01c2\u0001\u0000\u0000\u0000\u01c5\u01c8"+ + "\u0001\u0000\u0000\u0000\u01c6\u01c4\u0001\u0000\u0000\u0000\u01c6\u01c7"+ + "\u0001\u0000\u0000\u0000\u01c7Q\u0001\u0000\u0000\u0000\u01c8\u01c6\u0001"+ + "\u0000\u0000\u0000\u01c9\u01ca\u00036\u001b\u0000\u01ca\u01cb\u0005\u0085"+ + "\u0000\u0000\u01cb\u01cc\u00036\u001b\u0000\u01cc\u01d2\u0001\u0000\u0000"+ + "\u0000\u01cd\u01ce\u00036\u001b\u0000\u01ce\u01cf\u00057\u0000\u0000\u01cf"+ + "\u01d0\u00036\u001b\u0000\u01d0\u01d2\u0001\u0000\u0000\u0000\u01d1\u01c9"+ + "\u0001\u0000\u0000\u0000\u01d1\u01cd\u0001\u0000\u0000\u0000\u01d2S\u0001"+ + "\u0000\u0000\u0000\u01d3\u01d4\u0005\b\u0000\u0000\u01d4\u01d5\u0003\u0094"+ + "J\u0000\u01d5\u01d7\u0003\u00aaU\u0000\u01d6\u01d8\u0003V+\u0000\u01d7"+ + "\u01d6\u0001\u0000\u0000\u0000\u01d7\u01d8\u0001\u0000\u0000\u0000\u01d8"+ + "U\u0001\u0000\u0000\u0000\u01d9\u01de\u0003X,\u0000\u01da\u01db\u0005"+ + "<\u0000\u0000\u01db\u01dd\u0003X,\u0000\u01dc\u01da\u0001\u0000\u0000"+ + "\u0000\u01dd\u01e0\u0001\u0000\u0000\u0000\u01de\u01dc\u0001\u0000\u0000"+ + "\u0000\u01de\u01df\u0001\u0000\u0000\u0000\u01dfW\u0001\u0000\u0000\u0000"+ + "\u01e0\u01de\u0001\u0000\u0000\u0000\u01e1\u01e2\u0003<\u001e\u0000\u01e2"+ + "\u01e3\u00057\u0000\u0000\u01e3\u01e4\u0003\u00a0P\u0000\u01e4Y\u0001"+ + "\u0000\u0000\u0000\u01e5\u01e6\u0005M\u0000\u0000\u01e6\u01e8\u0003\u009a"+ + "M\u0000\u01e7\u01e5\u0001\u0000\u0000\u0000\u01e7\u01e8\u0001\u0000\u0000"+ + "\u0000\u01e8[\u0001\u0000\u0000\u0000\u01e9\u01ea\u0005\n\u0000\u0000"+ + "\u01ea\u01eb\u0003\u0094J\u0000\u01eb\u01ec\u0003\u00aaU\u0000\u01ec]"+ + "\u0001\u0000\u0000\u0000\u01ed\u01ee\u0005\u001c\u0000\u0000\u01ee\u01ef"+ + "\u00032\u0019\u0000\u01ef_\u0001\u0000\u0000\u0000\u01f0\u01f1\u0005\u0006"+ + "\u0000\u0000\u01f1\u01f2\u0003b1\u0000\u01f2a\u0001\u0000\u0000\u0000"+ + "\u01f3\u01f4\u0005a\u0000\u0000\u01f4\u01f5\u0003\u0004\u0002\u0000\u01f5"+ + "\u01f6\u0005b\u0000\u0000\u01f6c\u0001\u0000\u0000\u0000\u01f7\u01f8\u0005"+ + "\"\u0000\u0000\u01f8\u01f9\u0005\u008c\u0000\u0000\u01f9e\u0001\u0000"+ + "\u0000\u0000\u01fa\u01fb\u0005\u0005\u0000\u0000\u01fb\u01fe\u0003h4\u0000"+ + "\u01fc\u01fd\u0005H\u0000\u0000\u01fd\u01ff\u00036\u001b\u0000\u01fe\u01fc"+ + "\u0001\u0000\u0000\u0000\u01fe\u01ff\u0001\u0000\u0000\u0000\u01ff\u0209"+ + "\u0001\u0000\u0000\u0000\u0200\u0201\u0005M\u0000\u0000\u0201\u0206\u0003"+ + "j5\u0000\u0202\u0203\u0005<\u0000\u0000\u0203\u0205\u0003j5\u0000\u0204"+ + "\u0202\u0001\u0000\u0000\u0000\u0205\u0208\u0001\u0000\u0000\u0000\u0206"+ + "\u0204\u0001\u0000\u0000\u0000\u0206\u0207\u0001\u0000\u0000\u0000\u0207"+ + "\u020a\u0001\u0000\u0000\u0000\u0208\u0206\u0001\u0000\u0000\u0000\u0209"+ + "\u0200\u0001\u0000\u0000\u0000\u0209\u020a\u0001\u0000\u0000\u0000\u020a"+ + "g\u0001\u0000\u0000\u0000\u020b\u020c\u0007\u0004\u0000\u0000\u020ci\u0001"+ + "\u0000\u0000\u0000\u020d\u020e\u00036\u001b\u0000\u020e\u020f\u00057\u0000"+ + "\u0000\u020f\u0211\u0001\u0000\u0000\u0000\u0210\u020d\u0001\u0000\u0000"+ + "\u0000\u0210\u0211\u0001\u0000\u0000\u0000\u0211\u0212\u0001\u0000\u0000"+ + "\u0000\u0212\u0213\u00036\u001b\u0000\u0213k\u0001\u0000\u0000\u0000\u0214"+ + "\u0215\u0005\u000e\u0000\u0000\u0215\u0216\u0003\u00a0P\u0000\u0216m\u0001"+ + "\u0000\u0000\u0000\u0217\u0218\u0005\u0004\u0000\u0000\u0218\u021b\u0003"+ + "2\u0019\u0000\u0219\u021a\u0005H\u0000\u0000\u021a\u021c\u00032\u0019"+ + "\u0000\u021b\u0219\u0001\u0000\u0000\u0000\u021b\u021c\u0001\u0000\u0000"+ + "\u0000\u021c\u0222\u0001\u0000\u0000\u0000\u021d\u021e\u0005\u0085\u0000"+ + "\u0000\u021e\u021f\u00032\u0019\u0000\u021f\u0220\u0005<\u0000\u0000\u0220"+ + "\u0221\u00032\u0019\u0000\u0221\u0223\u0001\u0000\u0000\u0000\u0222\u021d"+ + "\u0001\u0000\u0000\u0000\u0222\u0223\u0001\u0000\u0000\u0000\u0223o\u0001"+ + "\u0000\u0000\u0000\u0224\u0225\u0005\u0015\u0000\u0000\u0225\u0226\u0003"+ + "r9\u0000\u0226q\u0001\u0000\u0000\u0000\u0227\u0229\u0003t:\u0000\u0228"+ + "\u0227\u0001\u0000\u0000\u0000\u0229\u022a\u0001\u0000\u0000\u0000\u022a"+ + "\u0228\u0001\u0000\u0000\u0000\u022a\u022b\u0001\u0000\u0000\u0000\u022b"+ + "s\u0001\u0000\u0000\u0000\u022c\u022d\u0005a\u0000\u0000\u022d\u022e\u0003"+ + "v;\u0000\u022e\u022f\u0005b\u0000\u0000\u022fu\u0001\u0000\u0000\u0000"+ + "\u0230\u0231\u0006;\uffff\uffff\u0000\u0231\u0232\u0003x<\u0000\u0232"+ + "\u0238\u0001\u0000\u0000\u0000\u0233\u0234\n\u0001\u0000\u0000\u0234\u0235"+ + "\u00051\u0000\u0000\u0235\u0237\u0003x<\u0000\u0236\u0233\u0001\u0000"+ + "\u0000\u0000\u0237\u023a\u0001\u0000\u0000\u0000\u0238\u0236\u0001\u0000"+ + "\u0000\u0000\u0238\u0239\u0001\u0000\u0000\u0000\u0239w\u0001\u0000\u0000"+ + "\u0000\u023a\u0238\u0001\u0000\u0000\u0000\u023b\u023c\u0003\b\u0004\u0000"+ + "\u023cy\u0001\u0000\u0000\u0000\u023d\u0241\u0005\f\u0000\u0000\u023e"+ + "\u023f\u00032\u0019\u0000\u023f\u0240\u00057\u0000\u0000\u0240\u0242\u0001"+ + "\u0000\u0000\u0000\u0241\u023e\u0001\u0000\u0000\u0000\u0241\u0242\u0001"+ + "\u0000\u0000\u0000\u0242\u0243\u0001\u0000\u0000\u0000\u0243\u0244\u0003"+ + "\u00a0P\u0000\u0244\u0245\u0005H\u0000\u0000\u0245\u0246\u0003\u0014\n"+ + "\u0000\u0246\u0247\u0003Z-\u0000\u0247{\u0001\u0000\u0000\u0000\u0248"+ + "\u024c\u0005\u0007\u0000\u0000\u0249\u024a\u00032\u0019\u0000\u024a\u024b"+ + "\u00057\u0000\u0000\u024b\u024d\u0001\u0000\u0000\u0000\u024c\u0249\u0001"+ + "\u0000\u0000\u0000\u024c\u024d\u0001\u0000\u0000\u0000\u024d\u024e\u0001"+ + "\u0000\u0000\u0000\u024e\u024f\u0003\u0094J\u0000\u024f\u0250\u0003Z-"+ + "\u0000\u0250}\u0001\u0000\u0000\u0000\u0251\u0252\u0005\u001b\u0000\u0000"+ + "\u0252\u0253\u0003\u001e\u000f\u0000\u0253\u0254\u0005H\u0000\u0000\u0254"+ + "\u0255\u0003:\u001d\u0000\u0255\u007f\u0001\u0000\u0000\u0000\u0256\u0257"+ + "\u0005\u0012\u0000\u0000\u0257\u025a\u0003.\u0017\u0000\u0258\u0259\u0005"+ + "8\u0000\u0000\u0259\u025b\u0003\u0010\b\u0000\u025a\u0258\u0001\u0000"+ + "\u0000\u0000\u025a\u025b\u0001\u0000\u0000\u0000\u025b\u0081\u0001\u0000"+ + "\u0000\u0000\u025c\u025d\u0005\u001f\u0000\u0000\u025d\u025e\u0003:\u001d"+ + "\u0000\u025e\u0083\u0001\u0000\u0000\u0000\u025f\u0261\u0005\u0016\u0000"+ + "\u0000\u0260\u0262\u0003<\u001e\u0000\u0261\u0260\u0001\u0000\u0000\u0000"+ + "\u0261\u0262\u0001\u0000\u0000\u0000\u0262\u0263\u0001\u0000\u0000\u0000"+ + "\u0263\u0264\u0003Z-\u0000\u0264\u0085\u0001\u0000\u0000\u0000\u0265\u0266"+ + "\u0005!\u0000\u0000\u0266\u0267\u0003\u0088D\u0000\u0267\u0268\u0005;"+ + "\u0000\u0000\u0268\u0087\u0001\u0000\u0000\u0000\u0269\u026a\u0003<\u001e"+ + "\u0000\u026a\u026b\u00057\u0000\u0000\u026b\u026c\u0003\u00a0P\u0000\u026c"+ + "\u0089\u0001\u0000\u0000\u0000\u026d\u026e\u0006E\uffff\uffff\u0000\u026e"+ + "\u026f\u0005E\u0000\u0000\u026f\u028b\u0003\u008aE\b\u0270\u028b\u0003"+ + "\u0090H\u0000\u0271\u028b\u0003\u008cF\u0000\u0272\u0274\u0003\u0090H"+ + "\u0000\u0273\u0275\u0005E\u0000\u0000\u0274\u0273\u0001\u0000\u0000\u0000"+ + "\u0274\u0275\u0001\u0000\u0000\u0000\u0275\u0276\u0001\u0000\u0000\u0000"+ + "\u0276\u0277\u0005A\u0000\u0000\u0277\u0278\u0005a\u0000\u0000\u0278\u027d"+ + "\u0003\u0090H\u0000\u0279\u027a\u0005<\u0000\u0000\u027a\u027c\u0003\u0090"+ + "H\u0000\u027b\u0279\u0001\u0000\u0000\u0000\u027c\u027f\u0001\u0000\u0000"+ + "\u0000\u027d\u027b\u0001\u0000\u0000\u0000\u027d\u027e\u0001\u0000\u0000"+ + "\u0000\u027e\u0280\u0001\u0000\u0000\u0000\u027f\u027d\u0001\u0000\u0000"+ + "\u0000\u0280\u0281\u0005b\u0000\u0000\u0281\u028b\u0001\u0000\u0000\u0000"+ + "\u0282\u0283\u0003\u0090H\u0000\u0283\u0285\u0005B\u0000\u0000\u0284\u0286"+ + "\u0005E\u0000\u0000\u0285\u0284\u0001\u0000\u0000\u0000\u0285\u0286\u0001"+ + "\u0000\u0000\u0000\u0286\u0287\u0001\u0000\u0000\u0000\u0287\u0288\u0005"+ + "F\u0000\u0000\u0288\u028b\u0001\u0000\u0000\u0000\u0289\u028b\u0003\u008e"+ + "G\u0000\u028a\u026d\u0001\u0000\u0000\u0000\u028a\u0270\u0001\u0000\u0000"+ + "\u0000\u028a\u0271\u0001\u0000\u0000\u0000\u028a\u0272\u0001\u0000\u0000"+ + "\u0000\u028a\u0282\u0001\u0000\u0000\u0000\u028a\u0289\u0001\u0000\u0000"+ + "\u0000\u028b\u0294\u0001\u0000\u0000\u0000\u028c\u028d\n\u0005\u0000\u0000"+ + "\u028d\u028e\u00055\u0000\u0000\u028e\u0293\u0003\u008aE\u0006\u028f\u0290"+ + "\n\u0004\u0000\u0000\u0290\u0291\u0005I\u0000\u0000\u0291\u0293\u0003"+ + "\u008aE\u0005\u0292\u028c\u0001\u0000\u0000\u0000\u0292\u028f\u0001\u0000"+ + "\u0000\u0000\u0293\u0296\u0001\u0000\u0000\u0000\u0294\u0292\u0001\u0000"+ + "\u0000\u0000\u0294\u0295\u0001\u0000\u0000\u0000\u0295\u008b\u0001\u0000"+ + "\u0000\u0000\u0296\u0294\u0001\u0000\u0000\u0000\u0297\u0299\u0003\u0090"+ + "H\u0000\u0298\u029a\u0005E\u0000\u0000\u0299\u0298\u0001\u0000\u0000\u0000"+ + "\u0299\u029a\u0001\u0000\u0000\u0000\u029a\u029b\u0001\u0000\u0000\u0000"+ + "\u029b\u029c\u0005D\u0000\u0000\u029c\u029d\u0003\u00aaU\u0000\u029d\u02c6"+ + "\u0001\u0000\u0000\u0000\u029e\u02a0\u0003\u0090H\u0000\u029f\u02a1\u0005"+ + "E\u0000\u0000\u02a0\u029f\u0001\u0000\u0000\u0000\u02a0\u02a1\u0001\u0000"+ + "\u0000\u0000\u02a1\u02a2\u0001\u0000\u0000\u0000\u02a2\u02a3\u0005K\u0000"+ + "\u0000\u02a3\u02a4\u0003\u00aaU\u0000\u02a4\u02c6\u0001\u0000\u0000\u0000"+ + "\u02a5\u02a7\u0003\u0090H\u0000\u02a6\u02a8\u0005E\u0000\u0000\u02a7\u02a6"+ + "\u0001\u0000\u0000\u0000\u02a7\u02a8\u0001\u0000\u0000\u0000\u02a8\u02a9"+ + "\u0001\u0000\u0000\u0000\u02a9\u02aa\u0005D\u0000\u0000\u02aa\u02ab\u0005"+ + "a\u0000\u0000\u02ab\u02b0\u0003\u00aaU\u0000\u02ac\u02ad\u0005<\u0000"+ + "\u0000\u02ad\u02af\u0003\u00aaU\u0000\u02ae\u02ac\u0001\u0000\u0000\u0000"+ + "\u02af\u02b2\u0001\u0000\u0000\u0000\u02b0\u02ae\u0001\u0000\u0000\u0000"+ + "\u02b0\u02b1\u0001\u0000\u0000\u0000\u02b1\u02b3\u0001\u0000\u0000\u0000"+ + "\u02b2\u02b0\u0001\u0000\u0000\u0000\u02b3\u02b4\u0005b\u0000\u0000\u02b4"+ + "\u02c6\u0001\u0000\u0000\u0000\u02b5\u02b7\u0003\u0090H\u0000\u02b6\u02b8"+ + "\u0005E\u0000\u0000\u02b7\u02b6\u0001\u0000\u0000\u0000\u02b7\u02b8\u0001"+ + "\u0000\u0000\u0000\u02b8\u02b9\u0001\u0000\u0000\u0000\u02b9\u02ba\u0005"+ + "K\u0000\u0000\u02ba\u02bb\u0005a\u0000\u0000\u02bb\u02c0\u0003\u00aaU"+ + "\u0000\u02bc\u02bd\u0005<\u0000\u0000\u02bd\u02bf\u0003\u00aaU\u0000\u02be"+ + "\u02bc\u0001\u0000\u0000\u0000\u02bf\u02c2\u0001\u0000\u0000\u0000\u02c0"+ + "\u02be\u0001\u0000\u0000\u0000\u02c0\u02c1\u0001\u0000\u0000\u0000\u02c1"+ + "\u02c3\u0001\u0000\u0000\u0000\u02c2\u02c0\u0001\u0000\u0000\u0000\u02c3"+ + "\u02c4\u0005b\u0000\u0000\u02c4\u02c6\u0001\u0000\u0000\u0000\u02c5\u0297"+ + "\u0001\u0000\u0000\u0000\u02c5\u029e\u0001\u0000\u0000\u0000\u02c5\u02a5"+ + "\u0001\u0000\u0000\u0000\u02c5\u02b5\u0001\u0000\u0000\u0000\u02c6\u008d"+ + "\u0001\u0000\u0000\u0000\u02c7\u02ca\u00032\u0019\u0000\u02c8\u02c9\u0005"+ + "9\u0000\u0000\u02c9\u02cb\u0003\f\u0006\u0000\u02ca\u02c8\u0001\u0000"+ + "\u0000\u0000\u02ca\u02cb\u0001\u0000\u0000\u0000\u02cb\u02cc\u0001\u0000"+ + "\u0000\u0000\u02cc\u02cd\u0005:\u0000\u0000\u02cd\u02ce\u0003\u00a0P\u0000"+ + "\u02ce\u008f\u0001\u0000\u0000\u0000\u02cf\u02d5\u0003\u0092I\u0000\u02d0"+ + "\u02d1\u0003\u0092I\u0000\u02d1\u02d2\u0003\u00acV\u0000\u02d2\u02d3\u0003"+ + "\u0092I\u0000\u02d3\u02d5\u0001\u0000\u0000\u0000\u02d4\u02cf\u0001\u0000"+ + "\u0000\u0000\u02d4\u02d0\u0001\u0000\u0000\u0000\u02d5\u0091\u0001\u0000"+ + "\u0000\u0000\u02d6\u02d7\u0006I\uffff\uffff\u0000\u02d7\u02db\u0003\u0094"+ + "J\u0000\u02d8\u02d9\u0007\u0005\u0000\u0000\u02d9\u02db\u0003\u0092I\u0003"+ + "\u02da\u02d6\u0001\u0000\u0000\u0000\u02da\u02d8\u0001\u0000\u0000\u0000"+ + "\u02db\u02e4\u0001\u0000\u0000\u0000\u02dc\u02dd\n\u0002\u0000\u0000\u02dd"+ + "\u02de\u0007\u0006\u0000\u0000\u02de\u02e3\u0003\u0092I\u0003\u02df\u02e0"+ + "\n\u0001\u0000\u0000\u02e0\u02e1\u0007\u0005\u0000\u0000\u02e1\u02e3\u0003"+ + "\u0092I\u0002\u02e2\u02dc\u0001\u0000\u0000\u0000\u02e2\u02df\u0001\u0000"+ + "\u0000\u0000\u02e3\u02e6\u0001\u0000\u0000\u0000\u02e4\u02e2\u0001\u0000"+ + "\u0000\u0000\u02e4\u02e5\u0001\u0000\u0000\u0000\u02e5\u0093\u0001\u0000"+ + "\u0000\u0000\u02e6\u02e4\u0001\u0000\u0000\u0000\u02e7\u02e8\u0006J\uffff"+ + "\uffff\u0000\u02e8\u02f0\u0003\u00a0P\u0000\u02e9\u02f0\u00032\u0019\u0000"+ + "\u02ea\u02f0\u0003\u0096K\u0000\u02eb\u02ec\u0005a\u0000\u0000\u02ec\u02ed"+ + "\u0003\u008aE\u0000\u02ed\u02ee\u0005b\u0000\u0000\u02ee\u02f0\u0001\u0000"+ + "\u0000\u0000\u02ef\u02e7\u0001\u0000\u0000\u0000\u02ef\u02e9\u0001\u0000"+ + "\u0000\u0000\u02ef\u02ea\u0001\u0000\u0000\u0000\u02ef\u02eb\u0001\u0000"+ + "\u0000\u0000\u02f0\u02f6\u0001\u0000\u0000\u0000\u02f1\u02f2\n\u0001\u0000"+ + "\u0000\u02f2\u02f3\u00059\u0000\u0000\u02f3\u02f5\u0003\f\u0006\u0000"+ + "\u02f4\u02f1\u0001\u0000\u0000\u0000\u02f5\u02f8\u0001\u0000\u0000\u0000"+ + "\u02f6\u02f4\u0001\u0000\u0000\u0000\u02f6\u02f7\u0001\u0000\u0000\u0000"+ + "\u02f7\u0095\u0001\u0000\u0000\u0000\u02f8\u02f6\u0001\u0000\u0000\u0000"+ + "\u02f9\u02fa\u0003\u0098L\u0000\u02fa\u0308\u0005a\u0000\u0000\u02fb\u0309"+ + "\u0005W\u0000\u0000\u02fc\u0301\u0003\u008aE\u0000\u02fd\u02fe\u0005<"+ + "\u0000\u0000\u02fe\u0300\u0003\u008aE\u0000\u02ff\u02fd\u0001\u0000\u0000"+ + "\u0000\u0300\u0303\u0001\u0000\u0000\u0000\u0301\u02ff\u0001\u0000\u0000"+ + "\u0000\u0301\u0302\u0001\u0000\u0000\u0000\u0302\u0306\u0001\u0000\u0000"+ + "\u0000\u0303\u0301\u0001\u0000\u0000\u0000\u0304\u0305\u0005<\u0000\u0000"+ + "\u0305\u0307\u0003\u009aM\u0000\u0306\u0304\u0001\u0000\u0000\u0000\u0306"+ + "\u0307\u0001\u0000\u0000\u0000\u0307\u0309\u0001\u0000\u0000\u0000\u0308"+ + "\u02fb\u0001\u0000\u0000\u0000\u0308\u02fc\u0001\u0000\u0000\u0000\u0308"+ + "\u0309\u0001\u0000\u0000\u0000\u0309\u030a\u0001\u0000\u0000\u0000\u030a"+ + "\u030b\u0005b\u0000\u0000\u030b\u0097\u0001\u0000\u0000\u0000\u030c\u0310"+ + "\u0003D\"\u0000\u030d\u0310\u0005@\u0000\u0000\u030e\u0310\u0005C\u0000"+ + "\u0000\u030f\u030c\u0001\u0000\u0000\u0000\u030f\u030d\u0001\u0000\u0000"+ + "\u0000\u030f\u030e\u0001\u0000\u0000\u0000\u0310\u0099\u0001\u0000\u0000"+ + "\u0000\u0311\u031a\u0005Z\u0000\u0000\u0312\u0317\u0003\u009cN\u0000\u0313"+ + "\u0314\u0005<\u0000\u0000\u0314\u0316\u0003\u009cN\u0000\u0315\u0313\u0001"+ + "\u0000\u0000\u0000\u0316\u0319\u0001\u0000\u0000\u0000\u0317\u0315\u0001"+ + "\u0000\u0000\u0000\u0317\u0318\u0001\u0000\u0000\u0000\u0318\u031b\u0001"+ + "\u0000\u0000\u0000\u0319\u0317\u0001\u0000\u0000\u0000\u031a\u0312\u0001"+ + "\u0000\u0000\u0000\u031a\u031b\u0001\u0000\u0000\u0000\u031b\u031c\u0001"+ + "\u0000\u0000\u0000\u031c\u031d\u0005[\u0000\u0000\u031d\u009b\u0001\u0000"+ + "\u0000\u0000\u031e\u031f\u0003\u00aaU\u0000\u031f\u0320\u0005:\u0000\u0000"+ + "\u0320\u0321\u0003\u009eO\u0000\u0321\u009d\u0001\u0000\u0000\u0000\u0322"+ + "\u0325\u0003\u00a0P\u0000\u0323\u0325\u0003\u009aM\u0000\u0324\u0322\u0001"+ + "\u0000\u0000\u0000\u0324\u0323\u0001\u0000\u0000\u0000\u0325\u009f\u0001"+ + "\u0000\u0000\u0000\u0326\u0351\u0005F\u0000\u0000\u0327\u0328\u0003\u00a8"+ + "T\u0000\u0328\u0329\u0005c\u0000\u0000\u0329\u0351\u0001\u0000\u0000\u0000"+ + "\u032a\u0351\u0003\u00a6S\u0000\u032b\u0351\u0003\u00a8T\u0000\u032c\u0351"+ + "\u0003\u00a2Q\u0000\u032d\u0351\u0003@ \u0000\u032e\u0351\u0003\u00aa"+ + "U\u0000\u032f\u0330\u0005_\u0000\u0000\u0330\u0335\u0003\u00a4R\u0000"+ + "\u0331\u0332\u0005<\u0000\u0000\u0332\u0334\u0003\u00a4R\u0000\u0333\u0331"+ + "\u0001\u0000\u0000\u0000\u0334\u0337\u0001\u0000\u0000\u0000\u0335\u0333"+ + "\u0001\u0000\u0000\u0000\u0335\u0336\u0001\u0000\u0000\u0000\u0336\u0338"+ + "\u0001\u0000\u0000\u0000\u0337\u0335\u0001\u0000\u0000\u0000\u0338\u0339"+ + "\u0005`\u0000\u0000\u0339\u0351\u0001\u0000\u0000\u0000\u033a\u033b\u0005"+ + "_\u0000\u0000\u033b\u0340\u0003\u00a2Q\u0000\u033c\u033d\u0005<\u0000"+ + "\u0000\u033d\u033f\u0003\u00a2Q\u0000\u033e\u033c\u0001\u0000\u0000\u0000"+ + "\u033f\u0342\u0001\u0000\u0000\u0000\u0340\u033e\u0001\u0000\u0000\u0000"+ + "\u0340\u0341\u0001\u0000\u0000\u0000\u0341\u0343\u0001\u0000\u0000\u0000"+ + "\u0342\u0340\u0001\u0000\u0000\u0000\u0343\u0344\u0005`\u0000\u0000\u0344"+ + "\u0351\u0001\u0000\u0000\u0000\u0345\u0346\u0005_\u0000\u0000\u0346\u034b"+ + "\u0003\u00aaU\u0000\u0347\u0348\u0005<\u0000\u0000\u0348\u034a\u0003\u00aa"+ + "U\u0000\u0349\u0347\u0001\u0000\u0000\u0000\u034a\u034d\u0001\u0000\u0000"+ + "\u0000\u034b\u0349\u0001\u0000\u0000\u0000\u034b\u034c\u0001\u0000\u0000"+ + "\u0000\u034c\u034e\u0001\u0000\u0000\u0000\u034d\u034b\u0001\u0000\u0000"+ + "\u0000\u034e\u034f\u0005`\u0000\u0000\u034f\u0351\u0001\u0000\u0000\u0000"+ + "\u0350\u0326\u0001\u0000\u0000\u0000\u0350\u0327\u0001\u0000\u0000\u0000"+ + "\u0350\u032a\u0001\u0000\u0000\u0000\u0350\u032b\u0001\u0000\u0000\u0000"+ + "\u0350\u032c\u0001\u0000\u0000\u0000\u0350\u032d\u0001\u0000\u0000\u0000"+ + "\u0350\u032e\u0001\u0000\u0000\u0000\u0350\u032f\u0001\u0000\u0000\u0000"+ + "\u0350\u033a\u0001\u0000\u0000\u0000\u0350\u0345\u0001\u0000\u0000\u0000"+ + "\u0351\u00a1\u0001\u0000\u0000\u0000\u0352\u0353\u0007\u0007\u0000\u0000"+ + "\u0353\u00a3\u0001\u0000\u0000\u0000\u0354\u0357\u0003\u00a6S\u0000\u0355"+ + "\u0357\u0003\u00a8T\u0000\u0356\u0354\u0001\u0000\u0000\u0000\u0356\u0355"+ + "\u0001\u0000\u0000\u0000\u0357\u00a5\u0001\u0000\u0000\u0000\u0358\u035a"+ + "\u0007\u0005\u0000\u0000\u0359\u0358\u0001\u0000\u0000\u0000\u0359\u035a"+ + "\u0001\u0000\u0000\u0000\u035a\u035b\u0001\u0000\u0000\u0000\u035b\u035c"+ + "\u00054\u0000\u0000\u035c\u00a7\u0001\u0000\u0000\u0000\u035d\u035f\u0007"+ + "\u0005\u0000\u0000\u035e\u035d\u0001\u0000\u0000\u0000\u035e\u035f\u0001"+ + "\u0000\u0000\u0000\u035f\u0360\u0001\u0000\u0000\u0000\u0360\u0361\u0005"+ + "3\u0000\u0000\u0361\u00a9\u0001\u0000\u0000\u0000\u0362\u0363\u00052\u0000"+ + "\u0000\u0363\u00ab\u0001\u0000\u0000\u0000\u0364\u0365\u0007\b\u0000\u0000"+ + "\u0365\u00ad\u0001\u0000\u0000\u0000\u0366\u0367\u0007\t\u0000\u0000\u0367"+ + "\u0368\u0005s\u0000\u0000\u0368\u0369\u0003\u00b0X\u0000\u0369\u036a\u0003"+ + "\u00b2Y\u0000\u036a\u00af\u0001\u0000\u0000\u0000\u036b\u036c\u0004X\u0010"+ + "\u0000\u036c\u036e\u0003\u001e\u000f\u0000\u036d\u036f\u0005\u0085\u0000"+ + "\u0000\u036e\u036d\u0001\u0000\u0000\u0000\u036e\u036f\u0001\u0000\u0000"+ + "\u0000\u036f\u0370\u0001\u0000\u0000\u0000\u0370\u0371\u0005i\u0000\u0000"+ + "\u0371\u0374\u0001\u0000\u0000\u0000\u0372\u0374\u0003\u001e\u000f\u0000"+ + "\u0373\u036b\u0001\u0000\u0000\u0000\u0373\u0372\u0001\u0000\u0000\u0000"+ + "\u0374\u00b1\u0001\u0000\u0000\u0000\u0375\u0376\u0005H\u0000\u0000\u0376"+ + "\u037b\u0003\u008aE\u0000\u0377\u0378\u0005<\u0000\u0000\u0378\u037a\u0003"+ + "\u008aE\u0000\u0379\u0377\u0001\u0000\u0000\u0000\u037a\u037d\u0001\u0000"+ + "\u0000\u0000\u037b\u0379\u0001\u0000\u0000\u0000\u037b\u037c\u0001\u0000"+ + "\u0000\u0000\u037c\u00b3\u0001\u0000\u0000\u0000\u037d\u037b\u0001\u0000"+ + "\u0000\u0000U\u00b8\u00c0\u00cd\u00d7\u00f3\u0102\u0108\u0111\u0117\u0124"+ + "\u0128\u0133\u0143\u014b\u014f\u0156\u015c\u0161\u016a\u0171\u0177\u0180"+ + "\u0187\u018f\u0197\u019b\u019f\u01a4\u01af\u01b4\u01b8\u01c6\u01d1\u01d7"+ + "\u01de\u01e7\u01fe\u0206\u0209\u0210\u021b\u0222\u022a\u0238\u0241\u024c"+ + "\u025a\u0261\u0274\u027d\u0285\u028a\u0292\u0294\u0299\u02a0\u02a7\u02b0"+ + "\u02b7\u02c0\u02c5\u02ca\u02d4\u02da\u02e2\u02e4\u02ef\u02f6\u0301\u0306"+ + "\u0308\u030f\u0317\u031a\u0324\u0335\u0340\u034b\u0350\u0356\u0359\u035e"+ + "\u036e\u0373\u037b"; public static final ATN _ATN = new ATNDeserializer().deserialize(_serializedATN.toCharArray()); static { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserBaseListener.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserBaseListener.java index 0378f145f13e2..a3f175e4fa289 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserBaseListener.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserBaseListener.java @@ -1448,18 +1448,6 @@ public class EsqlBaseParserBaseListener implements EsqlBaseParserListener { *

The default implementation does nothing.

*/ @Override public void exitJoinCondition(EsqlBaseParser.JoinConditionContext ctx) { } - /** - * {@inheritDoc} - * - *

The default implementation does nothing.

- */ - @Override public void enterJoinPredicate(EsqlBaseParser.JoinPredicateContext ctx) { } - /** - * {@inheritDoc} - * - *

The default implementation does nothing.

- */ - @Override public void exitJoinPredicate(EsqlBaseParser.JoinPredicateContext ctx) { } /** * {@inheritDoc} diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserBaseVisitor.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserBaseVisitor.java index 22d5d21ade374..cae5b3bec3d29 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserBaseVisitor.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserBaseVisitor.java @@ -853,11 +853,4 @@ public class EsqlBaseParserBaseVisitor extends AbstractParseTreeVisitor im * {@link #visitChildren} on {@code ctx}.

*/ @Override public T visitJoinCondition(EsqlBaseParser.JoinConditionContext ctx) { return visitChildren(ctx); } - /** - * {@inheritDoc} - * - *

The default implementation returns the result of calling - * {@link #visitChildren} on {@code ctx}.

- */ - @Override public T visitJoinPredicate(EsqlBaseParser.JoinPredicateContext ctx) { return visitChildren(ctx); } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserListener.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserListener.java index 177eb70daf8e9..3ff17967633f9 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserListener.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserListener.java @@ -1287,14 +1287,4 @@ public interface EsqlBaseParserListener extends ParseTreeListener { * @param ctx the parse tree */ void exitJoinCondition(EsqlBaseParser.JoinConditionContext ctx); - /** - * Enter a parse tree produced by {@link EsqlBaseParser#joinPredicate}. - * @param ctx the parse tree - */ - void enterJoinPredicate(EsqlBaseParser.JoinPredicateContext ctx); - /** - * Exit a parse tree produced by {@link EsqlBaseParser#joinPredicate}. - * @param ctx the parse tree - */ - void exitJoinPredicate(EsqlBaseParser.JoinPredicateContext ctx); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserVisitor.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserVisitor.java index 6dc071ec03ab4..e2dc54e502ec7 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserVisitor.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserVisitor.java @@ -773,10 +773,4 @@ public interface EsqlBaseParserVisitor extends ParseTreeVisitor { * @return the visitor result */ T visitJoinCondition(EsqlBaseParser.JoinConditionContext ctx); - /** - * Visit a parse tree produced by {@link EsqlBaseParser#joinPredicate}. - * @param ctx the parse tree - * @return the visitor result - */ - T visitJoinPredicate(EsqlBaseParser.JoinPredicateContext ctx); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ExpressionBuilder.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ExpressionBuilder.java index e726b48888a73..f11473dabc21d 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ExpressionBuilder.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ExpressionBuilder.java @@ -611,8 +611,10 @@ public Expression visitComparison(EsqlBaseParser.ComparisonContext ctx) { Expression left = expression(ctx.left); Expression right = expression(ctx.right); TerminalNode op = (TerminalNode) ctx.comparisonOperator().getChild(0); + return buildComparison(source(ctx), left, right, op); + } - Source source = source(ctx); + private Expression buildComparison(Source source, Expression left, Expression right, TerminalNode op) { ZoneId zoneId = DateUtils.UTC; return switch (op.getSymbol().getType()) { @@ -623,7 +625,7 @@ public Expression visitComparison(EsqlBaseParser.ComparisonContext ctx) { case EsqlBaseParser.LTE -> new LessThanOrEqual(source, left, right, zoneId); case EsqlBaseParser.GT -> new GreaterThan(source, left, right, zoneId); case EsqlBaseParser.GTE -> new GreaterThanOrEqual(source, left, right, zoneId); - default -> throw new ParsingException(source, "Unknown comparison operator {}", source.text()); + default -> throw new ParsingException(source, "Unknown comparison operator {}", op.getText()); }; } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/LogicalPlanBuilder.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/LogicalPlanBuilder.java index 63e476f84c2b7..00517fca672a6 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/LogicalPlanBuilder.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/LogicalPlanBuilder.java @@ -44,6 +44,10 @@ import org.elasticsearch.xpack.esql.expression.Order; import org.elasticsearch.xpack.esql.expression.UnresolvedNamePattern; import org.elasticsearch.xpack.esql.expression.function.UnresolvedFunction; +import org.elasticsearch.xpack.esql.expression.predicate.Predicates; +import org.elasticsearch.xpack.esql.expression.predicate.logical.Not; +import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.Equals; +import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.EsqlBinaryComparison; import org.elasticsearch.xpack.esql.plan.EsqlStatement; import org.elasticsearch.xpack.esql.plan.IndexPattern; import org.elasticsearch.xpack.esql.plan.QuerySetting; @@ -92,6 +96,7 @@ import java.util.function.Function; import static java.util.Collections.emptyList; +import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION; import static org.elasticsearch.xpack.esql.core.util.StringUtils.WILDCARD; import static org.elasticsearch.xpack.esql.expression.NamedExpressions.mergeOutputExpressions; import static org.elasticsearch.xpack.esql.parser.ParserUtils.source; @@ -675,11 +680,46 @@ public PlanFactory visitJoinCommand(EsqlBaseParser.JoinCommandContext ctx) { ); var condition = ctx.joinCondition(); + var joinInfo = typedParsing(this, condition, JoinInfo.class); - // ON only with un-qualified names for now - var predicates = expressions(condition.joinPredicate()); - List joinFields = new ArrayList<>(predicates.size()); - for (var f : predicates) { + return p -> { + boolean hasRemotes = p.anyMatch(node -> { + if (node instanceof UnresolvedRelation r) { + return Arrays.stream(Strings.splitStringByCommaToArray(r.indexPattern().indexPattern())) + .anyMatch(RemoteClusterAware::isRemoteIndexName); + } else { + return false; + } + }); + if (hasRemotes && EsqlCapabilities.Cap.ENABLE_LOOKUP_JOIN_ON_REMOTE.isEnabled() == false) { + throw new ParsingException(source, "remote clusters are not supported with LOOKUP JOIN"); + } + return new LookupJoin(source, p, right, joinInfo.joinFields(), hasRemotes, Predicates.combineAnd(joinInfo.joinExpressions())); + }; + } + + private record JoinInfo(List joinFields, List joinExpressions) {} + + @Override + public JoinInfo visitJoinCondition(EsqlBaseParser.JoinConditionContext ctx) { + var expressions = visitList(this, ctx.booleanExpression(), Expression.class); + if (expressions.isEmpty()) { + throw new ParsingException(source(ctx), "JOIN ON clause cannot be empty"); + } + + // inspect the first expression to determine the type of join (field-based or expression-based) + boolean isFieldBased = expressions.get(0) instanceof UnresolvedAttribute; + + if (isFieldBased) { + return processFieldBasedJoin(expressions); + } else { + return processExpressionBasedJoin(expressions, ctx); + } + } + + private JoinInfo processFieldBasedJoin(List expressions) { + List joinFields = new ArrayList<>(expressions.size()); + for (var f : expressions) { // verify each field is an unresolved attribute if (f instanceof UnresolvedAttribute ua) { if (ua.qualifier() != null) { @@ -691,12 +731,56 @@ public PlanFactory visitJoinCommand(EsqlBaseParser.JoinCommandContext ctx) { } joinFields.add(ua); } else { - throw new ParsingException(f.source(), "JOIN ON clause only supports fields at the moment, found [{}]", f.sourceText()); + throw new ParsingException( + f.source(), + "JOIN ON clause must be a comma separated list of fields or a single expression, found [{}]", + f.sourceText() + ); } } + validateJoinFields(joinFields); + return new JoinInfo(joinFields, emptyList()); + } - var matchFieldsCount = joinFields.size(); - if (matchFieldsCount > 1) { + private JoinInfo processExpressionBasedJoin(List expressions, EsqlBaseParser.JoinConditionContext ctx) { + if (LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled() == false) { + throw new ParsingException(source(ctx), "JOIN ON clause only supports fields at the moment."); + } + List joinFields = new ArrayList<>(); + List joinExpressions = new ArrayList<>(); + if (expressions.size() != 1) { + throw new ParsingException( + source(ctx), + "JOIN ON clause with expressions only supports a single expression, found [{}]", + expressions + ); + } + expressions = Predicates.splitAnd(expressions.get(0)); + for (var f : expressions) { + addJoinExpression(f, joinFields, joinExpressions); + } + return new JoinInfo(joinFields, joinExpressions); + } + + private void addJoinExpression(Expression exp, List joinFields, List joinExpressions) { + exp = handleNegationOfEquals(exp); + if (exp instanceof EsqlBinaryComparison comparison + && comparison.left() instanceof UnresolvedAttribute left + && comparison.right() instanceof UnresolvedAttribute right) { + joinFields.add(left); + joinFields.add(right); + joinExpressions.add(exp); + } else { + throw new ParsingException( + exp.source(), + "JOIN ON clause only supports fields or AND of Binary Expressions at the moment, found [{}]", + exp.sourceText() + ); + } + } + + private void validateJoinFields(List joinFields) { + if (joinFields.size() > 1) { Set matchFieldNames = new LinkedHashSet<>(); for (Attribute field : joinFields) { if (matchFieldNames.add(field.name()) == false) { @@ -706,24 +790,16 @@ public PlanFactory visitJoinCommand(EsqlBaseParser.JoinCommandContext ctx) { field.name() ); } - } } + } - return p -> { - boolean hasRemotes = p.anyMatch(node -> { - if (node instanceof UnresolvedRelation r) { - return Arrays.stream(Strings.splitStringByCommaToArray(r.indexPattern().indexPattern())) - .anyMatch(RemoteClusterAware::isRemoteIndexName); - } else { - return false; - } - }); - if (hasRemotes && EsqlCapabilities.Cap.ENABLE_LOOKUP_JOIN_ON_REMOTE.isEnabled() == false) { - throw new ParsingException(source, "remote clusters are not supported with LOOKUP JOIN"); - } - return new LookupJoin(source, p, right, joinFields, hasRemotes); - }; + private Expression handleNegationOfEquals(Expression f) { + if (f instanceof Not not && not.children().size() == 1 && not.children().get(0) instanceof Equals equals) { + // we only support NOT on Equals, by converting it to NotEquals + return equals.negate(); + } + return f; } private void checkForRemoteClusters(LogicalPlan plan, Source source, String commandName) { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/InlineStats.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/InlineStats.java index be34631ec8149..889c544599185 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/InlineStats.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/InlineStats.java @@ -118,7 +118,7 @@ private JoinConfig joinConfig() { } } } - return new JoinConfig(JoinTypes.LEFT, namedGroupings, leftFields, rightFields); + return new JoinConfig(JoinTypes.LEFT, leftFields, rightFields, null); } @Override diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/Lookup.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/Lookup.java index 56dae7b1f16c0..697eff24006d8 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/Lookup.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/Lookup.java @@ -115,7 +115,7 @@ public JoinConfig joinConfig() { } } } - return new JoinConfig(JoinTypes.LEFT, matchFields, leftFields, rightFields); + return new JoinConfig(JoinTypes.LEFT, leftFields, rightFields, null); } @Override diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/join/InlineJoin.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/join/InlineJoin.java index f75430a64af1d..0a0ea2a350d19 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/join/InlineJoin.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/join/InlineJoin.java @@ -62,7 +62,7 @@ public static LogicalPlan stubSource(UnaryPlan sourcePlan, LogicalPlan target) { * Keep the join in place or replace it with an Eval in case no grouping is necessary. */ public static LogicalPlan inlineData(InlineJoin target, LocalRelation data) { - if (target.config().matchFields().isEmpty()) { + if (target.config().leftFields().isEmpty()) { List schema = data.output(); Block[] blocks = data.supplier().get(); List aliases = new ArrayList<>(schema.size()); @@ -165,11 +165,10 @@ public InlineJoin( LogicalPlan left, LogicalPlan right, JoinType type, - List matchFields, List leftFields, List rightFields ) { - super(source, left, right, type, matchFields, leftFields, rightFields); + super(source, left, right, type, leftFields, rightFields, null); } private static InlineJoin readFrom(StreamInput in) throws IOException { @@ -192,16 +191,7 @@ protected NodeInfo info() { // Do not just add the JoinConfig as a whole - this would prevent correctly registering the // expressions and references. JoinConfig config = config(); - return NodeInfo.create( - this, - InlineJoin::new, - left(), - right(), - config.type(), - config.matchFields(), - config.leftFields(), - config.rightFields() - ); + return NodeInfo.create(this, InlineJoin::new, left(), right(), config.type(), config.leftFields(), config.rightFields()); } @Override diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/join/Join.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/join/Join.java index ab552e6827d22..572eb63bd5f82 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/join/Join.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/join/Join.java @@ -16,6 +16,7 @@ import org.elasticsearch.xpack.esql.common.Failures; import org.elasticsearch.xpack.esql.core.expression.Attribute; import org.elasticsearch.xpack.esql.core.expression.AttributeSet; +import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.Expressions; import org.elasticsearch.xpack.esql.core.expression.ReferenceAttribute; import org.elasticsearch.xpack.esql.core.tree.NodeInfo; @@ -116,12 +117,12 @@ public Join( LogicalPlan left, LogicalPlan right, JoinType type, - List matchFields, List leftFields, - List rightFields + List rightFields, + Expression joinOnConditions ) { super(source, left, right); - this.config = new JoinConfig(type, matchFields, leftFields, rightFields); + this.config = new JoinConfig(type, leftFields, rightFields, joinOnConditions); } public Join(StreamInput in) throws IOException { @@ -169,9 +170,9 @@ protected NodeInfo info() { left(), right(), config.type(), - config.matchFields(), config.leftFields(), - config.rightFields() + config.rightFields(), + config.joinOnConditions() ); } @@ -222,10 +223,19 @@ public static List computeOutput(List leftOutput, List output; // TODO: make the other side nullable if (LEFT.equals(joinType)) { - // right side becomes nullable and overrides left except for join keys, which we preserve from the left - AttributeSet rightKeys = AttributeSet.of(config.rightFields()); - List rightOutputWithoutMatchFields = rightOutput.stream().filter(attr -> rightKeys.contains(attr) == false).toList(); - output = mergeOutputAttributes(rightOutputWithoutMatchFields, leftOutput); + if (config.joinOnConditions() == null) { + // right side becomes nullable and overrides left except for join keys, which we preserve from the left + AttributeSet rightKeys = AttributeSet.of(config.rightFields()); + List rightOutputWithoutMatchFields = rightOutput.stream() + .filter(attr -> rightKeys.contains(attr) == false) + .toList(); + output = mergeOutputAttributes(rightOutputWithoutMatchFields, leftOutput); + } else { + // We don't allow any attributes in the joinOnConditions that don't have unique names + // so right always overwrites left in case of name clashes + output = mergeOutputAttributes(rightOutput, leftOutput); + } + } else { throw new IllegalArgumentException(joinType.joinName() + " unsupported"); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/join/JoinConfig.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/join/JoinConfig.java index 383606d6ccbed..e824ca1a62a1c 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/join/JoinConfig.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/join/JoinConfig.java @@ -7,49 +7,85 @@ package org.elasticsearch.xpack.esql.plan.logical.join; +import org.elasticsearch.TransportVersions; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.core.Nullable; import org.elasticsearch.xpack.esql.core.capabilities.Resolvables; import org.elasticsearch.xpack.esql.core.expression.Attribute; +import org.elasticsearch.xpack.esql.core.expression.Expression; import java.io.IOException; import java.util.List; /** - * Configuration for a {@code JOIN} style operation. - * @param matchFields fields either from the left or right fields which decide which side is kept - * @param leftFields matched with the right fields - * @param rightFields matched with the left fields + * @param type type of join + * @param leftFields fields from the left child to join on + * @param rightFields fields from the right child to join on + * @param joinOnConditions join conditions for expression based joins. If null, we assume equi-join on the left/right fields */ -// TODO: this class needs refactoring into a more general form (expressions) since it's currently contains -// both the condition (equi-join) between the left and right field as well as the output of the join keys -// which makes sense only for USING clause - which is better resolved in the analyzer (based on the names) -// hence why for now the attributes are set inside the analyzer -public record JoinConfig(JoinType type, List matchFields, List leftFields, List rightFields) +public record JoinConfig(JoinType type, List leftFields, List rightFields, @Nullable Expression joinOnConditions) implements Writeable { + public JoinConfig(StreamInput in) throws IOException { - this( - JoinTypes.readFrom(in), - in.readNamedWriteableCollectionAsList(Attribute.class), - in.readNamedWriteableCollectionAsList(Attribute.class), - in.readNamedWriteableCollectionAsList(Attribute.class) - ); + this(JoinTypes.readFrom(in), readLeftFields(in), in.readNamedWriteableCollectionAsList(Attribute.class), readJoinConditions(in)); + } + + private static List readLeftFields(StreamInput in) throws IOException { + if (in.getTransportVersion().onOrAfter(TransportVersions.ESQL_LOOKUP_JOIN_ON_EXPRESSION) == false) { + // For BWC, the left fields were written twice (once as match fields) + // We read the first set and ignore them. + in.readNamedWriteableCollectionAsList(Attribute.class); + } + return in.readNamedWriteableCollectionAsList(Attribute.class); + } + + private static Expression readJoinConditions(StreamInput in) throws IOException { + if (in.getTransportVersion().onOrAfter(TransportVersions.ESQL_LOOKUP_JOIN_ON_EXPRESSION)) { + return in.readOptionalNamedWriteable(Expression.class); + } + return null; } @Override public void writeTo(StreamOutput out) throws IOException { type.writeTo(out); - out.writeNamedWriteableCollection(matchFields); + if (out.getTransportVersion().onOrAfter(TransportVersions.ESQL_LOOKUP_JOIN_ON_EXPRESSION) == false) { + out.writeNamedWriteableCollection(leftFields); + } out.writeNamedWriteableCollection(leftFields); out.writeNamedWriteableCollection(rightFields); + if (out.getTransportVersion().onOrAfter(TransportVersions.ESQL_LOOKUP_JOIN_ON_EXPRESSION)) { + out.writeOptionalNamedWriteable(joinOnConditions); + } else if (joinOnConditions != null) { + throw new IllegalArgumentException("LOOKUP JOIN with ON conditions is not supported on remote node"); + } } public boolean expressionsResolved() { return type.resolved() - && Resolvables.resolved(matchFields) && Resolvables.resolved(leftFields) - && Resolvables.resolved(rightFields); + && Resolvables.resolved(rightFields) + && (joinOnConditions == null || joinOnConditions.resolved()); } + + @Override + public String toString() { + return "JoinConfig[" + + "type=" + + type + + ", " + + "leftFields=" + + leftFields + + ", " + + "rightFields=" + + rightFields + + ", " + + "joinOnConditions=" + + joinOnConditions + + ']'; + } + } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/join/LookupJoin.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/join/LookupJoin.java index 20913e0e27ce7..41d713bf7f613 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/join/LookupJoin.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/join/LookupJoin.java @@ -7,10 +7,12 @@ package org.elasticsearch.xpack.esql.plan.logical.join; +import org.elasticsearch.core.Nullable; import org.elasticsearch.xpack.esql.capabilities.PostAnalysisVerificationAware; import org.elasticsearch.xpack.esql.capabilities.TelemetryAware; import org.elasticsearch.xpack.esql.common.Failures; import org.elasticsearch.xpack.esql.core.expression.Attribute; +import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.NodeInfo; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.plan.logical.Limit; @@ -26,15 +28,19 @@ /** * Lookup join - specialized LEFT (OUTER) JOIN between the main left side and a lookup index (index_mode = lookup) on the right. + * This is only used during parsing and substituted to a regular {@link Join} during analysis. */ public class LookupJoin extends Join implements SurrogateLogicalPlan, TelemetryAware, PostAnalysisVerificationAware { - public LookupJoin(Source source, LogicalPlan left, LogicalPlan right, List joinFields, boolean isRemote) { - this(source, left, right, new UsingJoinType(LEFT, joinFields), emptyList(), emptyList(), emptyList(), isRemote); - } - - public LookupJoin(Source source, LogicalPlan left, LogicalPlan right, List joinFields) { - this(source, left, right, new UsingJoinType(LEFT, joinFields), emptyList(), emptyList(), emptyList(), false); + public LookupJoin( + Source source, + LogicalPlan left, + LogicalPlan right, + List joinFields, + boolean isRemote, + @Nullable Expression joinOnConditions + ) { + this(source, left, right, new UsingJoinType(LEFT, joinFields), emptyList(), emptyList(), isRemote, joinOnConditions); } public LookupJoin( @@ -42,12 +48,12 @@ public LookupJoin( LogicalPlan left, LogicalPlan right, JoinType type, - List joinFields, List leftFields, List rightFields, - boolean isRemote + boolean isRemote, + Expression joinOnConditions ) { - this(source, left, right, new JoinConfig(type, joinFields, leftFields, rightFields), isRemote); + this(source, left, right, new JoinConfig(type, leftFields, rightFields, joinOnConditions), isRemote); } public LookupJoin(Source source, LogicalPlan left, LogicalPlan right, JoinConfig joinConfig) { @@ -80,16 +86,19 @@ protected NodeInfo info() { left(), right(), config().type(), - config().matchFields(), config().leftFields(), config().rightFields(), - isRemote() + isRemote(), + config().joinOnConditions() ); } @Override public String telemetryLabel() { - return "LOOKUP JOIN"; + if (config().joinOnConditions() == null) { + return "LOOKUP JOIN"; + } + return "LOOKUP JOIN ON EXPRESSION"; } @Override diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/HashJoinExec.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/HashJoinExec.java index 55a34af436f8a..0dd0451df46ca 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/HashJoinExec.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/HashJoinExec.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.esql.plan.physical; +import org.elasticsearch.TransportVersions; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -32,7 +33,6 @@ public class HashJoinExec extends BinaryExec implements EstimatesRowSize { HashJoinExec::new ); - private final List matchFields; private final List leftFields; private final List rightFields; private final List addedFields; @@ -43,13 +43,11 @@ public HashJoinExec( Source source, PhysicalPlan left, PhysicalPlan hashData, - List matchFields, List leftFields, List rightFields, List addedFields ) { super(source, left, hashData); - this.matchFields = matchFields; this.leftFields = leftFields; this.rightFields = rightFields; this.addedFields = addedFields; @@ -57,7 +55,9 @@ public HashJoinExec( private HashJoinExec(StreamInput in) throws IOException { super(Source.readFrom((PlanStreamInput) in), in.readNamedWriteable(PhysicalPlan.class), in.readNamedWriteable(PhysicalPlan.class)); - this.matchFields = in.readNamedWriteableCollectionAsList(Attribute.class); + if (in.getTransportVersion().onOrAfter(TransportVersions.ESQL_LOOKUP_JOIN_ON_EXPRESSION) == false) { + in.readNamedWriteableCollectionAsList(Attribute.class); + } this.leftFields = in.readNamedWriteableCollectionAsList(Attribute.class); this.rightFields = in.readNamedWriteableCollectionAsList(Attribute.class); this.addedFields = in.readNamedWriteableCollectionAsList(Attribute.class); @@ -66,7 +66,9 @@ private HashJoinExec(StreamInput in) throws IOException { @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); - out.writeNamedWriteableCollection(matchFields); + if (out.getTransportVersion().onOrAfter(TransportVersions.ESQL_LOOKUP_JOIN_ON_EXPRESSION) == false) { + out.writeNamedWriteableCollection(leftFields); + } out.writeNamedWriteableCollection(leftFields); out.writeNamedWriteableCollection(rightFields); out.writeNamedWriteableCollection(addedFields); @@ -81,10 +83,6 @@ public PhysicalPlan joinData() { return right(); } - public List matchFields() { - return matchFields; - } - public List leftFields() { return leftFields; } @@ -142,12 +140,12 @@ public AttributeSet rightReferences() { @Override public HashJoinExec replaceChildren(PhysicalPlan left, PhysicalPlan right) { - return new HashJoinExec(source(), left, right, matchFields, leftFields, rightFields, addedFields); + return new HashJoinExec(source(), left, right, leftFields, rightFields, addedFields); } @Override protected NodeInfo info() { - return NodeInfo.create(this, HashJoinExec::new, left(), right(), matchFields, leftFields, rightFields, addedFields); + return NodeInfo.create(this, HashJoinExec::new, left(), right(), leftFields, rightFields, addedFields); } @Override @@ -162,14 +160,11 @@ public boolean equals(Object o) { return false; } HashJoinExec hash = (HashJoinExec) o; - return matchFields.equals(hash.matchFields) - && leftFields.equals(hash.leftFields) - && rightFields.equals(hash.rightFields) - && addedFields.equals(hash.addedFields); + return leftFields.equals(hash.leftFields) && rightFields.equals(hash.rightFields) && addedFields.equals(hash.addedFields); } @Override public int hashCode() { - return Objects.hash(super.hashCode(), matchFields, leftFields, rightFields, addedFields); + return Objects.hash(super.hashCode(), leftFields, rightFields, addedFields); } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/LookupJoinExec.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/LookupJoinExec.java index 2aff38993aa98..8e251d79ba735 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/LookupJoinExec.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/LookupJoinExec.java @@ -7,11 +7,13 @@ package org.elasticsearch.xpack.esql.plan.physical; +import org.elasticsearch.TransportVersions; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.xpack.esql.core.expression.Attribute; import org.elasticsearch.xpack.esql.core.expression.AttributeSet; +import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.Expressions; import org.elasticsearch.xpack.esql.core.tree.NodeInfo; import org.elasticsearch.xpack.esql.core.tree.Source; @@ -37,6 +39,7 @@ public class LookupJoinExec extends BinaryExec implements EstimatesRowSize { * the right hand side by a {@link EsQueryExec}, and thus lose the information of which fields we'll get from the lookup index. */ private final List addedFields; + private final Expression joinOnConditions; private List lazyOutput; public LookupJoinExec( @@ -45,12 +48,14 @@ public LookupJoinExec( PhysicalPlan lookup, List leftFields, List rightFields, - List addedFields + List addedFields, + Expression joinOnConditions ) { super(source, left, lookup); this.leftFields = leftFields; this.rightFields = rightFields; this.addedFields = addedFields; + this.joinOnConditions = joinOnConditions; } private LookupJoinExec(StreamInput in) throws IOException { @@ -58,6 +63,11 @@ private LookupJoinExec(StreamInput in) throws IOException { this.leftFields = in.readNamedWriteableCollectionAsList(Attribute.class); this.rightFields = in.readNamedWriteableCollectionAsList(Attribute.class); this.addedFields = in.readNamedWriteableCollectionAsList(Attribute.class); + if (in.getTransportVersion().onOrAfter(TransportVersions.ESQL_LOOKUP_JOIN_ON_EXPRESSION)) { + this.joinOnConditions = in.readOptionalNamedWriteable(Expression.class); + } else { + this.joinOnConditions = null; + } } @Override @@ -66,6 +76,11 @@ public void writeTo(StreamOutput out) throws IOException { out.writeNamedWriteableCollection(leftFields); out.writeNamedWriteableCollection(rightFields); out.writeNamedWriteableCollection(addedFields); + if (out.getTransportVersion().onOrAfter(TransportVersions.ESQL_LOOKUP_JOIN_ON_EXPRESSION)) { + out.writeOptionalNamedWriteable(joinOnConditions); + } else if (joinOnConditions != null) { + throw new IllegalArgumentException("LOOKUP JOIN with ON conditions is not supported on remote node"); + } } @Override @@ -136,12 +151,12 @@ public AttributeSet rightReferences() { @Override public LookupJoinExec replaceChildren(PhysicalPlan left, PhysicalPlan right) { - return new LookupJoinExec(source(), left, right, leftFields, rightFields, addedFields); + return new LookupJoinExec(source(), left, right, leftFields, rightFields, addedFields, joinOnConditions); } @Override protected NodeInfo info() { - return NodeInfo.create(this, LookupJoinExec::new, left(), right(), leftFields, rightFields, addedFields); + return NodeInfo.create(this, LookupJoinExec::new, left(), right(), leftFields, rightFields, addedFields, joinOnConditions); } @Override @@ -156,11 +171,22 @@ public boolean equals(Object o) { return false; } LookupJoinExec other = (LookupJoinExec) o; - return leftFields.equals(other.leftFields) && rightFields.equals(other.rightFields) && addedFields.equals(other.addedFields); + return leftFields.equals(other.leftFields) + && rightFields.equals(other.rightFields) + && addedFields.equals(other.addedFields) + && Objects.equals(joinOnConditions, other.joinOnConditions); } @Override public int hashCode() { - return Objects.hash(super.hashCode(), leftFields, rightFields, addedFields); + return Objects.hash(super.hashCode(), leftFields, rightFields, addedFields, joinOnConditions); + } + + public boolean isOnJoinExpression() { + return joinOnConditions != null; + } + + public Expression joinOnConditions() { + return joinOnConditions; } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/LocalExecutionPlanner.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/LocalExecutionPlanner.java index d7d5393b1557b..09acb8dcdc041 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/LocalExecutionPlanner.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/LocalExecutionPlanner.java @@ -760,7 +760,26 @@ private PhysicalOperation planLookupJoin(LookupJoinExec join, LocalExecutionPlan if (input == null) { throw new IllegalArgumentException("can't plan [" + join + "][" + left + "]"); } - matchFields.add(new MatchConfig(right, input)); + + // TODO: Using exactAttribute was supposed to handle TEXT fields with KEYWORD subfields - but we don't allow these in lookup + // indices, so the call to exactAttribute looks redundant now. + String fieldName = right.exactAttribute().fieldName().string(); + + // we support 2 types of joins: Field name joins and Expression joins + // for Field name join, we do not ship any join on expression. + // we built the Lucene query on the field name that is passed in the MatchConfig.fieldName + // so for Field name we need to pass the attribute name from the right side, because that is needed to build the query + // For expression joins, we pass an expression such as left_id > right_id. + // So in this case we pass in left_id as the field name, because that is what we are shipping to the lookup node + // The lookup node will replace that name, with the actual values for each row and perform the lookup join + // We need to pass the left name, because we need to know what data we have shipped. + // It is not acceptable to just use the left or right side of the operator because the same field can be joined multiple times + // e.g. LOOKUP JOIN ON left_id < right_id_1 and left_id >= right_id_2 + // we want to be able to optimize this in the future and only ship the left_id once + if (join.isOnJoinExpression()) { + fieldName = left.name(); + } + matchFields.add(new MatchConfig(fieldName, input)); } return source.with( new LookupFromIndexOperator.Factory( @@ -773,7 +792,8 @@ private PhysicalOperation planLookupJoin(LookupJoinExec join, LocalExecutionPlan indexName, join.addedFields().stream().map(f -> (NamedExpression) f).toList(), join.source(), - join.right() + join.right(), + join.joinOnConditions() ), layout ); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/mapper/LocalMapper.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/mapper/LocalMapper.java index c0ac137f8b2c8..8aa9255f44dd3 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/mapper/LocalMapper.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/mapper/LocalMapper.java @@ -108,7 +108,6 @@ private PhysicalPlan mapBinary(BinaryPlan binary) { join.source(), left, localData, - config.matchFields(), config.leftFields(), config.rightFields(), join.rightOutputFields() @@ -140,9 +139,11 @@ private PhysicalPlan mapBinary(BinaryPlan binary) { fragmentExec, config.leftFields(), config.rightFields(), - join.rightOutputFields() + join.rightOutputFields(), + config.joinOnConditions() ); } return MapperUtils.unsupported(binary); } + } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/mapper/Mapper.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/mapper/Mapper.java index f515f6df91e1b..dd9b733d97872 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/mapper/Mapper.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/mapper/Mapper.java @@ -226,7 +226,6 @@ private PhysicalPlan mapBinary(BinaryPlan bp) { join.source(), left, localData, - config.matchFields(), config.leftFields(), config.rightFields(), join.rightOutputFields() @@ -241,7 +240,8 @@ private PhysicalPlan mapBinary(BinaryPlan bp) { right, config.leftFields(), config.rightFields(), - join.rightOutputFields() + join.rightOutputFields(), + config.joinOnConditions() ); } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/ParsingTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/ParsingTests.java index 927872ab23cb9..1bcebdeba5acc 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/ParsingTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/ParsingTests.java @@ -132,16 +132,20 @@ public void testTooBigQuery() { public void testJoinOnConstant() { assumeTrue("LOOKUP JOIN available as snapshot only", EsqlCapabilities.Cap.JOIN_LOOKUP_V12.isEnabled()); + assumeTrue( + "requires LOOKUP JOIN ON boolean expression capability", + EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled() + ); assertEquals( - "1:55: JOIN ON clause only supports fields at the moment, found [123]", + "1:55: JOIN ON clause only supports fields or AND of Binary Expressions at the moment, found [123]", error("row languages = 1, gender = \"f\" | lookup join test on 123") ); assertEquals( - "1:55: JOIN ON clause only supports fields at the moment, found [\"abc\"]", + "1:55: JOIN ON clause only supports fields or AND of Binary Expressions at the moment, found [\"abc\"]", error("row languages = 1, gender = \"f\" | lookup join test on \"abc\"") ); assertEquals( - "1:55: JOIN ON clause only supports fields at the moment, found [false]", + "1:55: JOIN ON clause only supports fields or AND of Binary Expressions at the moment, found [false]", error("row languages = 1, gender = \"f\" | lookup join test on false") ); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java index 285add57bf707..d74d855598e90 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java @@ -2218,6 +2218,60 @@ public void testLookupJoinDataTypeMismatch() { ); } + public void testLookupJoinExpressionAmbiguousRight() { + assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V12.isEnabled()); + assumeTrue( + "requires LOOKUP JOIN ON boolean expression capability", + EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled() + ); + String queryString = """ + from test + | rename languages as language_code + | lookup join languages_lookup ON salary == language_code + """; + + assertEquals( + " ambiguous reference to [language_code]; matches any of [line 2:10 [language_code], line 3:15 [language_code]]", + error(queryString) + ); + } + + public void testLookupJoinExpressionAmbiguousLeft() { + assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V12.isEnabled()); + assumeTrue( + "requires LOOKUP JOIN ON boolean expression capability", + EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled() + ); + String queryString = """ + from test + | rename languages as language_name + | lookup join languages_lookup ON language_name == language_code + """; + + assertEquals( + " ambiguous reference to [language_name]; matches any of [line 2:10 [language_name], line 3:15 [language_name]]", + error(queryString) + ); + } + + public void testLookupJoinExpressionAmbiguousBoth() { + assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V12.isEnabled()); + assumeTrue( + "requires LOOKUP JOIN ON boolean expression capability", + EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled() + ); + String queryString = """ + from test + | rename languages as language_code + | lookup join languages_lookup ON language_code != language_code + """; + + assertEquals( + " ambiguous reference to [language_code]; matches any of [line 2:10 [language_code], line 3:15 [language_code]]", + error(queryString) + ); + } + public void testFullTextFunctionOptions() { checkOptionDataTypes(Match.ALLOWED_OPTIONS, "FROM test | WHERE match(title, \"Jean\", {\"%s\": %s})"); checkOptionDataTypes(QueryString.ALLOWED_OPTIONS, "FROM test | WHERE QSTR(\"title: Jean\", {\"%s\": %s})"); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/enrich/LookupFromIndexOperatorTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/enrich/LookupFromIndexOperatorTests.java index 0c31581e514d0..c342377e7894f 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/enrich/LookupFromIndexOperatorTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/enrich/LookupFromIndexOperatorTests.java @@ -7,6 +7,8 @@ package org.elasticsearch.xpack.esql.enrich; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; + import org.apache.lucene.document.Field; import org.apache.lucene.document.IntField; import org.apache.lucene.document.LongField; @@ -61,15 +63,18 @@ import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.esql.action.EsqlCapabilities; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; +import org.elasticsearch.xpack.esql.core.expression.FoldContext; import org.elasticsearch.xpack.esql.core.expression.Literal; import org.elasticsearch.xpack.esql.core.expression.NamedExpression; import org.elasticsearch.xpack.esql.core.expression.ReferenceAttribute; -import org.elasticsearch.xpack.esql.core.tree.Location; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.core.type.EsField; +import org.elasticsearch.xpack.esql.expression.predicate.Predicates; +import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.EsqlBinaryComparison; import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.LessThan; import org.elasticsearch.xpack.esql.plan.logical.EsRelation; import org.elasticsearch.xpack.esql.plan.logical.Filter; @@ -87,34 +92,58 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.stream.LongStream; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.matchesPattern; import static org.mockito.Mockito.mock; public class LookupFromIndexOperatorTests extends AsyncOperatorTestCase { private static final int LOOKUP_SIZE = 1000; - private static final int LESS_THAN_VALUE = -40; + private static final int LESS_THAN_VALUE = 40; private final ThreadPool threadPool = threadPool(); private final Directory lookupIndexDirectory = newDirectory(); private final List releasables = new ArrayList<>(); private int numberOfJoinColumns; // we only allow 1 or 2 columns due to simpleInput() implementation + private EsqlBinaryComparison.BinaryComparisonOperation operation; + + @ParametersFactory + public static Iterable parametersFactory() { + List operations = new ArrayList<>(); + operations.add(new Object[] { null }); + if (EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled()) { + for (EsqlBinaryComparison.BinaryComparisonOperation operation : EsqlBinaryComparison.BinaryComparisonOperation.values()) { + // we skip NEQ because there are too many matches and the test can timeout + if (operation != EsqlBinaryComparison.BinaryComparisonOperation.NEQ) { + operations.add(new Object[] { operation }); + } + } + } + return operations; + } + + public LookupFromIndexOperatorTests(EsqlBinaryComparison.BinaryComparisonOperation operation) { + super(); + this.operation = operation; + } @Before public void buildLookupIndex() throws IOException { numberOfJoinColumns = 1 + randomInt(1); // 1 or 2 join columns try (RandomIndexWriter writer = new RandomIndexWriter(random(), lookupIndexDirectory)) { + String suffix = (operation == null) ? "" : ("_" + "right"); for (int i = 0; i < LOOKUP_SIZE; i++) { List fields = new ArrayList<>(); - fields.add(new LongField("match0", i, Field.Store.NO)); + fields.add(new LongField("match0" + suffix, i, Field.Store.NO)); if (numberOfJoinColumns == 2) { - fields.add(new LongField("match1", i + 1, Field.Store.NO)); + fields.add(new LongField("match1" + suffix, i + 1, Field.Store.NO)); } fields.add(new KeywordFieldMapper.KeywordField("lkwd", new BytesRef("l" + i), KeywordFieldMapper.Defaults.FIELD_TYPE)); - fields.add(new IntField("lint", -i, Field.Store.NO)); + fields.add(new IntField("lint", i, Field.Store.NO)); writer.addDocument(fields); } } @@ -141,8 +170,16 @@ protected void assertSimpleOutput(List input, List results) { * row count is the same. But lookup cuts into pages of length 256 so the * result is going to have more pages usually. */ - int count = results.stream().mapToInt(Page::getPositionCount).sum(); - assertThat(count, equalTo(input.stream().mapToInt(Page::getPositionCount).sum())); + int inputCount = input.stream().mapToInt(Page::getPositionCount).sum(); + int outputCount = results.stream().mapToInt(Page::getPositionCount).sum(); + + if (operation == null || operation.equals(EsqlBinaryComparison.BinaryComparisonOperation.EQ)) { + assertThat(outputCount, equalTo(input.stream().mapToInt(Page::getPositionCount).sum())); + } else { + // For lookup join on non-equality, output count should be >= input count + assertThat("Output count should be >= input count for left outer join", outputCount, greaterThanOrEqualTo(inputCount)); + } + for (Page r : results) { assertThat(r.getBlockCount(), equalTo(numberOfJoinColumns + 2)); LongVector match = r.getBlock(0).asVector(); @@ -150,17 +187,48 @@ protected void assertSimpleOutput(List input, List results) { IntBlock lintBlock = r.getBlock(numberOfJoinColumns + 1); for (int p = 0; p < r.getPositionCount(); p++) { long m = match.getLong(p); - if (m > Math.abs(LESS_THAN_VALUE)) { - assertThat(lkwdBlock.getBytesRef(lkwdBlock.getFirstValueIndex(p), new BytesRef()).utf8ToString(), equalTo("l" + m)); - assertThat(lintBlock.getInt(lintBlock.getFirstValueIndex(p)), equalTo((int) -m)); - } else { + if (lkwdBlock.isNull(p) || lintBlock.isNull(p)) { + // If the joined values are null, this means no match was found (or it was filtered out) + // verify that both the columns are null assertTrue("at " + p, lkwdBlock.isNull(p)); assertTrue("at " + p, lintBlock.isNull(p)); + } else { + String joinedLkwd = lkwdBlock.getBytesRef(lkwdBlock.getFirstValueIndex(p), new BytesRef()).utf8ToString(); + // there was a match, verify that the join on condition was satisfied + int joinedLint = lintBlock.getInt(lintBlock.getFirstValueIndex(p)); + boolean conditionSatisfied = compare(m, joinedLint, operation); + assertTrue("Join condition not satisfied: " + m + " " + operation + " " + joinedLint, conditionSatisfied); + // Verify that the joined lkwd matches the lint value + assertThat(joinedLkwd, equalTo("l" + joinedLint)); } } } } + /** + * Compares two values using the specified binary comparison operation. + * + * @param left the left operand + * @param right the right operand + * @param op the binary comparison operation (null means equality join) + * @return true if the comparison condition is satisfied, false otherwise + */ + private boolean compare(long left, long right, EsqlBinaryComparison.BinaryComparisonOperation op) { + if (op == null) { + // field based join is the same as equals comparison + op = EsqlBinaryComparison.BinaryComparisonOperation.EQ; + } + // Use the operator's fold method for comparison + Literal leftLiteral = new Literal(Source.EMPTY, left, DataType.LONG); + Literal rightLiteral = new Literal(Source.EMPTY, right, DataType.LONG); + EsqlBinaryComparison operatorInstance = op.buildNewInstance(Source.EMPTY, leftLiteral, rightLiteral); + Object result = operatorInstance.fold(FoldContext.small()); + if (result instanceof Boolean) { + return (Boolean) result; + } + throw new IllegalArgumentException("Operator fold did not return a boolean"); + } + @Override protected Operator.OperatorFactory simple(SimpleOptions options) { String sessionId = "test"; @@ -174,10 +242,31 @@ protected Operator.OperatorFactory simple(SimpleOptions options) { ); List matchFields = new ArrayList<>(); + String suffix = (operation == null) ? "" : ("_left"); for (int i = 0; i < numberOfJoinColumns; i++) { - FieldAttribute.FieldName matchField = new FieldAttribute.FieldName("match" + i); + String matchField = "match" + i + suffix; matchFields.add(new MatchConfig(matchField, i, inputDataType)); } + Expression joinOnExpression = null; + if (operation != null) { + List conditions = new ArrayList<>(); + for (int i = 0; i < numberOfJoinColumns; i++) { + String matchFieldLeft = "match" + i + "_left"; + String matchFieldRight = "match" + i + "_right"; + FieldAttribute left = new FieldAttribute( + Source.EMPTY, + matchFieldLeft, + new EsField(matchFieldLeft, inputDataType, Map.of(), true, EsField.TimeSeriesFieldType.NONE) + ); + FieldAttribute right = new FieldAttribute( + Source.EMPTY, + matchFieldRight, + new EsField(matchFieldRight.replace("left", "right"), inputDataType, Map.of(), true, EsField.TimeSeriesFieldType.NONE) + ); + conditions.add(operation.buildNewInstance(Source.EMPTY, left, right)); + } + joinOnExpression = Predicates.combineAnd(conditions); + } return new LookupFromIndexOperator.Factory( matchFields, @@ -189,7 +278,8 @@ protected Operator.OperatorFactory simple(SimpleOptions options) { lookupIndex, loadFields, Source.EMPTY, - buildLessThanFilter(LESS_THAN_VALUE) + buildLessThanFilter(LESS_THAN_VALUE), + joinOnExpression ); } @@ -199,11 +289,7 @@ private FragmentExec buildLessThanFilter(int value) { "lint", new EsField("lint", DataType.INTEGER, Collections.emptyMap(), true, EsField.TimeSeriesFieldType.NONE) ); - Expression lessThan = new LessThan( - new Source(new Location(0, 0), "lint < " + value), - filterAttribute, - new Literal(Source.EMPTY, value, DataType.INTEGER) - ); + Expression lessThan = new LessThan(Source.EMPTY, filterAttribute, new Literal(Source.EMPTY, value, DataType.INTEGER)); EsRelation esRelation = new EsRelation(Source.EMPTY, "test", IndexMode.LOOKUP, Map.of(), List.of()); Filter filter = new Filter(Source.EMPTY, esRelation, lessThan); return new FragmentExec(filter); @@ -229,9 +315,11 @@ public void testSimpleDescription() { @Override protected Matcher expectedToStringOfSimple() { StringBuilder sb = new StringBuilder(); + String suffix = (operation == null) ? "" : ("_left"); sb.append("LookupOperator\\[index=idx load_fields=\\[lkwd\\{r}#\\d+, lint\\{r}#\\d+] "); for (int i = 0; i < numberOfJoinColumns; i++) { - sb.append("input_type=LONG match_field=match").append(i).append(" inputChannel=").append(i).append(" "); + // match_field=match_left (index first, then suffix) + sb.append("input_type=LONG match_field=match").append(i).append(suffix).append(" inputChannel=").append(i).append(" "); } // Accept either the legacy physical plan rendering (FilterExec/EsQueryExec) or the new FragmentExec rendering sb.append("right_pre_join_plan=(?:"); @@ -243,13 +331,15 @@ protected Matcher expectedToStringOfSimple() { + "limit\\[\\],?\\s*sort\\[(?:\\[\\])?\\]\\s*estimatedRowSize\\[null\\]\\s*queryBuilderAndTags \\[(?:\\[\\]\\])\\]" ); sb.append("|"); - // New FragmentExec pattern + // New FragmentExec pattern - match the actual output format sb.append("FragmentExec\\[filter=null, estimatedRowSize=\\d+, reducer=\\[\\], fragment=\\[<>\\n") .append("Filter\\[lint\\{f}#\\d+ < ") .append(LESS_THAN_VALUE) .append("\\[INTEGER]]\\n") - .append("\\\\_EsRelation\\[test]\\[LOOKUP]\\[\\]<>\\]\\]\\]"); + .append("\\\\_EsRelation\\[test]\\[LOOKUP]\\[\\]<>\\]\\]"); sb.append(")"); + // Accept join_on_expression=null or a valid join predicate + sb.append(" join_on_expression=(null|match\\d+left [=!<>]+ match\\d+right( AND match\\d+left [=!<>]+ match\\d+right)*|)\\]"); return matchesPattern(sb.toString()); } @@ -326,25 +416,15 @@ private AbstractLookupService.LookupShardContextFactory lookupShardContextFactor return shardId -> { MapperServiceTestCase mapperHelper = new MapperServiceTestCase() { }; - StringBuilder stringBuilder = new StringBuilder(); - stringBuilder.append(""" - { - "doc": { "properties": { - "match0": { "type": "long" }, - """); + String suffix = (operation == null) ? "" : ("_right"); + StringBuilder props = new StringBuilder(); + props.append(String.format(Locale.ROOT, "\"match0%s\": { \"type\": \"long\" }", suffix)); if (numberOfJoinColumns == 2) { - stringBuilder.append(""" - "match1": { "type": "long" }, - """); + props.append(String.format(Locale.ROOT, ", \"match1%s\": { \"type\": \"long\" }", suffix)); } - stringBuilder.append(""" - "lkwd": { "type": "keyword" }, - "lint": { "type": "integer" } - }} - } - """); - - MapperService mapperService = mapperHelper.createMapperService(stringBuilder.toString()); + props.append(", \"lkwd\": { \"type\": \"keyword\" }, \"lint\": { \"type\": \"integer\" }"); + String mapping = String.format(Locale.ROOT, "{\n \"doc\": { \"properties\": { %s } }\n}", props.toString()); + MapperService mapperService = mapperHelper.createMapperService(mapping); DirectoryReader reader = DirectoryReader.open(lookupIndexDirectory); SearchExecutionContext executionCtx = mapperHelper.createSearchExecutionContext(mapperService, newSearcher(reader)); var ctx = new EsPhysicalOperationProviders.DefaultShardContext(0, new NoOpReleasable(), executionCtx, AliasFilter.EMPTY); @@ -375,4 +455,12 @@ protected MapMatcher extendStatusMatcher(MapMatcher mapMatcher, List input return mapMatcher.entry("total_rows", totalInputRows).entry("pages_emitted", output.size()).entry("rows_emitted", totalOutputRows); } + + @Override + public void testSimpleCircuitBreaking() { + // only test field based join and EQ to prevents timeouts in Ci + if (operation == null || operation.equals(EsqlBinaryComparison.BinaryComparisonOperation.EQ)) { + super.testSimpleCircuitBreaking(); + } + } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/enrich/MatchConfigSerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/enrich/MatchConfigSerializationTests.java index 2df9831b5bd0d..00ee70b670d42 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/enrich/MatchConfigSerializationTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/enrich/MatchConfigSerializationTests.java @@ -19,7 +19,6 @@ import org.elasticsearch.TransportVersion; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.test.AbstractWireSerializingTestCase; -import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput; import org.elasticsearch.xpack.esql.io.stream.PlanStreamOutput; @@ -54,7 +53,7 @@ private MatchConfig randomMatchConfig() { String name = randomAlphaOfLengthBetween(1, 100); int channel = randomInt(); DataType type = randomFrom(DataType.types()); - return new MatchConfig(new FieldAttribute.FieldName(name), channel, type); + return new MatchConfig(name, channel, type); } @Override @@ -66,8 +65,8 @@ private MatchConfig mutateMatchConfig(MatchConfig instance) { int i = randomIntBetween(1, 3); return switch (i) { case 1 -> { - String name = randomValueOtherThan(instance.fieldName().string(), () -> randomAlphaOfLengthBetween(1, 100)); - yield new MatchConfig(new FieldAttribute.FieldName(name), instance.channel(), instance.type()); + String name = randomValueOtherThan(instance.fieldName(), () -> randomAlphaOfLengthBetween(1, 100)); + yield new MatchConfig(name, instance.channel(), instance.type()); } case 2 -> { int channel = randomValueOtherThan(instance.channel(), () -> randomInt()); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java index 1a181fe805e81..13853ba4f45e2 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java @@ -89,6 +89,7 @@ import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.In; import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.InsensitiveEquals; import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.LessThan; +import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.NotEquals; import org.elasticsearch.xpack.esql.index.EsIndex; import org.elasticsearch.xpack.esql.index.IndexResolution; import org.elasticsearch.xpack.esql.optimizer.rules.logical.LiteralsOnTheRight; @@ -1032,10 +1033,10 @@ public void testPushdownLimitsPastLeftJoin() { var rightChild = new LocalRelation(Source.EMPTY, List.of(fieldAttribute()), EmptyLocalSupplier.EMPTY); assertNotEquals(leftChild, rightChild); - var joinConfig = new JoinConfig(JoinTypes.LEFT, List.of(), List.of(), List.of()); + var joinConfig = new JoinConfig(JoinTypes.LEFT, List.of(), List.of(), null); var join = switch (randomIntBetween(0, 2)) { case 0 -> new Join(EMPTY, leftChild, rightChild, joinConfig); - case 1 -> new LookupJoin(EMPTY, leftChild, rightChild, joinConfig); + case 1 -> new LookupJoin(EMPTY, leftChild, rightChild, joinConfig, false); case 2 -> new InlineJoin(EMPTY, leftChild, rightChild, joinConfig); default -> throw new IllegalArgumentException(); }; @@ -2509,6 +2510,37 @@ public void testPruneJoinOnNullMatchingField() { var source = as(limit.child(), EsRelation.class); } + /** + *
{@code
+     * Project[[emp_no{f}#10, first_name{f}#11, languages{f}#13, language_code_left{r}#3, language_code{r}#21, language_name{
+     * r}#22]]
+     * \_Eval[[null[INTEGER] AS language_code_left#3, null[INTEGER] AS language_code#21, null[KEYWORD] AS language_name#22]]
+     *   \_Limit[1000[INTEGER],false]
+     *     \_EsRelation[test][_meta_field{f}#16, emp_no{f}#10, first_name{f}#11, ..]
+     * }
+ */ + public void testPruneJoinOnNullMatchingFieldExpressionJoin() { + assumeTrue( + "requires LOOKUP JOIN ON boolean expression capability", + EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled() + ); + var plan = optimizedPlan(""" + from test + | eval language_code_left = null::integer + | keep emp_no, first_name, languages, language_code_left + | lookup join languages_lookup on language_code_left == language_code + """); + + var project = as(plan, Project.class); + assertThat( + Expressions.names(project.output()), + contains("emp_no", "first_name", "languages", "language_code_left", "language_code", "language_name") + ); + var eval = as(project.child(), Eval.class); + var limit = asLimit(eval.child(), 1000, false); + var source = as(limit.child(), EsRelation.class); + } + /** *
{@code
      * Project[[emp_no{f}#15, first_name{f}#16, my_null{r}#3 AS language_code#9, language_name{r}#27]]
@@ -2534,6 +2566,35 @@ public void testPruneJoinOnNullAssignedMatchingField() {
         var source = as(limit.child(), EsRelation.class);
     }
 
+    /**
+     * 
{@code
+     * Project[[emp_no{f}#16, first_name{f}#17, language_code{r}#27, language_name{r}#28]]
+     * \_Eval[[null[INTEGER] AS language_code#27, null[KEYWORD] AS language_name#28]]
+     *   \_Limit[1000[INTEGER],false]
+     *     \_EsRelation[test][_meta_field{f}#22, emp_no{f}#16, first_name{f}#17, ..]
+     * }
+ */ + public void testPruneJoinOnNullAssignedMatchingFieldExpr() { + assumeTrue( + "requires LOOKUP JOIN ON boolean expression capability", + EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled() + ); + var plan = optimizedPlan(""" + from test + | eval my_null = null::integer + | rename languages as language_code_right + | eval language_code_right = my_null + | lookup join languages_lookup on language_code_right > language_code + | keep emp_no, first_name, language_code, language_name + """); + + var project = as(plan, Project.class); + assertThat(Expressions.names(project.output()), contains("emp_no", "first_name", "language_code", "language_name")); + var eval = as(project.child(), Eval.class); + var limit = asLimit(eval.child(), 1000, false); + var source = as(limit.child(), EsRelation.class); + } + private static List orderNames(TopN topN) { return topN.order().stream().map(o -> as(o.child(), NamedExpression.class).name()).toList(); } @@ -5819,7 +5880,7 @@ public void testDoubleInlinestatsWithEvalGetsPrunedEntirely() { var ref = as(order.child(), FieldAttribute.class); assertThat(ref.name(), is("emp_no")); var inlineJoin = as(topN.child(), InlineJoin.class); - assertThat(Expressions.names(inlineJoin.config().matchFields()), is(List.of("salaryK"))); + assertThat(Expressions.names(inlineJoin.config().leftFields()), is(List.of("salaryK"))); // Left var eval = as(inlineJoin.left(), Eval.class); assertThat(Expressions.names(eval.fields()), is(List.of("salaryK"))); @@ -5885,7 +5946,7 @@ public void testInlinestatsGetsPrunedPartially() { assertThat(Expressions.names(project.projections()), is(List.of("x", "a", "emp_no"))); var upperLimit = asLimit(project.child(), 1, false); var inlineJoin = as(upperLimit.child(), InlineJoin.class); - assertThat(Expressions.names(inlineJoin.config().matchFields()), is(List.of("emp_no"))); + assertThat(Expressions.names(inlineJoin.config().leftFields()), is(List.of("emp_no"))); // Left var relation = as(inlineJoin.left(), EsRelation.class); // Right @@ -5914,7 +5975,7 @@ public void testTripleInlinestatsGetsPrunedPartially() { assertThat(Expressions.names(project.projections()), is(List.of("x", "a", "emp_no"))); var upperLimit = asLimit(project.child(), 1, false); var inlineJoin = as(upperLimit.child(), InlineJoin.class); - assertThat(Expressions.names(inlineJoin.config().matchFields()), is(List.of("emp_no"))); + assertThat(Expressions.names(inlineJoin.config().leftFields()), is(List.of("emp_no"))); // Left var relation = as(inlineJoin.left(), EsRelation.class); // Right @@ -6214,7 +6275,7 @@ public void testInlinestatsWithLookupJoin() { assertThat(order.nullsPosition(), equalTo(Order.NullsPosition.FIRST)); assertThat(Expressions.name(order.child()), equalTo("abbrev")); var join = as(topN.child(), Join.class); - assertThat(Expressions.names(join.config().matchFields()), is(List.of("scalerank"))); + assertThat(Expressions.names(join.config().leftFields()), is(List.of("scalerank"))); var left = as(join.left(), EsRelation.class); assertThat(left.concreteIndices(), is(Set.of("airports"))); var right = as(join.right(), EsRelation.class); @@ -6249,7 +6310,7 @@ public void testInlinestatsWithAvg() { assertThat(Expressions.names(esqlProject.projections()), is(List.of("avg", "emp_no", "first_name"))); var upperLimit = asLimit(esqlProject.child(), 10, false); var inlineJoin = as(upperLimit.child(), InlineJoin.class); - assertThat(Expressions.names(inlineJoin.config().matchFields()), is(List.of("emp_no"))); + assertThat(Expressions.names(inlineJoin.config().leftFields()), is(List.of("emp_no"))); // Left var relation = as(inlineJoin.left(), EsRelation.class); // Right @@ -6840,7 +6901,7 @@ public void testLookupSimple() { assertThat(limit.limit().fold(FoldContext.small()), equalTo(1000)); assertThat(join.config().type(), equalTo(JoinTypes.LEFT)); - assertThat(join.config().matchFields().stream().map(Object::toString).toList(), matchesList().item(startsWith("int{r}"))); + assertThat(join.config().leftFields().stream().map(Object::toString).toList(), matchesList().item(startsWith("int{r}"))); assertThat(join.config().leftFields().size(), equalTo(1)); assertThat(join.config().rightFields().size(), equalTo(1)); Attribute lhs = join.config().leftFields().get(0); @@ -6929,7 +6990,7 @@ public void testLookupStats() { as(left.child(), EsRelation.class); assertThat(join.config().type(), equalTo(JoinTypes.LEFT)); - assertThat(join.config().matchFields().stream().map(Object::toString).toList(), matchesList().item(startsWith("int{r}"))); + assertThat(join.config().leftFields().stream().map(Object::toString).toList(), matchesList().item(startsWith("int{r}"))); assertThat(join.config().leftFields().size(), equalTo(1)); assertThat(join.config().rightFields().size(), equalTo(1)); Attribute lhs = join.config().leftFields().get(0); @@ -9084,4 +9145,95 @@ public void testDecayScaleMustBeLiteral() { ); assertThat(e.getMessage(), containsString("has non-literal value [scale]")); } + + /** + * + * Project[[languages{f}#8, language_code{f}#16, language_name{f}#17]] + * \_Limit[1000[INTEGER],true] + * \_Join[LEFT,[languages{f}#8, language_code{f}#16],[languages{f}#8],[language_code{f}#16],languages{f}#8 == language_code{ + * f}#16] + * |_Limit[1000[INTEGER],false] + * | \_EsRelation[test][_meta_field{f}#11, emp_no{f}#5, first_name{f}#6, ge..] + * \_EsRelation[languages_lookup][LOOKUP][language_code{f}#16, language_name{f}#17] + * + */ + public void testLookupJoinExpressionSwapped() { + assumeTrue( + "requires LOOKUP JOIN ON boolean expression capability", + EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled() + ); + LogicalPlan plan = optimizedPlan(""" + from test + | keep languages + | lookup join languages_lookup ON language_code == languages + """); + var project = as(plan, Project.class); + var limit = asLimit(project.child(), 1000, true); + var join = as(limit.child(), Join.class); + assertEquals("language_code == languages", join.config().joinOnConditions().toString()); + var equals = as(join.config().joinOnConditions(), Equals.class); + // we expect left and right to be swapped + var left = as(equals.left(), Attribute.class); + var right = as(equals.right(), Attribute.class); + assertEquals("language_code", right.name()); + assertEquals("languages", left.name()); + var limitPastJoin = asLimit(join.left(), 1000, false); + as(limitPastJoin.child(), EsRelation.class); + as(join.right(), EsRelation.class); + } + + /** + * + * Project[[_meta_field{f}#15, emp_no{f}#9, first_name{f}#10, gender{f}#11, hire_date{f}#16, job{f}#17, job.raw{f}#18, la + * nguages{f}#12 AS language_code_left#4, last_name{f}#13, long_noidx{f}#19, salary{f}#14, language_code{f}#20, language_name{f}#21]] + * \_Limit[1000[INTEGER],true] + * \_Join[LEFT,[languages{f}#12, languages{f}#12],[language_code{f}#20, language_code{f}#20],languages{f}#12 != language_co + * de{f}#20 AND languages{f}#12 > language_code{f}#20] + * |_Limit[1000[INTEGER],false] + * | \_EsRelation[test][_meta_field{f}#15, emp_no{f}#9, first_name{f}#10, g..] + * \_EsRelation[languages_lookup][LOOKUP][language_code{f}#20, language_name{f}#21] + * + */ + public void testLookupJoinExpressionSameAttrsDifferentConditions() { + assumeTrue( + "requires LOOKUP JOIN ON boolean expression capability", + EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled() + ); + String query = """ + from test + | rename languages as language_code_left + | lookup join languages_lookup ON language_code_left != language_code and language_code_left > language_code + """; + + LogicalPlan plan = optimizedPlan(query); + var project = as(plan, Project.class); + var limit = asLimit(project.child(), 1000, true); + var join = as(limit.child(), Join.class); + + // Verify the join conditions contain both != and > operators + var joinConditions = join.config().joinOnConditions(); + assertThat(joinConditions, instanceOf(And.class)); + var and = as(joinConditions, And.class); + + // Check the left condition (should be !=) + var notEquals = as(and.left(), NotEquals.class); + var leftAttr = as(notEquals.left(), Attribute.class); + var rightAttr = as(notEquals.right(), Attribute.class); + assertEquals("languages", leftAttr.name()); + assertEquals("language_code", rightAttr.name()); + + // Check the right condition (should be >) + var greaterThan = as(and.right(), GreaterThan.class); + var leftAttrGT = as(greaterThan.left(), Attribute.class); + var rightAttrGT = as(greaterThan.right(), Attribute.class); + assertEquals("languages", leftAttrGT.name()); + assertEquals("language_code", rightAttrGT.name()); + + // Verify the left side of join has Limit then EsRelation + var limitPastJoin = asLimit(join.left(), 1000, false); + as(limitPastJoin.child(), EsRelation.class); + + // Verify the right side of join is EsRelation with LOOKUP + as(join.right(), EsRelation.class); + } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java index 9e255d9322e73..31401469c0572 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java @@ -2936,15 +2936,7 @@ public void testVerifierOnMissingReferencesWithBinaryPlans() throws Exception { var planWithInvalidJoinRightSide = plan.transformUp( LookupJoinExec.class, // LookupJoinExec.rightReferences() is currently EMPTY (hack); use a HashJoinExec instead. - join -> new HashJoinExec( - join.source(), - join.left(), - join.left(), - join.leftFields(), - join.leftFields(), - join.rightFields(), - join.output() - ) + join -> new HashJoinExec(join.source(), join.left(), join.left(), join.leftFields(), join.rightFields(), join.output()) ); e = expectThrows(IllegalStateException.class, () -> physicalPlanOptimizer.verify(planWithInvalidJoinRightSide, plan.output())); @@ -7489,7 +7481,7 @@ public void testLookupSimple() { } PhysicalPlan plan = physicalPlan(query); var join = as(plan, HashJoinExec.class); - assertMap(join.matchFields().stream().map(Object::toString).toList(), matchesList().item(startsWith("int{r}"))); + assertMap(join.leftFields().stream().map(Object::toString).toList(), matchesList().item(startsWith("int{r}"))); assertMap( join.output().stream().map(Object::toString).toList(), matchesList().item(startsWith("_meta_field{f}")) @@ -7542,7 +7534,7 @@ public void testLookupThenProject() { var outerProject = as(plan, ProjectExec.class); assertThat(outerProject.projections().toString(), containsString("AS lang_name")); var join = as(outerProject.child(), HashJoinExec.class); - assertMap(join.matchFields().stream().map(Object::toString).toList(), matchesList().item(startsWith("int{r}"))); + assertMap(join.leftFields().stream().map(Object::toString).toList(), matchesList().item(startsWith("int{r}"))); assertMap( join.output().stream().map(Object::toString).toList(), matchesList().item(startsWith("_meta_field{f}")) @@ -7610,7 +7602,7 @@ public void testLookupThenTopN() { ); Join join = as(innerTopN.child(), Join.class); assertThat(join.config().type(), equalTo(JoinTypes.LEFT)); - assertMap(join.config().matchFields().stream().map(Objects::toString).toList(), matchesList().item(startsWith("int{r}"))); + assertMap(join.config().leftFields().stream().map(Objects::toString).toList(), matchesList().item(startsWith("int{r}"))); Project innerProject = as(join.left(), Project.class); assertThat(innerProject.projections(), hasSize(10)); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownAndCombineFiltersTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownAndCombineFiltersTests.java index 11c64a82e3f57..e1797d6c2c16a 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownAndCombineFiltersTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownAndCombineFiltersTests.java @@ -9,12 +9,14 @@ import org.elasticsearch.index.IndexMode; import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.xpack.esql.action.EsqlCapabilities; import org.elasticsearch.xpack.esql.core.expression.Alias; import org.elasticsearch.xpack.esql.core.expression.Attribute; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; import org.elasticsearch.xpack.esql.core.expression.FoldContext; import org.elasticsearch.xpack.esql.core.expression.MetadataAttribute; +import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.expression.function.aggregate.Count; import org.elasticsearch.xpack.esql.expression.function.fulltext.Match; @@ -375,7 +377,7 @@ private static EsRelation relation(List fieldAttributes) { } public void testPushDownFilterPastLeftJoinWithPushable() { - Join join = createLeftJoin(); + Join join = createLeftJoinOnFields(); EsRelation left = (EsRelation) join.left(); FieldAttribute c = (FieldAttribute) join.right().output().get(0); @@ -393,7 +395,7 @@ public void testPushDownFilterPastLeftJoinWithPushable() { } public void testPushDownFilterPastLeftJoinWithExistingFilter() { - Join join = createLeftJoin(); + Join join = createLeftJoinOnFields(); EsRelation left = (EsRelation) join.left(); FieldAttribute c = (FieldAttribute) join.right().output().get(0); @@ -434,7 +436,7 @@ public void testPushDownFilterPastLeftJoinWithExistingFilter() { } public void testDoNotPushDownExistingFilterAgain() { - Join join = createLeftJoin(); + Join join = createLeftJoinOnFields(); EsRelation left = (EsRelation) join.left(); FieldAttribute c = (FieldAttribute) join.right().output().get(0); @@ -461,7 +463,7 @@ public void testDoNotPushDownExistingFilterAgain() { } public void testPushDownFilterPastLeftJoinWithExistingFilterCalledTwice() { - Join join = createLeftJoin(); + Join join = createLeftJoinOnFields(); EsRelation left = (EsRelation) join.left(); FieldAttribute c = (FieldAttribute) join.right().output().get(0); @@ -488,7 +490,7 @@ public void testPushDownFilterPastLeftJoinWithExistingFilterCalledTwice() { } public void testPushDownFilterPastLeftJoinWithNonPushable() { - Join join = createLeftJoin(); + Join join = createLeftJoinOnFields(); FieldAttribute c = (FieldAttribute) join.right().output().get(0); // Non-pushable filter @@ -503,7 +505,7 @@ public void testPushDownFilterPastLeftJoinWithNonPushable() { } public void testPushDownFilterPastLeftJoinWithPartiallyPushableAnd() { - Join join = createLeftJoin(); + Join join = createLeftJoinOnFields(); EsRelation left = (EsRelation) join.left(); FieldAttribute c = (FieldAttribute) join.right().output().get(0); @@ -525,7 +527,7 @@ public void testPushDownFilterPastLeftJoinWithPartiallyPushableAnd() { } public void testPushDownFilterPastLeftJoinWithOr() { - Join join = createLeftJoin(); + Join join = createLeftJoinOnFields(); FieldAttribute c = (FieldAttribute) join.right().output().get(0); Expression pushableCondition = greaterThanOf(c, ONE); @@ -543,7 +545,7 @@ public void testPushDownFilterPastLeftJoinWithOr() { } public void testPushDownFilterPastLeftJoinWithNotButStillPushable() { - Join join = createLeftJoin(); + Join join = createLeftJoinOnFields(); FieldAttribute c = (FieldAttribute) join.right().output().get(0); Expression pushableCondition = greaterThanOf(c, ONE); @@ -560,7 +562,7 @@ public void testPushDownFilterPastLeftJoinWithNotButStillPushable() { } public void testPushDownFilterPastLeftJoinWithNotNonPushable() { - Join join = createLeftJoin(); + Join join = createLeftJoinOnFields(); FieldAttribute c = (FieldAttribute) join.right().output().get(0); Expression nonPushableCondition = new IsNull(EMPTY, c); @@ -586,7 +588,7 @@ public void testPushDownFilterPastLeftJoinWithComplexMix() { FieldAttribute g = getFieldAttribute("g"); EsRelation left = relation(List.of(a, getFieldAttribute("b"))); EsRelation right = relation(List.of(c, d, e, f, g)); - JoinConfig joinConfig = new JoinConfig(JoinTypes.LEFT, List.of(a), List.of(a), List.of(c)); + JoinConfig joinConfig = new JoinConfig(JoinTypes.LEFT, List.of(a), List.of(c), null); Join join = new Join(EMPTY, left, right, joinConfig); // Predicates @@ -830,14 +832,177 @@ public void testPushDownFilterPastTwoLookupJoins() { assertThat(rightFilter.condition().toString(), is("language_name > \"a\"")); } - private Join createLeftJoin() { + private Join createLeftJoinOnFields() { FieldAttribute a = getFieldAttribute("a"); FieldAttribute b = getFieldAttribute("b"); FieldAttribute c = getFieldAttribute("c"); EsRelation left = relation(List.of(a, b)); EsRelation right = relation(List.of(c, b)); - JoinConfig joinConfig = new JoinConfig(JoinTypes.LEFT, List.of(b), List.of(a, b), List.of(b, c)); + JoinConfig joinConfig = new JoinConfig(JoinTypes.LEFT, List.of(a, b), List.of(b, c), null); return new Join(EMPTY, left, right, joinConfig); } + + private Join createLeftJoinOnExpression() { + FieldAttribute a = getFieldAttribute("a"); + FieldAttribute b1 = getFieldAttribute("b1"); + FieldAttribute b2 = getFieldAttribute("b2"); + FieldAttribute c = getFieldAttribute("c"); + EsRelation left = relation(List.of(a, b1)); + EsRelation right = relation(List.of(c, b2)); + Expression joinOnCondition = new GreaterThanOrEqual(Source.EMPTY, b1, b2); + JoinConfig joinConfig = new JoinConfig(JoinTypes.LEFT, List.of(b1), List.of(b2), joinOnCondition); + return new Join(EMPTY, left, right, joinConfig); + } + + public void testLeftJoinOnExpressionPushable() { + Join join = createLeftJoinOnExpression(); + EsRelation left = (EsRelation) join.left(); + FieldAttribute c = (FieldAttribute) join.right().output().get(0); + + // Pushable filter + Expression pushableCondition = greaterThanOf(c, ONE); + Filter filter = new Filter(EMPTY, join, pushableCondition); + LogicalPlan optimized = new PushDownAndCombineFilters().apply(filter, optimizerContext); + // The filter should still be on top + Filter topFilter = as(optimized, Filter.class); + assertEquals(pushableCondition, topFilter.condition()); + Join optimizedJoin = as(topFilter.child(), Join.class); + assertEquals(left, optimizedJoin.left()); + Filter rightFilter = as(optimizedJoin.right(), Filter.class); + assertEquals(pushableCondition, rightFilter.condition()); + } + + public void testLeftJoinOnExpressionPushableLeftAndRight() { + Join join = createLeftJoinOnExpression(); + FieldAttribute a = (FieldAttribute) join.left().output().get(0); + FieldAttribute c = (FieldAttribute) join.right().output().get(0); + + // Pushable filters on both left and right + Expression leftPushableCondition = greaterThanOf(a, ONE); + Expression rightPushableCondition = greaterThanOf(c, TWO); + Expression combinedCondition = Predicates.combineAnd(List.of(leftPushableCondition, rightPushableCondition)); + Filter filter = new Filter(EMPTY, join, combinedCondition); + LogicalPlan optimized = new PushDownAndCombineFilters().apply(filter, optimizerContext); + + // The top filter should only contain the right pushable condition + // because left pushable conditions are completely pushed down and removed from the top + Filter topFilter = as(optimized, Filter.class); + assertEquals(rightPushableCondition, topFilter.condition()); + Join optimizedJoin = as(topFilter.child(), Join.class); + + // Check that the left side has the left pushable filter + Filter leftFilter = as(optimizedJoin.left(), Filter.class); + assertEquals(leftPushableCondition, leftFilter.condition()); + + // Check that the right side has the right pushable filter + Filter rightFilter = as(optimizedJoin.right(), Filter.class); + assertEquals(rightPushableCondition, rightFilter.condition()); + } + + public void testPushDownFilterPastLeftJoinExpressionWithPartiallyPushableAnd() { + Join join = createLeftJoinOnExpression(); + EsRelation left = (EsRelation) join.left(); + FieldAttribute c = (FieldAttribute) join.right().output().get(0); + + Expression pushableCondition = greaterThanOf(c, ONE); + Expression nonPushableCondition = new IsNull(EMPTY, c); + + // Partially pushable filter + Expression partialCondition = new And(EMPTY, pushableCondition, nonPushableCondition); + Filter filter = new Filter(EMPTY, join, partialCondition); + LogicalPlan optimized = new PushDownAndCombineFilters().apply(filter, optimizerContext); + Filter topFilter = as(optimized, Filter.class); + // The top filter condition should be the original one + assertEquals(partialCondition, topFilter.condition()); + Join optimizedJoin = as(topFilter.child(), Join.class); + assertEquals(left, optimizedJoin.left()); + Filter rightFilter = as(optimizedJoin.right(), Filter.class); + // Only the pushable part should be a candidate + assertEquals(pushableCondition, rightFilter.condition()); + } + + /** + *Limit[1000[INTEGER],false] + * \_Filter[ISNULL(language_name{f}#17)] + * \_Join[LEFT,[languages{f}#8, language_code{f}#16],[languages{f}#8],[language_code{f}#16],languages{f}#8 > language_code{f + * }#16] + * |_EsRelation[test][_meta_field{f}#11, emp_no{f}#5, first_name{f}#6, ge..] + * \_EsRelation[languages_lookup][LOOKUP][language_code{f}#16, language_name{f}#17] + * + */ + public void testDoNotPushDownIsNullFilterPastLookupJoinExpression() { + assumeTrue( + "requires LOOKUP JOIN ON boolean expression capability", + EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled() + ); + var plan = plan(""" + FROM test + | LOOKUP JOIN languages_lookup ON languages > language_code + | WHERE language_name IS NULL + """); + + var limit = as(plan, Limit.class); + var filter = as(limit.child(), Filter.class); + var join = as(filter.child(), Join.class); + assertThat(join.right(), instanceOf(EsRelation.class)); + } + + /** + * Limit[1000[INTEGER],false] + * \_Filter[ISNOTNULL(language_name{f}#21) AND language_name{f}#21 > a[KEYWORD] AND LIKE(language_name{f}#21, "*b", false) + * AND COALESCE(language_name{f}#21,c[KEYWORD]) == c[KEYWORD] AND RLIKE(language_name{f}#21, "f.*", false)] + * \_Join[LEFT,[languages{f}#12, language_code{f}#20],[languages{f}#12],[language_code{f}#20],languages{f}#12 == language_code + * {f}#20] + * |_EsRelation[test][_meta_field{f}#15, emp_no{f}#9, first_name{f}#10, g..] + * \_Filter[ISNOTNULL(language_name{f}#21) AND language_name{f}#21 > a[KEYWORD] AND LIKE(language_name{f}#21, "*b", false) + * AND RLIKE(language_name{f}#21, "f.*", false)] + * \_EsRelation[languages_lookup][LOOKUP][language_code{f}#20, language_name{f}#21] + */ + public void testPushDownLookupJoinExpressionMultipleWhere() { + assumeTrue( + "requires LOOKUP JOIN ON boolean expression capability", + EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled() + ); + var plan = plan(""" + FROM test + | LOOKUP JOIN languages_lookup ON languages <= language_code + | WHERE language_name IS NOT NULL + | WHERE language_name > "a" + | WHERE language_name LIKE "*b" + | WHERE COALESCE(language_name, "c") == "c" + | WHERE language_name RLIKE "f.*" + """); + + var limit = as(plan, Limit.class); + var topFilter = as(limit.child(), Filter.class); + + // Verify the top-level filter contains all 5 original conditions combined + Set expectedAllFilters = Set.of( + "language_name IS NOT NULL", + "language_name > \"a\"", + "language_name LIKE \"*b\"", + "COALESCE(language_name, \"c\") == \"c\"", + "language_name RLIKE \"f.*\"" + ); + + Set actualAllFilters = new HashSet<>(Predicates.splitAnd(topFilter.condition()).stream().map(Object::toString).toList()); + assertEquals(expectedAllFilters, actualAllFilters); + + // Verify the join is below the top-level filter + var join = as(topFilter.child(), Join.class); + + // Verify a new filter with only the pushable predicates has been pushed to the right side of the join + var rightFilter = as(join.right(), Filter.class); + Set expectedPushedFilters = Set.of( + "language_name IS NOT NULL", + "language_name > \"a\"", + "language_name LIKE \"*b\"", + "language_name RLIKE \"f.*\"" + ); + Set actualPushedFilters = new HashSet<>( + Predicates.splitAnd(rightFilter.condition()).stream().map(Object::toString).toList() + ); + assertEquals(expectedPushedFilters, actualPushedFilters); + } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownJoinPastProjectTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownJoinPastProjectTests.java index 96f63d0d7eb81..fa668e9e840b8 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownJoinPastProjectTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownJoinPastProjectTests.java @@ -240,6 +240,203 @@ public void testShadowingAfterPushdown2() { assertLeftJoinConfig(join.config(), "emp_no", mainRel.outputSet(), "languages", lookupRel.outputSet()); } + /** + * Project[[languages{f}#30, emp_no{f}#27, salary{f}#32]] + * \_Limit[1000[INTEGER],true] + * \_Join[LEFT,[emp_no{f}#16, languages{f}#30],[emp_no{f}#16],[languages{f}#30],emp_no{f}#16 == languages{f}#30] + * |_Limit[1000[INTEGER],false] + * | \_EsRelation[test][_meta_field{f}#22, emp_no{f}#16, first_name{f}#17, ..] + * \_EsRelation[test_lookup][LOOKUP][emp_no{f}#27, languages{f}#30, salary{f}#32] + */ + public void testShadowingAfterPushdownExpressionJoin() { + assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V12.isEnabled()); + assumeTrue( + "requires LOOKUP JOIN ON boolean expression capability", + EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled() + ); + + String query = """ + FROM test_lookup + | RENAME languages as lang2 + | EVAL y = emp_no + | RENAME y AS lang + | LOOKUP JOIN test_lookup ON lang == languages + | KEEP languages, emp_no, salary + """; + + var plan = optimizedPlan(query); + var project = as(plan, Project.class); + var limit1 = asLimit(project.child(), 1000, true); + var join = as(limit1.child(), Join.class); + var lookupRel = as(join.right(), EsRelation.class); + var limit2 = asLimit(join.left(), 1000, false); + var mainRel = as(limit2.child(), EsRelation.class); + + var projections = project.projections(); + assertThat(Expressions.names(projections), contains("languages", "emp_no", "salary")); + + var languages = as(projections.get(0), FieldAttribute.class); + assertEquals("languages", languages.fieldName().string()); + assertTrue(lookupRel.outputSet().contains(languages)); + + var empNo = as(projections.get(1), FieldAttribute.class); + assertEquals("emp_no", empNo.fieldName().string()); + assertTrue(lookupRel.outputSet().contains(empNo)); + + var salary = as(projections.get(2), FieldAttribute.class); + assertEquals("salary", salary.fieldName().string()); + assertTrue(lookupRel.outputSet().contains(salary)); + } + + /** + * Project[[languages{f}#31, emp_no{f}#28, salary{f}#33, $$languages$temp_name$39{r$}#40 AS lang2#4]] + * \_Limit[1000[INTEGER],true] + * \_Join[LEFT,[emp_no{f}#17],[languages{f}#31],emp_no{f}#17 == languages{f}#31] + * |_Eval[[languages{f}#20 AS $$languages$temp_name$39#40]] + * | \_Limit[1000[INTEGER],false] + * | \_EsRelation[test][_meta_field{f}#23, emp_no{f}#17, first_name{f}#18, ..] + * \_EsRelation[test_lookup][LOOKUP][emp_no{f}#28, languages{f}#31, salary{f}#33] + */ + public void testShadowingAfterPushdownExpressionJoinKeepOrig() { + assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V12.isEnabled()); + assumeTrue( + "requires LOOKUP JOIN ON boolean expression capability", + EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled() + ); + + String query = """ + FROM test_lookup + | RENAME languages as lang2 + | EVAL y = emp_no + | RENAME y AS lang + | LOOKUP JOIN test_lookup ON lang == languages + | KEEP languages, emp_no, salary, lang2 + """; + + var plan = optimizedPlan(query); + var project = as(plan, Project.class); + var limit1 = asLimit(project.child(), 1000, true); + var join = as(limit1.child(), Join.class); + var lookupRel = as(join.right(), EsRelation.class); + var eval = as(join.left(), Eval.class); + var limit2 = asLimit(eval.child(), 1000, false); + var mainRel = as(limit2.child(), EsRelation.class); + + var projections = project.projections(); + assertThat(Expressions.names(projections), contains("languages", "emp_no", "salary", "lang2")); + + var languages = as(projections.get(0), FieldAttribute.class); + assertEquals("languages", languages.fieldName().string()); + assertTrue(lookupRel.outputSet().contains(languages)); + + var empNo = as(projections.get(1), FieldAttribute.class); + assertEquals("emp_no", empNo.fieldName().string()); + assertTrue(lookupRel.outputSet().contains(empNo)); + + var salary = as(projections.get(2), FieldAttribute.class); + assertEquals("salary", salary.fieldName().string()); + assertTrue(lookupRel.outputSet().contains(salary)); + + var lang2TempName = unwrapAlias(projections.get(3), ReferenceAttribute.class); + assertThat(lang2TempName.name(), startsWith("$$languages$temp_name$")); + assertTrue(eval.outputSet().contains(lang2TempName)); + } + + /** + * Project[[languages{f}#24, emp_no{f}#21, salary{f}#26]] + * \_Limit[1000[INTEGER],true] + * \_Join[LEFT,[languages{f}#24, languages{f}#13],[languages{f}#13],[languages{f}#24],languages{f}#13 == languages{f}#24] + * |_Limit[1000[INTEGER],false] + * | \_EsRelation[test][_meta_field{f}#16, emp_no{f}#10, first_name{f}#11, ..] + * \_EsRelation[test_lookup][LOOKUP][emp_no{f}#21, languages{f}#24, salary{f}#26] + */ + public void testShadowingAfterPushdownRenameExpressionJoin() { + assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V12.isEnabled()); + assumeTrue( + "requires LOOKUP JOIN ON boolean expression capability", + EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled() + ); + + String query = """ + FROM test_lookup + | RENAME languages AS lang + | LOOKUP JOIN test_lookup ON lang == languages + | KEEP languages, emp_no, salary + """; + + var plan = optimizedPlan(query); + var project = as(plan, Project.class); + var limit1 = asLimit(project.child(), 1000, true); + var join = as(limit1.child(), Join.class); + var lookupRel = as(join.right(), EsRelation.class); + var limit2 = asLimit(join.left(), 1000, false); + var mainRel = as(limit2.child(), EsRelation.class); + + var projections = project.projections(); + assertThat(Expressions.names(projections), contains("languages", "emp_no", "salary")); + + var languages = as(projections.get(0), FieldAttribute.class); + assertEquals("languages", languages.fieldName().string()); + assertTrue(lookupRel.outputSet().contains(languages)); + + var empNo = as(projections.get(1), FieldAttribute.class); + assertEquals("emp_no", empNo.fieldName().string()); + assertTrue(lookupRel.outputSet().contains(empNo)); + + var salary = as(projections.get(2), FieldAttribute.class); + assertEquals("salary", salary.fieldName().string()); + assertTrue(lookupRel.outputSet().contains(salary)); + } + + /** + * Project[[languages{f}#25, emp_no{f}#22, salary{f}#27]] + * \_Limit[1000[INTEGER],true] + * \_Join[LEFT,[lang{r}#4, languages{f}#25],[lang{r}#4],[languages{f}#25],lang{r}#4 == languages{f}#25] + * |_Eval[[languages{f}#14 + 0[INTEGER] AS lang#4]] + * | \_Limit[1000[INTEGER],false] + * | \_EsRelation[test][_meta_field{f}#17, emp_no{f}#11, first_name{f}#12, ..] + * \_EsRelation[test_lookup][LOOKUP][emp_no{f}#22, languages{f}#25, salary{f}#27] + */ + public void testShadowingAfterPushdownEvalExpressionJoin() { + assumeTrue("Requires LOOKUP JOIN", EsqlCapabilities.Cap.JOIN_LOOKUP_V12.isEnabled()); + assumeTrue( + "requires LOOKUP JOIN ON boolean expression capability", + EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled() + ); + + String query = """ + FROM test_lookup + | EVAL lang = languages + 0 + | DROP languages + | LOOKUP JOIN test_lookup ON lang == languages + | KEEP languages, emp_no, salary + """; + + var plan = optimizedPlan(query); + var project = as(plan, Project.class); + var limit1 = asLimit(project.child(), 1000, true); + var join = as(limit1.child(), Join.class); + var lookupRel = as(join.right(), EsRelation.class); + var eval = as(join.left(), Eval.class); + var limit2 = asLimit(eval.child(), 1000, false); + var mainRel = as(limit2.child(), EsRelation.class); + + var projections = project.projections(); + assertThat(Expressions.names(projections), contains("languages", "emp_no", "salary")); + + var languages = as(projections.get(0), FieldAttribute.class); + assertEquals("languages", languages.fieldName().string()); + assertTrue(lookupRel.outputSet().contains(languages)); + + var empNo = as(projections.get(1), FieldAttribute.class); + assertEquals("emp_no", empNo.fieldName().string()); + assertTrue(lookupRel.outputSet().contains(empNo)); + + var salary = as(projections.get(2), FieldAttribute.class); + assertEquals("salary", salary.fieldName().string()); + assertTrue(lookupRel.outputSet().contains(salary)); + } + private static void assertLeftJoinConfig( JoinConfig config, String expectedLeftFieldName, diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java index d909ef883e1b6..4fe89399e7e08 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java @@ -13,6 +13,7 @@ import org.elasticsearch.common.lucene.BytesRefs; import org.elasticsearch.core.Tuple; import org.elasticsearch.index.IndexMode; +import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.esql.EsqlTestUtils; import org.elasticsearch.xpack.esql.action.EsqlCapabilities; import org.elasticsearch.xpack.esql.core.capabilities.UnresolvedException; @@ -34,12 +35,14 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToInteger; import org.elasticsearch.xpack.esql.expression.function.scalar.string.regex.RLike; import org.elasticsearch.xpack.esql.expression.function.scalar.string.regex.WildcardLike; +import org.elasticsearch.xpack.esql.expression.predicate.Predicates; import org.elasticsearch.xpack.esql.expression.predicate.logical.Not; import org.elasticsearch.xpack.esql.expression.predicate.logical.Or; import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Add; import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Div; import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Mod; import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.Equals; +import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.EsqlBinaryComparison; import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.GreaterThan; import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.GreaterThanOrEqual; import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.LessThan; @@ -3130,25 +3133,114 @@ public void testValidFromPattern() { assertThat(as(plan, UnresolvedRelation.class).indexPattern().indexPattern(), equalTo(unquoteIndexPattern(basePattern))); } - public void testValidJoinPattern() { + public void testValidJoinPatternFieldJoin() { assumeTrue("LOOKUP JOIN requires corresponding capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V12.isEnabled()); var basePattern = randomIndexPatterns(without(CROSS_CLUSTER)); var joinPattern = randomIndexPattern(without(WILDCARD_PATTERN), without(CROSS_CLUSTER), without(INDEX_SELECTOR)); - var onField = randomIdentifier(); + var numberOfOnFields = randomIntBetween(1, 5); + List existingIdentifiers = new ArrayList<>(); + StringBuilder onFields = new StringBuilder(); + for (var i = 0; i < numberOfOnFields; i++) { + if (randomBoolean()) { + onFields.append(" "); + } + String onField = randomValueOtherThanMany(existingIdentifiers::contains, () -> randomIdentifier()); + existingIdentifiers.add(onField); + onFields.append(onField); + if (randomBoolean()) { + onFields.append(" "); + } + if (i < numberOfOnFields - 1) { + onFields.append(", "); + } - var plan = statement("FROM " + basePattern + " | LOOKUP JOIN " + joinPattern + " ON " + onField); + } + var plan = statement("FROM " + basePattern + " | LOOKUP JOIN " + joinPattern + " ON " + onFields); var join = as(plan, LookupJoin.class); assertThat(as(join.left(), UnresolvedRelation.class).indexPattern().indexPattern(), equalTo(unquoteIndexPattern(basePattern))); assertThat(as(join.right(), UnresolvedRelation.class).indexPattern().indexPattern(), equalTo(unquoteIndexPattern(joinPattern))); var joinType = as(join.config().type(), JoinTypes.UsingJoinType.class); - assertThat(joinType.columns(), hasSize(1)); - assertThat(as(joinType.columns().getFirst(), UnresolvedAttribute.class).name(), equalTo(onField)); + assertThat(joinType.columns(), hasSize(numberOfOnFields)); + for (int i = 0; i < numberOfOnFields; i++) { + assertThat(as(joinType.columns().get(i), UnresolvedAttribute.class).name(), equalTo(existingIdentifiers.get(i))); + } assertThat(joinType.coreJoin().joinName(), equalTo("LEFT OUTER")); } + public void testExpressionJoinNonSnapshotBuild() { + assumeFalse("LOOKUP JOIN is not yet in non-snapshot builds", Build.current().isSnapshot()); + expectThrows( + ParsingException.class, + startsWith("line 1:31: JOIN ON clause only supports fields at the moment."), + () -> statement("FROM test | LOOKUP JOIN test2 ON left_field >= right_field") + ); + } + + public void testValidJoinPatternExpressionJoin() { + assumeTrue("LOOKUP JOIN requires corresponding capability", EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled()); + + var basePattern = randomIndexPatterns(without(CROSS_CLUSTER)); + var joinPattern = randomIndexPattern(without(WILDCARD_PATTERN), without(CROSS_CLUSTER), without(INDEX_SELECTOR)); + var numberOfExpressions = randomIntBetween(1, 5); + + var expressions = new ArrayList, EsqlBinaryComparison.BinaryComparisonOperation>>(); + StringBuilder onExpressionString = new StringBuilder(); + + for (var i = 0; i < numberOfExpressions; i++) { + var left = randomIdentifier(); + var right = randomIdentifier(); + var op = randomBinaryComparisonOperation(); + expressions.add(new Tuple<>(new Tuple<>(left, right), op)); + + onExpressionString.append(left); + if (randomBoolean()) { + onExpressionString.append(" "); + } + onExpressionString.append(op.symbol()); + if (randomBoolean()) { + onExpressionString.append(" "); + } + onExpressionString.append(right); + + if (i < numberOfExpressions - 1) { + onExpressionString.append(" AND "); + } + } + + // add a check that the feature is disabled on non-snaphsot build + String query = "FROM " + basePattern + " | LOOKUP JOIN " + joinPattern + " ON " + onExpressionString; + var plan = statement(query); + + var join = as(plan, LookupJoin.class); + assertThat(as(join.left(), UnresolvedRelation.class).indexPattern().indexPattern(), equalTo(unquoteIndexPattern(basePattern))); + assertThat(as(join.right(), UnresolvedRelation.class).indexPattern().indexPattern(), equalTo(unquoteIndexPattern(joinPattern))); + + var joinType = join.config().type(); + assertThat(joinType.joinName(), startsWith("LEFT OUTER")); + + List actualExpressions = Predicates.splitAnd(join.config().joinOnConditions()); + assertThat(actualExpressions.size(), equalTo(numberOfExpressions)); + + for (int i = 0; i < numberOfExpressions; i++) { + var expected = expressions.get(i); + var actual = actualExpressions.get(i); + + assertThat(actual, instanceOf(EsqlBinaryComparison.class)); + var actualComp = (EsqlBinaryComparison) actual; + + assertThat(((UnresolvedAttribute) actualComp.left()).name(), equalTo(expected.v1().v1())); + assertThat(((UnresolvedAttribute) actualComp.right()).name(), equalTo(expected.v1().v2())); + assertThat(actualComp.getFunctionType(), equalTo(expected.v2())); + } + } + + private EsqlBinaryComparison.BinaryComparisonOperation randomBinaryComparisonOperation() { + return randomFrom(EsqlBinaryComparison.BinaryComparisonOperation.values()); + } + public void testInvalidFromPatterns() { var sourceCommands = EsqlCapabilities.Cap.METRICS_COMMAND.isEnabled() ? new String[] { "FROM", "TS" } : new String[] { "FROM" }; var indexIsBlank = "Blank index specified in index pattern"; @@ -3271,24 +3363,103 @@ public void testInvalidPatternsWithIntermittentQuotes() { } } - public void testValidJoinPatternWithRemote() { + public void testValidJoinPatternWithRemoteFieldJoin() { + testValidJoinPatternWithRemote(randomIdentifier()); + } + + public void testValidJoinPatternWithRemoteExpressionJoin() { + assumeTrue( + "requires LOOKUP JOIN ON boolean expression capability", + EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled() + ); + testValidJoinPatternWithRemote(singleExpressionJoinClause()); + } + + private void testValidJoinPatternWithRemote(String onClause) { var fromPatterns = randomIndexPatterns(CROSS_CLUSTER); var joinPattern = randomIndexPattern(without(CROSS_CLUSTER), without(WILDCARD_PATTERN), without(INDEX_SELECTOR)); - var plan = statement("FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + randomIdentifier()); + var plan = statement("FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + onClause); var join = as(plan, LookupJoin.class); assertThat(as(join.left(), UnresolvedRelation.class).indexPattern().indexPattern(), equalTo(unquoteIndexPattern(fromPatterns))); assertThat(as(join.right(), UnresolvedRelation.class).indexPattern().indexPattern(), equalTo(unquoteIndexPattern(joinPattern))); } - public void testInvalidJoinPatterns() { + public void testInvalidJoinPatternsFieldJoin() { + testInvalidJoinPatterns(randomIdentifier()); + } + + public void testInvalidJoinPatternsFieldJoinTwo() { + testInvalidJoinPatterns(randomIdentifier() + ", " + randomIdentifier()); + } + + public void testInvalidJoinPatternsExpressionJoin() { + testInvalidJoinPatterns(singleExpressionJoinClause()); + } + + public void testInvalidJoinPatternsExpressionJoinTwo() { + testInvalidJoinPatterns(singleExpressionJoinClause() + " AND " + singleExpressionJoinClause()); + } + + public void testInvalidJoinPatternsExpressionJoinMix() { + testInvalidJoinPatterns(randomIdentifier() + ", " + singleExpressionJoinClause()); + } + + public void testInvalidJoinPatternsExpressionJoinMixTwo() { + testInvalidJoinPatterns(singleExpressionJoinClause() + " AND " + randomIdentifier()); + } + + public void testInvalidLookupJoinOnClause() { + assumeTrue( + "requires LOOKUP JOIN ON boolean expression capability", + EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled() + ); + expectError( + "FROM test | LOOKUP JOIN test2 ON " + randomIdentifier() + " , " + singleExpressionJoinClause(), + "JOIN ON clause must be a comma separated list of fields or a single expression, found" + ); + + expectError( + "FROM test | LOOKUP JOIN test2 ON " + singleExpressionJoinClause() + " , " + randomIdentifier(), + "JOIN ON clause with expressions only supports a single expression, found" + ); + + expectError( + "FROM test | LOOKUP JOIN test2 ON " + singleExpressionJoinClause() + " , " + singleExpressionJoinClause(), + "JOIN ON clause with expressions only supports a single expression, found" + ); + + expectError( + "FROM test | LOOKUP JOIN test2 ON " + singleExpressionJoinClause() + " AND " + randomIdentifier(), + "JOIN ON clause only supports fields or AND of Binary Expressions at the moment, found" + ); + + expectError( + "FROM test | LOOKUP JOIN test2 ON " + randomIdentifier() + " AND " + randomIdentifier(), + "JOIN ON clause only supports fields or AND of Binary Expressions at the moment, found" + ); + + expectError( + "FROM test | LOOKUP JOIN test2 ON " + randomIdentifier() + " AND " + singleExpressionJoinClause(), + "JOIN ON clause only supports fields or AND of Binary Expressions at the moment, found " + ); + } + + private String singleExpressionJoinClause() { + var left = randomIdentifier(); + var right = randomValueOtherThan(left, ESTestCase::randomIdentifier); + var op = randomBinaryComparisonOperation(); + return left + (randomBoolean() ? " " : "") + op.symbol() + (randomBoolean() ? " " : "") + right; + } + + private void testInvalidJoinPatterns(String onClause) { assumeTrue("LOOKUP JOIN requires corresponding capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V12.isEnabled()); { // wildcard var joinPattern = randomIndexPattern(WILDCARD_PATTERN, without(CROSS_CLUSTER), without(INDEX_SELECTOR)); expectError( - "FROM " + randomIndexPatterns() + " | LOOKUP JOIN " + joinPattern + " ON " + randomIdentifier(), + "FROM " + randomIndexPatterns() + " | LOOKUP JOIN " + joinPattern + " ON " + onClause, "invalid index pattern [" + unquoteIndexPattern(joinPattern) + "], * is not allowed in LOOKUP JOIN" ); } @@ -3297,7 +3468,7 @@ public void testInvalidJoinPatterns() { var fromPatterns = randomIndexPatterns(without(CROSS_CLUSTER)); var joinPattern = randomIndexPattern(CROSS_CLUSTER, without(WILDCARD_PATTERN), without(INDEX_SELECTOR)); expectError( - "FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + randomIdentifier(), + "FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + onClause, "invalid index pattern [" + unquoteIndexPattern(joinPattern) + "], remote clusters are not supported with LOOKUP JOIN" ); } @@ -3307,7 +3478,7 @@ public void testInvalidJoinPatterns() { var fromPatterns = quote(randomIdentifier()) + ":" + unquoteIndexPattern(randomIndexPattern(without(CROSS_CLUSTER))); var joinPattern = randomIndexPattern(); expectError( - "FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + randomIdentifier(), + "FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + onClause, // Since the from pattern is partially quoted, we get an error at the end of the partially quoted string. " mismatched input ':'" ); @@ -3318,7 +3489,7 @@ public void testInvalidJoinPatterns() { var fromPatterns = randomIdentifier() + ":" + quote(randomIndexPatterns(without(CROSS_CLUSTER))); var joinPattern = randomIndexPattern(); expectError( - "FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + randomIdentifier(), + "FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + onClause, // Since the from pattern is partially quoted, we get an error at the beginning of the partially quoted // index name that we're expecting an unquoted string. "expecting UNQUOTED_SOURCE" @@ -3330,7 +3501,7 @@ public void testInvalidJoinPatterns() { // Generate a syntactically invalid (partial quoted) pattern. var joinPattern = quote(randomIdentifier()) + ":" + unquoteIndexPattern(randomIndexPattern(without(CROSS_CLUSTER))); expectError( - "FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + randomIdentifier(), + "FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + onClause, // Since the join pattern is partially quoted, we get an error at the end of the partially quoted string. "mismatched input ':'" ); @@ -3341,7 +3512,7 @@ public void testInvalidJoinPatterns() { // Generate a syntactically invalid (partially quoted) pattern. var joinPattern = randomIdentifier() + ":" + quote(randomIndexPattern(without(CROSS_CLUSTER))); expectError( - "FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + randomIdentifier(), + "FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + onClause, // Since the from pattern is partially quoted, we get an error at the beginning of the partially quoted // index name that we're expecting an unquoted string. "no viable alternative at input" @@ -3358,7 +3529,7 @@ public void testInvalidJoinPatterns() { fromPatterns = unquoteIndexPattern(fromPatterns) + "::data"; var joinPattern = randomIndexPattern(without(CROSS_CLUSTER), without(WILDCARD_PATTERN), without(INDEX_SELECTOR)); expectError( - "FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + randomIdentifier(), + "FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + onClause, "mismatched input '::' expecting {" ); } @@ -3372,7 +3543,7 @@ public void testInvalidJoinPatterns() { fromPatterns = "\"" + unquoteIndexPattern(fromPatterns) + "::data\""; var joinPattern = randomIndexPattern(without(CROSS_CLUSTER), without(WILDCARD_PATTERN), without(INDEX_SELECTOR)); expectError( - "FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + randomIdentifier(), + "FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + onClause, "Selectors are not yet supported on remote cluster patterns" ); } @@ -3384,7 +3555,7 @@ public void testInvalidJoinPatterns() { // it to chance. joinPattern = unquoteIndexPattern(joinPattern); expectError( - "FROM " + randomIndexPatterns(without(CROSS_CLUSTER)) + " | LOOKUP JOIN " + joinPattern + " ON " + randomIdentifier(), + "FROM " + randomIndexPatterns(without(CROSS_CLUSTER)) + " | LOOKUP JOIN " + joinPattern + " ON " + onClause, "no viable alternative at input " ); } @@ -3397,7 +3568,7 @@ public void testInvalidJoinPatterns() { // it to chance. joinPattern = "\"" + unquoteIndexPattern(joinPattern) + "\""; expectError( - "FROM " + randomIndexPatterns(without(CROSS_CLUSTER)) + " | LOOKUP JOIN " + joinPattern + " ON " + randomIdentifier(), + "FROM " + randomIndexPatterns(without(CROSS_CLUSTER)) + " | LOOKUP JOIN " + joinPattern + " ON " + onClause, "invalid index pattern [" + unquoteIndexPattern(joinPattern) + "], index pattern selectors are not supported in LOOKUP JOIN" @@ -3411,7 +3582,7 @@ public void testInvalidJoinPatterns() { var joinPattern = quote(randomIdentifier()) + "::" + randomFrom("data", "failures"); // After the end of the partially quoted string, i.e. the index name, parser now expects "ON..." and not a selector string. expectError( - "FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + randomIdentifier(), + "FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + onClause, "mismatched input ':' expecting 'on'" ); } @@ -3423,10 +3594,7 @@ public void testInvalidJoinPatterns() { var joinPattern = randomIdentifier() + "::" + quote(randomFrom("data", "failures")); // After the index name and "::", parser expects an unquoted string, i.e. the selector string should not be // partially quoted. - expectError( - "FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + randomIdentifier(), - "no viable alternative at input" - ); + expectError("FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + onClause, "no viable alternative at input"); } } } @@ -3587,7 +3755,7 @@ public void testForkAllCommands() { ( RENAME a as c ) ( MV_EXPAND a ) ( CHANGE_POINT a on b ) - ( LOOKUP JOIN idx2 ON f1 ) + ( LOOKUP JOIN idx2 ON f1 | LOOKUP JOIN idx3 ON f1 > f3) ( ENRICH idx2 on f1 with f2 = f3 ) ( FORK ( WHERE a:"baz" ) ( EVAL x = [ 1, 2, 3 ] ) ) ( COMPLETION a=b WITH { "inference_id": "c" } ) @@ -4044,6 +4212,10 @@ public void testInvalidFuse() { public void testDoubleParamsForIdentifier() { assumeTrue("double parameters markers for identifiers", EsqlCapabilities.Cap.DOUBLE_PARAMETER_MARKERS_FOR_IDENTIFIERS.isEnabled()); + assumeTrue( + "requires LOOKUP JOIN ON boolean expression capability", + EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled() + ); // There are three variations of double parameters - named, positional or anonymous, e.g. ??n, ??1 or ??, covered. // Each query is executed three times with the three variations. @@ -4463,6 +4635,41 @@ public void testDoubleParamsForIdentifier() { ); } + // lookup join on expression + namedDoubleParams = List.of("??f1", "??f2", "??f3", "??f4"); + positionalDoubleParams = List.of("??1", "??2", "??3", "??4"); + anonymousDoubleParams = List.of("??", "??", "??", "??"); + doubleParams.clear(); + doubleParams.add(namedDoubleParams); + doubleParams.add(positionalDoubleParams); + doubleParams.add(anonymousDoubleParams); + for (List params : doubleParams) { + String query = LoggerMessageFormat.format(null, """ + from test + | lookup join idx on {}.{} == {}.{} + | limit 1""", params.get(0), params.get(1), params.get(2), params.get(3)); + LogicalPlan plan = statement( + query, + new QueryParams( + List.of( + paramAsConstant("f1", "f.1"), + paramAsConstant("f2", "f.2"), + paramAsConstant("f3", "f.3"), + paramAsConstant("f4", "f.4") + ) + ) + ); + Limit limit = as(plan, Limit.class); + LookupJoin join = as(limit.child(), LookupJoin.class); + UnresolvedRelation ur = as(join.right(), UnresolvedRelation.class); + assertEquals(ur.indexPattern().indexPattern(), "idx"); + assertTrue(join.config().type().joinName().contains("LEFT OUTER")); + EsqlBinaryComparison on = as(join.config().joinOnConditions(), EsqlBinaryComparison.class); + assertEquals(on.getFunctionType(), EsqlBinaryComparison.BinaryComparisonOperation.EQ); + assertEquals(as(on.left(), UnresolvedAttribute.class).name(), "f.1.f.2"); + assertEquals(as(on.right(), UnresolvedAttribute.class).name(), "f.3.f.4"); + } + namedDoubleParams = List.of("??f1", "??f2", "??f3", "??f4", "??f5", "??f6"); positionalDoubleParams = List.of("??1", "??2", "??3", "??4", "??5", "??6"); anonymousDoubleParams = List.of("??", "??", "??", "??", "??", "??"); @@ -4511,6 +4718,10 @@ public void testDoubleParamsForIdentifier() { public void testMixedSingleDoubleParams() { assumeTrue("double parameters markers for identifiers", EsqlCapabilities.Cap.DOUBLE_PARAMETER_MARKERS_FOR_IDENTIFIERS.isEnabled()); + assumeTrue( + "requires LOOKUP JOIN ON boolean expression capability", + EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled() + ); // This is a subset of testDoubleParamsForIdentifier, with single and double parameter markers mixed in the queries // Single parameter markers represent a constant value or pattern // double parameter markers represent identifiers - field or function names @@ -4680,13 +4891,20 @@ public void testMixedSingleDoubleParams() { String param2 = randomBoolean() ? "?" : "??"; String param3 = randomBoolean() ? "?" : "??"; if (param1.equals("?") || param2.equals("?") || param3.equals("?")) { - expectError( - LoggerMessageFormat.format(null, "from test | " + command, param1, param2, param3), - List.of(paramAsConstant("f1", "f1"), paramAsConstant("f2", "f2"), paramAsConstant("f3", "f3")), - command.contains("join") - ? "JOIN ON clause only supports fields at the moment" - : "declared as a constant, cannot be used as an identifier" - ); + if (command.contains("lookup join") == false) { + expectError( + LoggerMessageFormat.format(null, "from test | " + command, param1, param2, param3), + List.of(paramAsConstant("f1", "f1"), paramAsConstant("f2", "f2"), paramAsConstant("f3", "f3")), + "declared as a constant, cannot be used as an identifier" + ); + } else { + expectError( + LoggerMessageFormat.format(null, "from test | " + command, param1, param2, param3), + List.of(paramAsConstant("f1", "f1"), paramAsConstant("f2", "f2"), paramAsConstant("f3", "f3")), + "JOIN ON clause only supports fields or AND of Binary Expressions at the moment" + ); + + } } } } @@ -4807,6 +5025,9 @@ public void testBracketsInIndexNames() { expectError("from te()st", "line 1:8: token recognition error at: '('"); expectError("from test | enrich foo)", "line -1:-1: Invalid query [from test | enrich foo)]"); expectError("from test | lookup join foo) on bar", "line 1:28: token recognition error at: ')'"); + if (EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled()) { + expectError("from test | lookup join foo) on bar1 > bar2", "line 1:28: token recognition error at: ')'"); + } } private void expectErrorForBracketsWithoutQuotes(String pattern) { @@ -4817,6 +5038,7 @@ private void expectErrorForBracketsWithoutQuotes(String pattern) { expectThrows(ParsingException.class, () -> processingCommand("from remote1:" + pattern + ",remote2:" + pattern)); expectThrows(ParsingException.class, () -> processingCommand("from test | lookup join " + pattern + " on bar")); + expectThrows(ParsingException.class, () -> processingCommand("from test | lookup join " + pattern + " on bar1 < bar2")); expectThrows(ParsingException.class, () -> processingCommand("from test | enrich " + pattern)); } @@ -4841,11 +5063,18 @@ private void expectSuccessForBracketsWithinQuotes(String indexName) { if (indexName.contains("*")) { expectThrows(ParsingException.class, () -> processingCommand("from test | lookup join \"" + indexName + "\" on bar")); + expectThrows(ParsingException.class, () -> processingCommand("from test | lookup join \"" + indexName + "\" on bar1 > bar2")); } else { plan = statement("from test | lookup join \"" + indexName + "\" on bar"); LookupJoin lookup = as(plan, LookupJoin.class); UnresolvedRelation right = as(lookup.right(), UnresolvedRelation.class); assertThat(right.indexPattern().indexPattern(), is(indexName)); + if (EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled()) { + plan = statement("from test | lookup join \"" + indexName + "\" on bar1 <= bar2"); + lookup = as(plan, LookupJoin.class); + right = as(lookup.right(), UnresolvedRelation.class); + assertThat(right.indexPattern().indexPattern(), is(indexName)); + } } } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/logical/AbstractLogicalPlanSerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/logical/AbstractLogicalPlanSerializationTests.java index 4960bae3d62f4..d980b34b6744c 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/logical/AbstractLogicalPlanSerializationTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/logical/AbstractLogicalPlanSerializationTests.java @@ -33,6 +33,8 @@ protected final NamedWriteableRegistry getNamedWriteableRegistry() { entries.addAll(PlanWritables.others()); entries.addAll(ExpressionWritables.aggregates()); entries.addAll(ExpressionWritables.allExpressions()); + entries.addAll(ExpressionWritables.binaryComparisons()); + entries.addAll(ExpressionWritables.scalars()); return new NamedWriteableRegistry(entries); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/logical/CommandLicenseTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/logical/CommandLicenseTests.java index e55a04c03cd1b..204aee977f5c2 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/logical/CommandLicenseTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/logical/CommandLicenseTests.java @@ -178,7 +178,7 @@ private static LogicalPlan createInstance(Class clazz, Lo return new Sample(source, null, child); } case "LookupJoin" -> { - return new LookupJoin(source, child, child, List.of()); + return new LookupJoin(source, child, child, List.of(), false, null); } case "Limit" -> { return new Limit(source, null, child); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/logical/JoinSerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/logical/JoinSerializationTests.java index 2b812e4caf260..76d40ccce072c 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/logical/JoinSerializationTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/logical/JoinSerializationTests.java @@ -17,6 +17,8 @@ import java.io.IOException; import java.util.List; +import static org.elasticsearch.xpack.esql.plan.physical.LookupJoinExecSerializationTests.randomJoinOnExpression; + public class JoinSerializationTests extends AbstractLogicalPlanSerializationTests { @Override protected Join createTestInstance() { @@ -29,10 +31,9 @@ protected Join createTestInstance() { private static JoinConfig randomJoinConfig() { JoinType type = randomFrom(JoinTypes.LEFT, JoinTypes.RIGHT, JoinTypes.INNER, JoinTypes.FULL, JoinTypes.CROSS); - List matchFields = randomFieldAttributes(1, 10, false); List leftFields = randomFieldAttributes(1, 10, false); List rightFields = randomFieldAttributes(1, 10, false); - return new JoinConfig(type, matchFields, leftFields, rightFields); + return new JoinConfig(type, leftFields, rightFields, randomJoinOnExpression()); } @Override diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/logical/JoinTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/logical/JoinTests.java index 62c4812d157e5..187dcbd07d778 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/logical/JoinTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/logical/JoinTests.java @@ -27,7 +27,6 @@ public class JoinTests extends ESTestCase { public void testExpressionsAndReferences() { int numMatchFields = between(1, 10); - List matchFields = new ArrayList<>(numMatchFields); List leftFields = new ArrayList<>(numMatchFields); List leftAttributes = new ArrayList<>(numMatchFields); List rightFields = new ArrayList<>(numMatchFields); @@ -41,25 +40,22 @@ public void testExpressionsAndReferences() { leftAttributes.add(left.toAttribute()); rightFields.add(right); rightAttributes.add(right.toAttribute()); - matchFields.add(randomBoolean() ? left.toAttribute() : right.toAttribute()); } Row left = new Row(Source.EMPTY, leftFields); Row right = new Row(Source.EMPTY, rightFields); - JoinConfig joinConfig = new JoinConfig(JoinTypes.LEFT, matchFields, leftAttributes, rightAttributes); + JoinConfig joinConfig = new JoinConfig(JoinTypes.LEFT, leftAttributes, rightAttributes, null); Join join = new Join(Source.EMPTY, left, right, joinConfig); // matchfields are a subset of the left and right fields, so they don't contribute to the size of the references set. // assertEquals(2 * numMatchFields, join.references().size()); AttributeSet refs = join.references(); - assertTrue(refs.containsAll(matchFields)); assertTrue(refs.containsAll(leftAttributes)); assertTrue(refs.containsAll(rightAttributes)); Set exprs = Set.copyOf(join.expressions()); - assertTrue(exprs.containsAll(matchFields)); assertTrue(exprs.containsAll(leftAttributes)); assertTrue(exprs.containsAll(rightAttributes)); } @@ -67,7 +63,6 @@ public void testExpressionsAndReferences() { public void testTransformExprs() { int numMatchFields = between(1, 10); - List matchFields = new ArrayList<>(numMatchFields); List leftFields = new ArrayList<>(numMatchFields); List leftAttributes = new ArrayList<>(numMatchFields); List rightFields = new ArrayList<>(numMatchFields); @@ -81,18 +76,17 @@ public void testTransformExprs() { leftAttributes.add(left.toAttribute()); rightFields.add(right); rightAttributes.add(right.toAttribute()); - matchFields.add(randomBoolean() ? left.toAttribute() : right.toAttribute()); } Row left = new Row(Source.EMPTY, leftFields); Row right = new Row(Source.EMPTY, rightFields); - JoinConfig joinConfig = new JoinConfig(JoinTypes.LEFT, matchFields, leftAttributes, rightAttributes); + JoinConfig joinConfig = new JoinConfig(JoinTypes.LEFT, leftAttributes, rightAttributes, null); Join join = new Join(Source.EMPTY, left, right, joinConfig); - assertTrue(join.config().matchFields().stream().allMatch(ref -> ref.dataType().equals(DataType.INTEGER))); + assertTrue(join.config().leftFields().stream().allMatch(ref -> ref.dataType().equals(DataType.INTEGER))); Join transformedJoin = (Join) join.transformExpressionsOnly(Attribute.class, attr -> attr.withDataType(DataType.BOOLEAN)); - assertTrue(transformedJoin.config().matchFields().stream().allMatch(ref -> ref.dataType().equals(DataType.BOOLEAN))); + assertTrue(transformedJoin.config().leftFields().stream().allMatch(ref -> ref.dataType().equals(DataType.BOOLEAN))); } private static Alias aliasForLiteral(String name) { diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/physical/AbstractPhysicalPlanSerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/physical/AbstractPhysicalPlanSerializationTests.java index c2a012cd8e853..219b0a3586a6c 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/physical/AbstractPhysicalPlanSerializationTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/physical/AbstractPhysicalPlanSerializationTests.java @@ -50,6 +50,8 @@ protected final NamedWriteableRegistry getNamedWriteableRegistry() { entries.addAll(PlanWritables.getNamedWriteables()); entries.addAll(ExpressionWritables.aggregates()); entries.addAll(ExpressionWritables.allExpressions()); + entries.addAll(ExpressionWritables.scalars()); + entries.addAll(ExpressionWritables.binaryComparisons()); entries.addAll(new SearchModule(Settings.EMPTY, List.of()).getNamedWriteables()); // Query builders entries.add(Add.ENTRY); // Used by the eval tests entries.add(AggregateMetricDoubleBlockBuilder.AggregateMetricDoubleLiteral.ENTRY); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/physical/HashJoinExecSerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/physical/HashJoinExecSerializationTests.java index 78ff1a5973ea3..f19b727171cb8 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/physical/HashJoinExecSerializationTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/physical/HashJoinExecSerializationTests.java @@ -18,11 +18,10 @@ public static HashJoinExec randomHashJoinExec(int depth) { Source source = randomSource(); PhysicalPlan child = randomChild(depth); LocalSourceExec joinData = LocalSourceExecSerializationTests.randomLocalSourceExec(); - List matchFields = randomFields(); List leftFields = randomFields(); List rightFields = randomFields(); List output = randomFields(); - return new HashJoinExec(source, child, joinData, matchFields, leftFields, rightFields, output); + return new HashJoinExec(source, child, joinData, leftFields, rightFields, output); } private static List randomFields() { @@ -38,20 +37,18 @@ protected HashJoinExec createTestInstance() { protected HashJoinExec mutateInstance(HashJoinExec instance) throws IOException { PhysicalPlan child = instance.left(); PhysicalPlan joinData = instance.joinData(); - List matchFields = randomFieldAttributes(1, 5, false); List leftFields = randomFieldAttributes(1, 5, false); List rightFields = randomFieldAttributes(1, 5, false); List output = randomFieldAttributes(1, 5, false); - switch (between(0, 5)) { + switch (between(0, 4)) { case 0 -> child = randomValueOtherThan(child, () -> randomChild(0)); case 1 -> joinData = randomValueOtherThan(joinData, LocalSourceExecSerializationTests::randomLocalSourceExec); - case 2 -> matchFields = randomValueOtherThan(matchFields, HashJoinExecSerializationTests::randomFields); - case 3 -> leftFields = randomValueOtherThan(leftFields, HashJoinExecSerializationTests::randomFields); - case 4 -> rightFields = randomValueOtherThan(rightFields, HashJoinExecSerializationTests::randomFields); - case 5 -> output = randomValueOtherThan(output, HashJoinExecSerializationTests::randomFields); + case 2 -> leftFields = randomValueOtherThan(leftFields, HashJoinExecSerializationTests::randomFields); + case 3 -> rightFields = randomValueOtherThan(rightFields, HashJoinExecSerializationTests::randomFields); + case 4 -> output = randomValueOtherThan(output, HashJoinExecSerializationTests::randomFields); default -> throw new UnsupportedOperationException(); } - return new HashJoinExec(instance.source(), child, joinData, matchFields, leftFields, rightFields, output); + return new HashJoinExec(instance.source(), child, joinData, leftFields, rightFields, output); } @Override diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/physical/LookupJoinExecSerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/physical/LookupJoinExecSerializationTests.java index 0fedf104f1df1..085bef301f627 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/physical/LookupJoinExecSerializationTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/physical/LookupJoinExecSerializationTests.java @@ -8,12 +8,39 @@ package org.elasticsearch.xpack.esql.plan.physical; import org.elasticsearch.xpack.esql.core.expression.Attribute; +import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; +import org.elasticsearch.xpack.esql.expression.predicate.Predicates; +import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.EsqlBinaryComparison; import java.io.IOException; +import java.util.ArrayList; import java.util.List; public class LookupJoinExecSerializationTests extends AbstractPhysicalPlanSerializationTests { + + public static Expression randomJoinOnExpression() { + int randomInt = between(1, 20); + List expressionList = new ArrayList<>(); + for (int i = 0; i < randomInt; i++) { + expressionList.add(randomJoinPredicate()); + } + return Predicates.combineAnd(expressionList); + + } + + private static Expression randomJoinPredicate() { + return randomBinaryComparisonOperator().buildNewInstance(randomSource(), randomAttribute(), randomAttribute()); + } + + private static EsqlBinaryComparison.BinaryComparisonOperation randomBinaryComparisonOperator() { + return randomFrom(EsqlBinaryComparison.BinaryComparisonOperation.values()); + } + + private static Attribute randomAttribute() { + return randomFieldAttributes(1, 1, false).get(0); + } + public static LookupJoinExec randomLookupJoinExec(int depth) { Source source = randomSource(); PhysicalPlan child = randomChild(depth); @@ -21,7 +48,7 @@ public static LookupJoinExec randomLookupJoinExec(int depth) { List leftFields = randomFields(); List rightFields = randomFields(); List addedFields = randomFields(); - return new LookupJoinExec(source, child, lookup, leftFields, rightFields, addedFields); + return new LookupJoinExec(source, child, lookup, leftFields, rightFields, addedFields, randomJoinOnExpression()); } private static List randomFields() { @@ -40,15 +67,17 @@ protected LookupJoinExec mutateInstance(LookupJoinExec instance) throws IOExcept List leftFields = randomFields(); List rightFields = randomFields(); List addedFields = randomFields(); - switch (between(0, 4)) { + Expression joinOnConditions = instance.joinOnConditions(); + switch (between(0, 5)) { case 0 -> child = randomValueOtherThan(child, () -> randomChild(0)); case 1 -> lookup = randomValueOtherThan(lookup, () -> randomChild(0)); case 2 -> leftFields = randomValueOtherThan(leftFields, LookupJoinExecSerializationTests::randomFields); case 3 -> rightFields = randomValueOtherThan(rightFields, LookupJoinExecSerializationTests::randomFields); case 4 -> addedFields = randomValueOtherThan(addedFields, LookupJoinExecSerializationTests::randomFields); + case 5 -> joinOnConditions = randomValueOtherThan(joinOnConditions, LookupJoinExecSerializationTests::randomJoinOnExpression); default -> throw new UnsupportedOperationException(); } - return new LookupJoinExec(instance.source(), child, lookup, leftFields, rightFields, addedFields); + return new LookupJoinExec(instance.source(), child, lookup, leftFields, rightFields, addedFields, joinOnConditions); } @Override diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/tree/EsqlNodeSubclassTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/tree/EsqlNodeSubclassTests.java index 65f48ad06e299..49cc6dfc57f1e 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/tree/EsqlNodeSubclassTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/tree/EsqlNodeSubclassTests.java @@ -99,6 +99,7 @@ import static org.elasticsearch.xpack.esql.index.EsIndexSerializationTests.randomEsIndex; import static org.elasticsearch.xpack.esql.index.EsIndexSerializationTests.randomIndexNameWithModes; import static org.elasticsearch.xpack.esql.plan.AbstractNodeSerializationTests.randomFieldAttributes; +import static org.elasticsearch.xpack.esql.plan.physical.LookupJoinExecSerializationTests.randomJoinOnExpression; import static org.mockito.Mockito.mock; /** @@ -514,7 +515,7 @@ public void accept(Page page) { JoinTypes.LEFT, List.of(UnresolvedAttributeTests.randomUnresolvedAttribute()), List.of(UnresolvedAttributeTests.randomUnresolvedAttribute()), - List.of(UnresolvedAttributeTests.randomUnresolvedAttribute()) + randomJoinOnExpression() ); }