Skip to content

Commit 9d17569

Browse files
committed
Support max=n option
Signed-off-by: Lantao Jin <ltjin@amazon.com>
1 parent 29548b2 commit 9d17569

15 files changed

Lines changed: 436 additions & 104 deletions

File tree

common/src/main/java/org/opensearch/sql/common/setting/Settings.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public enum Key {
3636
CALCITE_PUSHDOWN_ENABLED("plugins.calcite.pushdown.enabled"),
3737
CALCITE_PUSHDOWN_ROWCOUNT_ESTIMATION_FACTOR(
3838
"plugins.calcite.pushdown.rowcount.estimation.factor"),
39+
CALCITE_SUPPORT_ALL_JOIN_TYPES("plugins.calcite.all_join_types.allowed"),
3940

4041
/** Query Settings. */
4142
FIELD_TYPE_TOLERANCE("plugins.query.field_type_tolerance"),

core/src/main/java/org/opensearch/sql/ast/expression/Literal.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,5 @@ public String toString() {
4949

5050
public static Literal TRUE = new Literal(true, DataType.BOOLEAN);
5151
public static Literal FALSE = new Literal(false, DataType.BOOLEAN);
52+
public static Literal ZERO = new Literal(Integer.valueOf("0"), DataType.INTEGER);
5253
}

core/src/main/java/org/opensearch/sql/ast/tree/Join.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ public enum JoinType {
9797
FULL
9898
}
9999

100+
/** RIGHT, CROSS, FULL are performance sensitive join types */
101+
public static List<JoinType> highCostJoinTypes() {
102+
return List.of(JoinType.RIGHT, JoinType.CROSS, JoinType.FULL);
103+
}
104+
100105
@Getter
101106
@RequiredArgsConstructor
102107
public static class JoinHint {

core/src/main/java/org/opensearch/sql/calcite/CalciteRelNodeVisitor.java

Lines changed: 55 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,7 @@ public RelNode visitJoin(Join node, CalcitePlanContext context) {
677677
List<UnresolvedPlan> children = node.getChildren();
678678
children.forEach(c -> analyze(c, context));
679679
if (node.getJoinCondition().isEmpty()) {
680-
// For spl compatible grammar
680+
// join-with-field-list grammar
681681
List<String> leftColumns = context.relBuilder.peek(1).getRowType().getFieldNames();
682682
List<String> rightColumns = context.relBuilder.peek().getRowType().getFieldNames();
683683
List<String> duplicatedFieldNames =
@@ -715,14 +715,31 @@ public RelNode visitJoin(Join node, CalcitePlanContext context) {
715715
.map(field -> JoinAndLookupUtils.analyzeFieldsForLookUp(field, false, context))
716716
.toList();
717717
}
718+
Literal max = node.getArgumentMap().get("max");
719+
if (max != null && !max.equals(Literal.ZERO)) {
720+
// max != 0 means the right-side should be dedup
721+
Integer allowedDuplication = (Integer) max.getValue();
722+
if (allowedDuplication < 0) {
723+
throw new SemanticCheckException("max option must be a positive integer");
724+
}
725+
List<RexNode> dedupeFields =
726+
node.getJoinFields().isPresent()
727+
? node.getJoinFields().get().stream()
728+
.map(a -> (RexNode) context.relBuilder.field(a.getField().toString()))
729+
.toList()
730+
: duplicatedFieldNames.stream()
731+
.map(a -> (RexNode) context.relBuilder.field(a))
732+
.toList();
733+
buildDedup(context, dedupeFields, allowedDuplication);
734+
}
718735
context.relBuilder.join(
719736
JoinAndLookupUtils.translateJoinType(node.getJoinType()), joinCondition);
720737
if (!toBeRemovedFields.isEmpty()) {
721738
context.relBuilder.projectExcept(toBeRemovedFields);
722739
}
723740
return context.relBuilder.peek();
724741
}
725-
// The join old grammar doesn't allow empty join condition
742+
// The join-with-criteria grammar doesn't allow empty join condition
726743
RexNode joinCondition =
727744
node.getJoinCondition()
728745
.map(c -> rexVisitor.analyzeJoinCondition(c, context))
@@ -925,42 +942,46 @@ public RelNode visitDedupe(Dedupe node, CalcitePlanContext context) {
925942
// DropColumns('_row_number_)
926943
context.relBuilder.projectExcept(_row_number_);
927944
} else {
928-
/*
929-
* | dedup 2 a, b keepempty=false
930-
* DropColumns('_row_number_)
931-
* +- Filter ('_row_number_ <= n)
932-
* +- Window [row_number() windowspecdefinition('a, 'b, 'a ASC NULLS FIRST, 'b ASC NULLS FIRST, specifiedwindowoundedpreceding$(), currentrow$())) AS _row_number_], ['a, 'b], ['a ASC NULLS FIRST, 'b ASC NULLS FIRST]
933-
* +- Filter (isnotnull('a) AND isnotnull('b))
934-
* +- ...
935-
*/
936-
// Filter (isnotnull('a) AND isnotnull('b))
937-
context.relBuilder.filter(
938-
context.relBuilder.and(
939-
dedupeFields.stream().map(context.relBuilder::isNotNull).toList()));
940-
// Window [row_number() windowspecdefinition('a, 'b, 'a ASC NULLS FIRST, 'b ASC NULLS FIRST,
941-
// specifiedwindowoundedpreceding$(), currentrow$())) AS _row_number_], ['a, 'b], ['a ASC
942-
// NULLS FIRST, 'b ASC NULLS FIRST]
943-
RexNode rowNumber =
944-
context
945-
.relBuilder
946-
.aggregateCall(SqlStdOperatorTable.ROW_NUMBER)
947-
.over()
948-
.partitionBy(dedupeFields)
949-
.orderBy(dedupeFields)
950-
.rowsTo(RexWindowBounds.CURRENT_ROW)
951-
.as("_row_number_");
952-
context.relBuilder.projectPlus(rowNumber);
953-
RexNode _row_number_ = context.relBuilder.field("_row_number_");
954-
// Filter ('_row_number_ <= n)
955-
context.relBuilder.filter(
956-
context.relBuilder.lessThanOrEqual(
957-
_row_number_, context.relBuilder.literal(allowedDuplication)));
958-
// DropColumns('_row_number_)
959-
context.relBuilder.projectExcept(_row_number_);
945+
buildDedup(context, dedupeFields, allowedDuplication);
960946
}
961947
return context.relBuilder.peek();
962948
}
963949

950+
private static void buildDedup(
951+
CalcitePlanContext context, List<RexNode> dedupeFields, Integer allowedDuplication) {
952+
/*
953+
* | dedup 2 a, b keepempty=false
954+
* DropColumns('_row_number_)
955+
* +- Filter ('_row_number_ <= n)
956+
* +- Window [row_number() windowspecdefinition('a, 'b, 'a ASC NULLS FIRST, 'b ASC NULLS FIRST, specifiedwindowoundedpreceding$(), currentrow$())) AS _row_number_], ['a, 'b], ['a ASC NULLS FIRST, 'b ASC NULLS FIRST]
957+
* +- Filter (isnotnull('a) AND isnotnull('b))
958+
* +- ...
959+
*/
960+
// Filter (isnotnull('a) AND isnotnull('b))
961+
context.relBuilder.filter(
962+
context.relBuilder.and(dedupeFields.stream().map(context.relBuilder::isNotNull).toList()));
963+
// Window [row_number() windowspecdefinition('a, 'b, 'a ASC NULLS FIRST, 'b ASC NULLS FIRST,
964+
// specifiedwindowoundedpreceding$(), currentrow$())) AS _row_number_], ['a, 'b], ['a ASC
965+
// NULLS FIRST, 'b ASC NULLS FIRST]
966+
RexNode rowNumber =
967+
context
968+
.relBuilder
969+
.aggregateCall(SqlStdOperatorTable.ROW_NUMBER)
970+
.over()
971+
.partitionBy(dedupeFields)
972+
.orderBy(dedupeFields)
973+
.rowsTo(RexWindowBounds.CURRENT_ROW)
974+
.as("_row_number_");
975+
context.relBuilder.projectPlus(rowNumber);
976+
RexNode _row_number_ = context.relBuilder.field("_row_number_");
977+
// Filter ('_row_number_ <= n)
978+
context.relBuilder.filter(
979+
context.relBuilder.lessThanOrEqual(
980+
_row_number_, context.relBuilder.literal(allowedDuplication)));
981+
// DropColumns('_row_number_)
982+
context.relBuilder.projectExcept(_row_number_);
983+
}
984+
964985
@Override
965986
public RelNode visitWindow(Window node, CalcitePlanContext context) {
966987
visitChildren(node, context);

docs/user/ppl/cmd/join.rst

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,24 @@ Version
1919

2020
Syntax-1
2121
========
22-
| [joinType] join [leftAlias] [rightAlias] on <joinCriteria> <right-dataset>
22+
| [joinType] join [max=n] [leftAlias] [rightAlias] (on | where) <joinCriteria> <right-dataset>
2323
24-
* joinType: optional. The type of join to perform. The default is ``INNER`` if not specified. Other option is ``LEFT [OUTER]``, ``RIGHT [OUTER]``, ``FULL [OUTER]``, ``CROSS``, ``[LEFT] SEMI``, ``[LEFT] ANTI``.
24+
* joinType: optional. The type of join to perform. The default is ``inner`` if not specified. Other option is ``left``, ``outer``(alias of ``left``), ``semi``, ``anti`` and performance sensitive types ``right``, ``full`` and ``cross``.
25+
* max=n: optional. Controls how many subsearch results could be joined against to each row in main search. The default value is 0, means unlimited.
2526
* leftAlias: optional. The subsearch alias to use with the left join side, to avoid ambiguous naming. Fixed pattern: ``left = <leftAlias>``
2627
* rightAlias: optional. The subsearch alias to use with the right join side, to avoid ambiguous naming. Fixed pattern: ``right = <rightAlias>``
27-
* joinCriteria: mandatory. It could be any comparison expression.
28+
* joinCriteria: mandatory. It could be any comparison expression. Must follow with ``on`` (since 3.0.0) or ``where`` (since 3.3.0) keyword.
2829
* right-dataset: mandatory. Right dataset could be either an ``index`` or a ``subsearch`` with/without alias.
2930

3031
Syntax-2
3132
========
3233
| (Since 3.3.0)
33-
| join [type=<joinType>] [overwrite=<bool>] <join-field-list> <right-dataset>
34+
| join [type=<joinType>] [overwrite=<bool>] [max=n] <join-field-list> <right-dataset>
3435
35-
* type=<joinType>: optional. The type of join to perform. The default is ``INNER`` if not specified. Other option is ``LEFT``, ``RIGHT``, ``FULL``, ``CROSS``, ``SEMI``, ``ANTI``.
36+
* type=<joinType>: optional. The type of join to perform. The default is ``INNER`` if not specified. Other option is ``left``, ``outer``(alias of ``left``), ``semi``, ``anti`` and performance sensitive types ``right``, ``full`` and ``cross``.
3637
* overwrite=<bool>: optional. Specifies whether duplicate-named fields from <right-dataset> (subsearch results) should replace corresponding fields in the main search results. The default value is ``true``.
37-
* join-field-list: optional. The fields to use to build join criteria. The ``join-field-list`` must be present in both sides. If no <join-field-list> is present, all fields that are common to both sides are used.
38+
* max=n: optional. Controls how many subsearch results could be joined against to each row in main search. The default value is 0, means unlimited.
39+
* join-field-list: optional. The fields to use to build join criteria. The ``join-field-list`` must be present in both sides. If no <join-field-list> is present, all fields that are common to both sides are used. The comma is optional.
3840
* right-dataset: mandatory. Right dataset could be either an ``index`` or a ``subsearch`` with/without alias.
3941

4042
Configuration
@@ -69,6 +71,7 @@ Usage
6971
Join (syntax-1)::
7072

7173
source = table1 | inner join left = l right = r on l.a = r.a table2 | fields l.a, r.a, b, c
74+
source = table1 | inner join left = l right = r where l.a = r.a table2 | fields l.a, r.a, b, c
7275
source = table1 | left join left = l right = r on l.a = r.a table2 | fields l.a, r.a, b, c
7376
source = table1 | right join left = l right = r on l.a = r.a table2 | fields l.a, r.a, b, c
7477
source = table1 | full left = l right = r on l.a = r.a table2 | fields l.a, r.a, b, c
@@ -86,6 +89,7 @@ Join (syntax-2)::
8689

8790
source = table1 | join a table2 | fields a, b, c
8891
source = table1 | join a, b table2 | fields a, b, c
92+
source = table1 | join type=outer a b table2 | fields a, b, c
8993
source = table1 | join type=left overwrite=false a, b [source=table2 | rename d as b] | fields a, b, c
9094

9195
Example 1: Two indices join
@@ -179,3 +183,5 @@ Assume table1 and table2 only contain field ``id``, following PPL queries and th
179183
- table1.id, tt.id, tt.b, a
180184

181185
But for the syntax-2 (since 3.2.0), duplicate-named fields in output results are deduplicated, with field retention determined by the value of 'overwrite' option.
186+
187+
Join types ``inner``, ``left``, ``outer`` (alias of ``left``), ``semi`` and ``anti`` are supported by default. ``right``, ``full``, ``cross`` are performance sensitive join types which are disabled by default. Set config ``plugins.calcite.all_join_types.allowed = true`` to enable.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLJoinIT.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public class CalcitePPLJoinIT extends PPLIntegTestCase {
2929
public void init() throws Exception {
3030
super.init();
3131
enableCalcite();
32+
supportAllJoinTypes();
3233

3334
loadIndex(Index.STATE_COUNTRY);
3435
loadIndex(Index.OCCUPATION);

integ-test/src/test/java/org/opensearch/sql/ppl/PPLIntegTestCase.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,12 @@ public static void withFallbackEnabled(Runnable f, String msg) throws IOExceptio
212212
}
213213
}
214214

215+
public static void supportAllJoinTypes() throws IOException {
216+
updateClusterSettings(
217+
new SQLIntegTestCase.ClusterSetting(
218+
"persistent", Key.CALCITE_SUPPORT_ALL_JOIN_TYPES.getKeyValue(), "true"));
219+
}
220+
215221
public static void withSettings(Key setting, String value, Runnable f) throws IOException {
216222
String originalValue = getClusterSetting(setting.getKeyValue(), "transient");
217223
if (originalValue.equals(value)) f.run();

opensearch/src/main/java/org/opensearch/sql/opensearch/setting/OpenSearchSettings.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,13 @@ public class OpenSearchSettings extends Settings {
127127
Setting.Property.NodeScope,
128128
Setting.Property.Dynamic);
129129

130+
public static final Setting<?> CALCITE_SUPPORT_ALL_JOIN_TYPES_SETTING =
131+
Setting.boolSetting(
132+
Key.CALCITE_SUPPORT_ALL_JOIN_TYPES.getKeyValue(),
133+
false,
134+
Setting.Property.NodeScope,
135+
Setting.Property.Dynamic);
136+
130137
public static final Setting<?> QUERY_MEMORY_LIMIT_SETTING =
131138
Setting.memorySizeSetting(
132139
Key.QUERY_MEMORY_LIMIT.getKeyValue(),
@@ -351,6 +358,12 @@ public OpenSearchSettings(ClusterSettings clusterSettings) {
351358
Key.CALCITE_PUSHDOWN_ROWCOUNT_ESTIMATION_FACTOR,
352359
CALCITE_PUSHDOWN_ROWCOUNT_ESTIMATION_FACTOR_SETTING,
353360
new Updater(Key.CALCITE_PUSHDOWN_ROWCOUNT_ESTIMATION_FACTOR));
361+
register(
362+
settingBuilder,
363+
clusterSettings,
364+
Key.CALCITE_SUPPORT_ALL_JOIN_TYPES,
365+
CALCITE_SUPPORT_ALL_JOIN_TYPES_SETTING,
366+
new Updater(Key.CALCITE_SUPPORT_ALL_JOIN_TYPES));
354367
register(
355368
settingBuilder,
356369
clusterSettings,
@@ -527,6 +540,7 @@ public static List<Setting<?>> pluginSettings() {
527540
.add(CALCITE_FALLBACK_ALLOWED_SETTING)
528541
.add(CALCITE_PUSHDOWN_ENABLED_SETTING)
529542
.add(CALCITE_PUSHDOWN_ROWCOUNT_ESTIMATION_FACTOR_SETTING)
543+
.add(CALCITE_SUPPORT_ALL_JOIN_TYPES_SETTING)
530544
.add(DEFAULT_PATTERN_METHOD_SETTING)
531545
.add(DEFAULT_PATTERN_MODE_SETTING)
532546
.add(DEFAULT_PATTERN_MAX_SAMPLE_COUNT_SETTING)

ppl/src/main/antlr/OpenSearchPPLParser.g4

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -316,26 +316,37 @@ tableSourceClause
316316

317317
// join
318318
joinCommand
319-
: joinType JOIN sideAlias joinHintList? joinCriteria right = tableOrSubqueryClause
320-
| JOIN (joinOption)* (fieldList)? right = tableOrSubqueryClause
319+
: JOIN (joinOption)* (fieldList)? right = tableOrSubqueryClause
320+
| sqlLikeJoinType? JOIN (joinOption)* sideAlias joinHintList? joinCriteria right = tableOrSubqueryClause
321321
;
322322

323-
joinType
324-
: INNER?
323+
sqlLikeJoinType
324+
: INNER
325325
| CROSS
326-
| LEFT OUTER?
326+
| (LEFT OUTER? | OUTER)
327327
| RIGHT OUTER?
328328
| FULL OUTER?
329329
| LEFT? SEMI
330330
| LEFT? ANTI
331331
;
332332

333+
joinType
334+
: INNER
335+
| CROSS
336+
| OUTER
337+
| LEFT
338+
| RIGHT
339+
| FULL
340+
| SEMI
341+
| ANTI
342+
;
343+
333344
sideAlias
334345
: (LEFT EQUAL leftAlias = qualifiedName)? COMMA? (RIGHT EQUAL rightAlias = qualifiedName)?
335346
;
336347

337348
joinCriteria
338-
: ON logicalExpression
349+
: (ON | WHERE) logicalExpression
339350
;
340351

341352
joinHintList
@@ -350,6 +361,7 @@ hintPair
350361
joinOption
351362
: OVERWRITE EQUAL booleanLiteral # overwriteOption
352363
| TYPE EQUAL joinType # typeOption
364+
| MAX EQUAL integerLiteral # maxOption
353365
;
354366

355367
renameClasue
@@ -530,7 +542,7 @@ tableFunction
530542

531543
// fields
532544
fieldList
533-
: fieldExpression (COMMA fieldExpression)*
545+
: fieldExpression ((COMMA)? fieldExpression)*
534546
;
535547

536548
wcFieldList
@@ -1118,8 +1130,8 @@ keywordsCanBeId
11181130
| multiFieldRelevanceFunctionName
11191131
| commandName
11201132
| collectionFunctionName
1121-
| comparisonOperator
11221133
| explainMode
1134+
| REGEXP
11231135
// commands assist keywords
11241136
| CASE
11251137
| ELSE

0 commit comments

Comments
 (0)