|
48 | 48 | import org.apache.calcite.rel.hint.RelHint; |
49 | 49 | import org.apache.calcite.rel.logical.LogicalAggregate; |
50 | 50 | import org.apache.calcite.rel.logical.LogicalValues; |
| 51 | +import org.apache.calcite.rel.type.RelDataType; |
51 | 52 | import org.apache.calcite.rel.type.RelDataTypeField; |
52 | 53 | import org.apache.calcite.rex.RexCall; |
53 | 54 | import org.apache.calcite.rex.RexCorrelVariable; |
|
60 | 61 | import org.apache.calcite.sql.fun.SqlStdOperatorTable; |
61 | 62 | import org.apache.calcite.sql.type.SqlTypeFamily; |
62 | 63 | import org.apache.calcite.sql.type.SqlTypeName; |
63 | | -import org.apache.calcite.sql.validate.SqlValidatorUtil; |
64 | 64 | import org.apache.calcite.tools.RelBuilder; |
65 | 65 | import org.apache.calcite.tools.RelBuilder.AggCall; |
66 | 66 | import org.apache.calcite.util.Holder; |
|
111 | 111 | import org.opensearch.sql.ast.tree.Lookup; |
112 | 112 | import org.opensearch.sql.ast.tree.Lookup.OutputStrategy; |
113 | 113 | import org.opensearch.sql.ast.tree.ML; |
| 114 | +import org.opensearch.sql.ast.tree.Multisearch; |
114 | 115 | import org.opensearch.sql.ast.tree.Paginate; |
115 | 116 | import org.opensearch.sql.ast.tree.Parse; |
116 | 117 | import org.opensearch.sql.ast.tree.Patterns; |
@@ -1649,65 +1650,73 @@ public RelNode visitAppend(Append node, CalcitePlanContext context) { |
1649 | 1650 | node.getSubSearch().accept(new EmptySourcePropagateVisitor(), null); |
1650 | 1651 | prunedSubSearch.accept(this, context); |
1651 | 1652 |
|
1652 | | - // 3. Merge two query schemas |
| 1653 | + // 3. Merge two query schemas using shared logic |
1653 | 1654 | RelNode subsearchNode = context.relBuilder.build(); |
1654 | 1655 | RelNode mainNode = context.relBuilder.build(); |
1655 | | - List<RelDataTypeField> mainFields = mainNode.getRowType().getFieldList(); |
1656 | | - List<RelDataTypeField> subsearchFields = subsearchNode.getRowType().getFieldList(); |
1657 | | - Map<String, RelDataTypeField> subsearchFieldMap = |
1658 | | - subsearchFields.stream() |
1659 | | - .map(typeField -> Pair.of(typeField.getName(), typeField)) |
1660 | | - .collect(Collectors.toMap(Pair::getKey, Pair::getValue)); |
1661 | | - boolean[] isSelected = new boolean[subsearchFields.size()]; |
1662 | | - List<String> names = new ArrayList<>(); |
1663 | | - List<RexNode> mainUnionProjects = new ArrayList<>(); |
1664 | | - List<RexNode> subsearchUnionProjects = new ArrayList<>(); |
1665 | | - |
1666 | | - // 3.1 Start with main query's schema. If subsearch plan doesn't have matched column, |
1667 | | - // add same type column in place with NULL literal |
1668 | | - for (int i = 0; i < mainFields.size(); i++) { |
1669 | | - mainUnionProjects.add(context.rexBuilder.makeInputRef(mainNode, i)); |
1670 | | - RelDataTypeField mainField = mainFields.get(i); |
1671 | | - RelDataTypeField subsearchField = subsearchFieldMap.get(mainField.getName()); |
1672 | | - names.add(mainField.getName()); |
1673 | | - if (subsearchFieldMap.containsKey(mainField.getName()) |
1674 | | - && subsearchField != null |
1675 | | - && subsearchField.getType().equals(mainField.getType())) { |
1676 | | - subsearchUnionProjects.add( |
1677 | | - context.rexBuilder.makeInputRef(subsearchNode, subsearchField.getIndex())); |
1678 | | - isSelected[subsearchField.getIndex()] = true; |
1679 | | - } else { |
1680 | | - subsearchUnionProjects.add(context.rexBuilder.makeNullLiteral(mainField.getType())); |
1681 | | - } |
| 1656 | + |
| 1657 | + // Use shared schema merging logic that handles type conflicts via field renaming |
| 1658 | + List<RelNode> nodesToMerge = Arrays.asList(mainNode, subsearchNode); |
| 1659 | + List<RelNode> projectedNodes = |
| 1660 | + SchemaUnifier.buildUnifiedSchemaWithConflictResolution(nodesToMerge, context); |
| 1661 | + |
| 1662 | + // 4. Union the projected plans |
| 1663 | + for (RelNode projectedNode : projectedNodes) { |
| 1664 | + context.relBuilder.push(projectedNode); |
1682 | 1665 | } |
| 1666 | + context.relBuilder.union(true); |
| 1667 | + return context.relBuilder.peek(); |
| 1668 | + } |
1683 | 1669 |
|
1684 | | - // 3.2 Add remaining subsearch columns to the merged schema |
1685 | | - for (int j = 0; j < subsearchFields.size(); j++) { |
1686 | | - RelDataTypeField subsearchField = subsearchFields.get(j); |
1687 | | - if (!isSelected[j]) { |
1688 | | - mainUnionProjects.add(context.rexBuilder.makeNullLiteral(subsearchField.getType())); |
1689 | | - subsearchUnionProjects.add(context.rexBuilder.makeInputRef(subsearchNode, j)); |
1690 | | - names.add(subsearchField.getName()); |
| 1670 | + @Override |
| 1671 | + public RelNode visitMultisearch(Multisearch node, CalcitePlanContext context) { |
| 1672 | + List<RelNode> subsearchNodes = new ArrayList<>(); |
| 1673 | + for (UnresolvedPlan subsearch : node.getSubsearches()) { |
| 1674 | + UnresolvedPlan prunedSubSearch = subsearch.accept(new EmptySourcePropagateVisitor(), null); |
| 1675 | + prunedSubSearch.accept(this, context); |
| 1676 | + subsearchNodes.add(context.relBuilder.build()); |
| 1677 | + } |
| 1678 | + |
| 1679 | + // Use shared schema merging logic that handles type conflicts via field renaming |
| 1680 | + List<RelNode> alignedNodes = |
| 1681 | + SchemaUnifier.buildUnifiedSchemaWithConflictResolution(subsearchNodes, context); |
| 1682 | + |
| 1683 | + for (RelNode alignedNode : alignedNodes) { |
| 1684 | + context.relBuilder.push(alignedNode); |
| 1685 | + } |
| 1686 | + context.relBuilder.union(true, alignedNodes.size()); |
| 1687 | + |
| 1688 | + RelDataType rowType = context.relBuilder.peek().getRowType(); |
| 1689 | + String timestampField = findTimestampField(rowType); |
| 1690 | + if (timestampField != null) { |
| 1691 | + RelDataTypeField timestampFieldRef = rowType.getField(timestampField, false, false); |
| 1692 | + if (timestampFieldRef != null) { |
| 1693 | + RexNode timestampRef = |
| 1694 | + context.rexBuilder.makeInputRef( |
| 1695 | + context.relBuilder.peek(), timestampFieldRef.getIndex()); |
| 1696 | + context.relBuilder.sort(context.relBuilder.desc(timestampRef)); |
1691 | 1697 | } |
1692 | 1698 | } |
1693 | 1699 |
|
1694 | | - // 3.3 Uniquify names in case the merged names have duplicates |
1695 | | - List<String> uniqNames = |
1696 | | - SqlValidatorUtil.uniquify(names, SqlValidatorUtil.EXPR_SUGGESTER, true); |
1697 | | - |
1698 | | - // 4. Apply new schema over two query plans |
1699 | | - RelNode projectedMainNode = |
1700 | | - context.relBuilder.push(mainNode).project(mainUnionProjects, uniqNames).build(); |
1701 | | - RelNode projectedSubsearchNode = |
1702 | | - context.relBuilder.push(subsearchNode).project(subsearchUnionProjects, uniqNames).build(); |
1703 | | - |
1704 | | - // 5. Union all two projected plans |
1705 | | - context.relBuilder.push(projectedMainNode); |
1706 | | - context.relBuilder.push(projectedSubsearchNode); |
1707 | | - context.relBuilder.union(true); |
1708 | 1700 | return context.relBuilder.peek(); |
1709 | 1701 | } |
1710 | 1702 |
|
| 1703 | + /** |
| 1704 | + * Finds the timestamp field for multisearch ordering. |
| 1705 | + * |
| 1706 | + * @param rowType The row type to search for timestamp fields |
| 1707 | + * @return The name of the timestamp field, or null if not found |
| 1708 | + */ |
| 1709 | + private String findTimestampField(RelDataType rowType) { |
| 1710 | + String[] candidates = {"@timestamp", "_time", "timestamp", "time"}; |
| 1711 | + for (String fieldName : candidates) { |
| 1712 | + RelDataTypeField field = rowType.getField(fieldName, false, false); |
| 1713 | + if (field != null) { |
| 1714 | + return fieldName; |
| 1715 | + } |
| 1716 | + } |
| 1717 | + return null; |
| 1718 | + } |
| 1719 | + |
1711 | 1720 | /* |
1712 | 1721 | * Unsupported Commands of PPL with Calcite for OpenSearch 3.0.0-beta |
1713 | 1722 | */ |
|
0 commit comments