From 23ff544172a86e205043bb14b14e01436b8bb823 Mon Sep 17 00:00:00 2001
From: Lantao Jin <ltjin@amazon.com>
Date: Mon, 3 Mar 2025 22:54:00 +0800
Subject: [PATCH 1/2] Implement ppl InSubquery command with Calcite

Signed-off-by: Lantao Jin <ltjin@amazon.com>
---
 .../sql/ast/AbstractNodeVisitor.java          |    5 +
 .../ast/expression/subquery/InSubquery.java   |   34 +
 .../sql/calcite/CalciteRelNodeVisitor.java    |    2 +-
 .../sql/calcite/CalciteRexNodeVisitor.java    |   40 +-
 .../calcite/utils/OpenSearchTypeFactory.java  |    4 +
 .../standalone/CalcitePPLAggregationIT.java   |  498 ++-----
 .../calcite/standalone/CalcitePPLBasicIT.java |  744 +++-------
 .../standalone/CalcitePPLInSubqueryIT.java    |  353 +++++
 .../standalone/CalcitePPLIntegTestCase.java   |   23 +
 .../calcite/standalone/CalcitePPLJoinIT.java  | 1213 ++++-------------
 .../calcite/standalone/CalcitePPLSortIT.java  |  580 ++------
 .../sql/legacy/SQLIntegTestCase.java          |   14 +-
 .../org/opensearch/sql/legacy/TestUtils.java  |   10 +
 .../opensearch/sql/legacy/TestsConstants.java |    2 +
 .../opensearch/sql/ppl/PPLIntegTestCase.java  |    2 +-
 .../work_information_index_mapping.json       |   18 +
 .../worker_index_mapping.json                 |   21 +
 .../src/test/resources/work_information.json  |   10 +
 integ-test/src/test/resources/worker.json     |   14 +
 .../scan/OpenSearchIndexEnumerator.java       |   10 +-
 .../util/JdbcOpenSearchDataTypeConvertor.java |   12 +-
 ppl/src/main/antlr/OpenSearchPPLParser.g4     |   19 +
 .../opensearch/sql/ppl/parser/AstBuilder.java |    6 +
 .../sql/ppl/parser/AstExpressionBuilder.java  |   26 +-
 .../ppl/calcite/CalcitePPLInSubqueryTest.java |  237 ++++
 25 files changed, 1482 insertions(+), 2415 deletions(-)
 create mode 100644 core/src/main/java/org/opensearch/sql/ast/expression/subquery/InSubquery.java
 create mode 100644 integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLInSubqueryIT.java
 create mode 100644 integ-test/src/test/resources/indexDefinitions/work_information_index_mapping.json
 create mode 100644 integ-test/src/test/resources/indexDefinitions/worker_index_mapping.json
 create mode 100644 integ-test/src/test/resources/work_information.json
 create mode 100644 integ-test/src/test/resources/worker.json
 create mode 100644 ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLInSubqueryTest.java

diff --git a/core/src/main/java/org/opensearch/sql/ast/AbstractNodeVisitor.java b/core/src/main/java/org/opensearch/sql/ast/AbstractNodeVisitor.java
index 968ac38988e..22eac0c2213 100644
--- a/core/src/main/java/org/opensearch/sql/ast/AbstractNodeVisitor.java
+++ b/core/src/main/java/org/opensearch/sql/ast/AbstractNodeVisitor.java
@@ -36,6 +36,7 @@
 import org.opensearch.sql.ast.expression.When;
 import org.opensearch.sql.ast.expression.WindowFunction;
 import org.opensearch.sql.ast.expression.Xor;
+import org.opensearch.sql.ast.expression.subquery.InSubquery;
 import org.opensearch.sql.ast.statement.Explain;
 import org.opensearch.sql.ast.statement.Query;
 import org.opensearch.sql.ast.statement.Statement;
@@ -314,6 +315,10 @@ public T visitExplain(Explain node, C context) {
     return visitStatement(node, context);
   }
 
+  public T visitInSubquery(InSubquery node, C context) {
+    return visitChildren(node, context);
+  }
+
   public T visitPaginate(Paginate paginate, C context) {
     return visitChildren(paginate, context);
   }
diff --git a/core/src/main/java/org/opensearch/sql/ast/expression/subquery/InSubquery.java b/core/src/main/java/org/opensearch/sql/ast/expression/subquery/InSubquery.java
new file mode 100644
index 00000000000..4845d50ec65
--- /dev/null
+++ b/core/src/main/java/org/opensearch/sql/ast/expression/subquery/InSubquery.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.opensearch.sql.ast.expression.subquery;
+
+import java.util.List;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.ToString;
+import org.opensearch.sql.ast.AbstractNodeVisitor;
+import org.opensearch.sql.ast.expression.UnresolvedExpression;
+import org.opensearch.sql.ast.tree.UnresolvedPlan;
+
+@Getter
+@ToString
+@EqualsAndHashCode(callSuper = false)
+@RequiredArgsConstructor
+public class InSubquery extends UnresolvedExpression {
+  private final List<UnresolvedExpression> value;
+  private final UnresolvedPlan query;
+
+  @Override
+  public List<UnresolvedExpression> getChild() {
+    return value;
+  }
+
+  @Override
+  public <R, C> R accept(AbstractNodeVisitor<R, C> nodeVisitor, C context) {
+    return nodeVisitor.visitInSubquery(this, context);
+  }
+}
diff --git a/core/src/main/java/org/opensearch/sql/calcite/CalciteRelNodeVisitor.java b/core/src/main/java/org/opensearch/sql/calcite/CalciteRelNodeVisitor.java
index 15ae04db2dd..06018009248 100644
--- a/core/src/main/java/org/opensearch/sql/calcite/CalciteRelNodeVisitor.java
+++ b/core/src/main/java/org/opensearch/sql/calcite/CalciteRelNodeVisitor.java
@@ -51,7 +51,7 @@ public class CalciteRelNodeVisitor extends AbstractNodeVisitor<RelNode, CalciteP
   private final CalciteAggCallVisitor aggVisitor;
 
   public CalciteRelNodeVisitor() {
-    this.rexVisitor = new CalciteRexNodeVisitor();
+    this.rexVisitor = new CalciteRexNodeVisitor(this);
     this.aggVisitor = new CalciteAggCallVisitor(rexVisitor);
   }
 
diff --git a/core/src/main/java/org/opensearch/sql/calcite/CalciteRexNodeVisitor.java b/core/src/main/java/org/opensearch/sql/calcite/CalciteRexNodeVisitor.java
index fd67d08bf1a..781193d0e02 100644
--- a/core/src/main/java/org/opensearch/sql/calcite/CalciteRexNodeVisitor.java
+++ b/core/src/main/java/org/opensearch/sql/calcite/CalciteRexNodeVisitor.java
@@ -12,6 +12,8 @@
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
+import lombok.RequiredArgsConstructor;
+import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.rex.RexBuilder;
@@ -38,9 +40,13 @@
 import org.opensearch.sql.ast.expression.SpanUnit;
 import org.opensearch.sql.ast.expression.UnresolvedExpression;
 import org.opensearch.sql.ast.expression.Xor;
+import org.opensearch.sql.ast.expression.subquery.InSubquery;
+import org.opensearch.sql.ast.tree.UnresolvedPlan;
 import org.opensearch.sql.calcite.utils.BuiltinFunctionUtils;
 
+@RequiredArgsConstructor
 public class CalciteRexNodeVisitor extends AbstractNodeVisitor<RexNode, CalcitePlanContext> {
+  private final CalciteRelNodeVisitor planVisitor;
 
   public RexNode analyze(UnresolvedExpression unresolved, CalcitePlanContext context) {
     return unresolved.accept(this, context);
@@ -150,11 +156,18 @@ public RexNode visitEqualTo(EqualTo node, CalcitePlanContext context) {
   public RexNode visitQualifiedName(QualifiedName node, CalcitePlanContext context) {
     if (context.isResolvingJoinCondition()) {
       List<String> parts = node.getParts();
-      if (parts.size() == 1) { // Handle the case of `id = cid`
+      if (parts.size() == 1) {
+        // Handle the case of `id = cid`
         try {
-          return context.relBuilder.field(2, 0, parts.get(0));
-        } catch (IllegalArgumentException i) {
-          return context.relBuilder.field(2, 1, parts.get(0));
+          // TODO what if there is join clause in InSubquery in join condition
+          // for subquery in join condition
+          return context.relBuilder.field(parts.get(0));
+        } catch (IllegalArgumentException e) {
+          try {
+            return context.relBuilder.field(2, 0, parts.get(0));
+          } catch (IllegalArgumentException ee) {
+            return context.relBuilder.field(2, 1, parts.get(0));
+          }
         }
       } else if (parts.size()
           == 2) { // Handle the case of `t1.id = t2.id` or `alias1.id = alias2.id`
@@ -242,4 +255,23 @@ public RexNode visitFunction(Function node, CalcitePlanContext context) {
     return context.rexBuilder.makeCall(
         BuiltinFunctionUtils.translate(node.getFuncName()), arguments);
   }
+
+  @Override
+  public RexNode visitInSubquery(InSubquery node, CalcitePlanContext context) {
+    List<RexNode> nodes = node.getChild().stream().map(child -> analyze(child, context)).toList();
+    UnresolvedPlan subquery = node.getQuery();
+    RelNode subqueryRel = subquery.accept(planVisitor, context);
+    context.relBuilder.build();
+    return context.relBuilder.in(subqueryRel, nodes);
+    // TODO
+    // The {@link org.apache.calcite.tools.RelBuilder#in(RexNode,java.util.function.Function)}
+    // only support one expression. Change to follow code after calcite fixed.
+    //    return context.relBuilder.in(
+    //        nodes.getFirst(),
+    //        b -> {
+    //          RelNode subqueryRel = subquery.accept(planVisitor, context);
+    //          b.build();
+    //          return subqueryRel;
+    //        });
+  }
 }
diff --git a/core/src/main/java/org/opensearch/sql/calcite/utils/OpenSearchTypeFactory.java b/core/src/main/java/org/opensearch/sql/calcite/utils/OpenSearchTypeFactory.java
index 4a992c885ae..47250cc7fed 100644
--- a/core/src/main/java/org/opensearch/sql/calcite/utils/OpenSearchTypeFactory.java
+++ b/core/src/main/java/org/opensearch/sql/calcite/utils/OpenSearchTypeFactory.java
@@ -112,6 +112,10 @@ public static RelDataType convertExprTypeToRelDataType(ExprType fieldType, boole
         return TYPE_FACTORY.createSqlType(SqlTypeName.BINARY, nullable);
       } else if (fieldType.legacyTypeName().equalsIgnoreCase("timestamp")) {
         return TYPE_FACTORY.createSqlType(SqlTypeName.TIMESTAMP, nullable);
+      } else if (fieldType.legacyTypeName().equalsIgnoreCase("date")) {
+        return TYPE_FACTORY.createSqlType(SqlTypeName.DATE, nullable);
+      } else if (fieldType.legacyTypeName().equalsIgnoreCase("time")) {
+        return TYPE_FACTORY.createSqlType(SqlTypeName.TIME, nullable);
       } else if (fieldType.legacyTypeName().equalsIgnoreCase("geo_point")) {
         return TYPE_FACTORY.createSqlType(SqlTypeName.GEOMETRY, nullable);
       } else if (fieldType.legacyTypeName().equalsIgnoreCase("text")) {
diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLAggregationIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLAggregationIT.java
index 73c7d2bd363..d875967e934 100644
--- a/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLAggregationIT.java
+++ b/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLAggregationIT.java
@@ -6,8 +6,13 @@
 package org.opensearch.sql.calcite.standalone;
 
 import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_BANK;
+import static org.opensearch.sql.util.MatcherUtils.rows;
+import static org.opensearch.sql.util.MatcherUtils.schema;
+import static org.opensearch.sql.util.MatcherUtils.verifyDataRows;
+import static org.opensearch.sql.util.MatcherUtils.verifySchema;
 
 import java.io.IOException;
+import org.json.JSONObject;
 import org.junit.Ignore;
 import org.junit.jupiter.api.Test;
 import org.opensearch.client.Request;
@@ -30,296 +35,103 @@ public void testSimpleCount0() throws IOException {
     request2.setJsonEntity("{\"name\": \"world\", \"age\": 30}");
     client().performRequest(request2);
 
-    String actual = execute("source=test | stats count() as c");
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"c\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      2\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 1,\n"
-            + "  \"size\": 1\n"
-            + "}",
-        actual);
+    JSONObject actual = executeQuery("source=test | stats count() as c");
+    verifySchema(actual, schema("c", "long"));
+    verifyDataRows(actual, rows(2));
   }
 
   @Test
   public void testSimpleCount() {
-    String actual = execute(String.format("source=%s | stats count() as c", TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"c\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      7\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 1,\n"
-            + "  \"size\": 1\n"
-            + "}",
-        actual);
+    JSONObject actual =
+        executeQuery(String.format("source=%s | stats count() as c", TEST_INDEX_BANK));
+    verifySchema(actual, schema("c", "long"));
+    verifyDataRows(actual, rows(7));
   }
 
   @Test
   public void testSimpleAvg() {
-    String actual = execute(String.format("source=%s | stats avg(balance)", TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"avg(balance)\",\n"
-            + "      \"type\": \"double\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      26710.428571428572\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 1,\n"
-            + "  \"size\": 1\n"
-            + "}",
-        actual);
+    JSONObject actual =
+        executeQuery(String.format("source=%s | stats avg(balance)", TEST_INDEX_BANK));
+    verifySchema(actual, schema("avg(balance)", "double"));
+    verifyDataRows(actual, rows(26710.428571428572));
   }
 
   @Test
   public void testSumAvg() {
-    String actual = execute(String.format("source=%s | stats sum(balance)", TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"sum(balance)\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      186973\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 1,\n"
-            + "  \"size\": 1\n"
-            + "}",
-        actual);
+    JSONObject actual =
+        executeQuery(String.format("source=%s | stats sum(balance)", TEST_INDEX_BANK));
+    verifySchema(actual, schema("sum(balance)", "long"));
+
+    verifyDataRows(actual, rows(186973));
   }
 
   @Test
   public void testMultipleAggregatesWithAliases() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source=%s | stats avg(balance) as avg, max(balance) as max, min(balance) as min,"
                     + " count()",
                 TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"avg\",\n"
-            + "      \"type\": \"double\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"max\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"min\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"count()\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      26710.428571428572,\n"
-            + "      48086,\n"
-            + "      4180,\n"
-            + "      7\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 1,\n"
-            + "  \"size\": 1\n"
-            + "}",
-        actual);
+    verifySchema(
+        actual,
+        schema("avg", "double"),
+        schema("max", "long"),
+        schema("min", "long"),
+        schema("count()", "long"));
+    verifyDataRows(actual, rows(26710.428571428572, 48086, 4180, 7));
   }
 
   @Test
   public void testMultipleAggregatesWithAliasesByClause() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source=%s | stats avg(balance) as avg, max(balance) as max, min(balance) as min,"
                     + " count() as cnt by gender",
                 TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"gender\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"avg\",\n"
-            + "      \"type\": \"double\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"max\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"min\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"cnt\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"F\",\n"
-            + "      40488.0,\n"
-            + "      48086,\n"
-            + "      32838,\n"
-            + "      3\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"M\",\n"
-            + "      16377.25,\n"
-            + "      39225,\n"
-            + "      4180,\n"
-            + "      4\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 2,\n"
-            + "  \"size\": 2\n"
-            + "}",
-        actual);
+    verifySchema(
+        actual,
+        schema("gender", "string"),
+        schema("avg", "double"),
+        schema("max", "long"),
+        schema("min", "long"),
+        schema("cnt", "long"));
+    verifyDataRows(
+        actual, rows("F", 40488.0, 48086, 32838, 3), rows("M", 16377.25, 39225, 4180, 4));
   }
 
   @Test
   public void testAvgByField() {
-    String actual =
-        execute(String.format("source=%s | stats avg(balance) by gender", TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"gender\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"avg(balance)\",\n"
-            + "      \"type\": \"double\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"F\",\n"
-            + "      40488.0\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"M\",\n"
-            + "      16377.25\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 2,\n"
-            + "  \"size\": 2\n"
-            + "}",
-        actual);
+    JSONObject actual =
+        executeQuery(String.format("source=%s | stats avg(balance) by gender", TEST_INDEX_BANK));
+    verifySchema(actual, schema("gender", "string"), schema("avg(balance)", "double"));
+    verifyDataRows(actual, rows("F", 40488.0), rows("M", 16377.25));
   }
 
   @org.junit.Test
   public void testAvgBySpan() {
-    String actual =
-        execute(String.format("source=%s | stats avg(balance) by span(age, 10)", TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"span(age,10)\",\n"
-            + "      \"type\": \"double\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"avg(balance)\",\n"
-            + "      \"type\": \"double\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      20.0,\n"
-            + "      32838.0\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      30.0,\n"
-            + "      25689.166666666668\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 2,\n"
-            + "  \"size\": 2\n"
-            + "}",
-        actual);
+    JSONObject actual =
+        executeQuery(
+            String.format("source=%s | stats avg(balance) by span(age, 10)", TEST_INDEX_BANK));
+    verifySchema(actual, schema("span(age,10)", "double"), schema("avg(balance)", "double"));
+    verifyDataRows(actual, rows(20.0, 32838.0), rows(30.0, 25689.166666666668));
   }
 
   @Test
   public void testAvgBySpanAndFields() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source=%s | stats avg(balance) by span(age, 10) as age_span, gender",
                 TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"gender\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"age_span\",\n"
-            + "      \"type\": \"double\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"avg(balance)\",\n"
-            + "      \"type\": \"double\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"F\",\n"
-            + "      30.0,\n"
-            + "      44313.0\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"M\",\n"
-            + "      30.0,\n"
-            + "      16377.25\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"F\",\n"
-            + "      20.0,\n"
-            + "      32838.0\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 3,\n"
-            + "  \"size\": 3\n"
-            + "}",
-        actual);
+    verifySchema(
+        actual,
+        schema("gender", "string"),
+        schema("age_span", "double"),
+        schema("avg(balance)", "double"));
+    verifyDataRows(
+        actual, rows("F", 30.0, 44313.0), rows("M", 30.0, 16377.25), rows("F", 20.0, 32838.0));
   }
 
   /**
@@ -328,207 +140,85 @@ public void testAvgBySpanAndFields() {
    */
   @Ignore
   public void testAvgByTimeSpanAndFields() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source=%s | stats avg(balance) by span(birthdate, 1 day) as age_balance",
                 TEST_INDEX_BANK));
-    assertEquals("", actual);
+    verifySchema(
+        actual, schema("span(birthdate, 1 day)", "string"), schema("age_balance", "double"));
+    verifyDataRows(actual, rows("F", 3L), rows("M", 4L));
   }
 
   @Test
   public void testCountDistinct() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format("source=%s | stats distinct_count(state) by gender", TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"gender\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"distinct_count(state)\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"F\",\n"
-            + "      3\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"M\",\n"
-            + "      4\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 2,\n"
-            + "  \"size\": 2\n"
-            + "}",
-        actual);
+    verifySchema(actual, schema("gender", "string"), schema("distinct_count(state)", "long"));
+    verifyDataRows(actual, rows("F", 3L), rows("M", 4L));
   }
 
   @Test
   public void testCountDistinctWithAlias() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source=%s | stats distinct_count(state) as dc by gender", TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"gender\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"dc\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"F\",\n"
-            + "      3\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"M\",\n"
-            + "      4\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 2,\n"
-            + "  \"size\": 2\n"
-            + "}",
-        actual);
+    verifySchema(actual, schema("gender", "string"), schema("dc", "long"));
+    verifyDataRows(actual, rows("F", 3L), rows("M", 4L));
   }
 
   @Ignore
   public void testApproxCountDistinct() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source=%s | stats distinct_count_approx(state) by gender", TEST_INDEX_BANK));
   }
 
   @Test
   public void testStddevSampStddevPop() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source=%s | stats stddev_samp(balance) as ss, stddev_pop(balance) as sp by gender",
                 TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"gender\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"ss\",\n"
-            + "      \"type\": \"double\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"sp\",\n"
-            + "      \"type\": \"double\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"F\",\n"
-            + "      7624.132999889233,\n"
-            + "      6225.078526947806\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"M\",\n"
-            + "      16177.114233282358,\n"
-            + "      14009.791885945344\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 2,\n"
-            + "  \"size\": 2\n"
-            + "}",
-        actual);
+    verifySchema(
+        actual, schema("gender", "string"), schema("ss", "double"), schema("sp", "double"));
+    verifyDataRows(
+        actual,
+        rows("F", 7624.132999889233, 6225.078526947806),
+        rows("M", 16177.114233282358, 14009.791885945344));
   }
 
   @Test
   public void testAggWithEval() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source=%s | eval a = 1, b = a | stats avg(a) as avg_a by b", TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"b\",\n"
-            + "      \"type\": \"integer\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"avg_a\",\n"
-            + "      \"type\": \"double\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      1,\n"
-            + "      1.0\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 1,\n"
-            + "  \"size\": 1\n"
-            + "}",
-        actual);
+    verifySchema(actual, schema("b", "integer"), schema("avg_a", "double"));
+    verifyDataRows(actual, rows(1, 1.0));
   }
 
   @Test
   public void testAggWithBackticksAlias() {
-    String actual =
-        execute(String.format("source=%s | stats sum(`balance`) as `sum_b`", TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"sum_b\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      186973\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 1,\n"
-            + "  \"size\": 1\n"
-            + "}",
-        actual);
+    JSONObject actual =
+        executeQuery(String.format("source=%s | stats sum(`balance`) as `sum_b`", TEST_INDEX_BANK));
+    verifySchema(actual, schema("sum_b", "long"));
+    verifyDataRows(actual, rows(186973L));
   }
 
   @Test
   public void testSimpleTwoLevelStats() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source=%s | stats avg(balance) as avg_by_gender by gender | stats"
                     + " avg(avg_by_gender) as avg_avg",
                 TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"avg_avg\",\n"
-            + "      \"type\": \"double\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      28432.625\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 1,\n"
-            + "  \"size\": 1\n"
-            + "}",
-        actual);
+    verifySchema(actual, schema("avg_avg", "double"));
+    verifyDataRows(actual, rows(28432.625));
   }
 }
diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLBasicIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLBasicIT.java
index ca1250b2cf1..5f892073a53 100644
--- a/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLBasicIT.java
+++ b/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLBasicIT.java
@@ -6,9 +6,13 @@
 package org.opensearch.sql.calcite.standalone;
 
 import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_BANK;
+import static org.opensearch.sql.util.MatcherUtils.rows;
+import static org.opensearch.sql.util.MatcherUtils.schema;
+import static org.opensearch.sql.util.MatcherUtils.verifyDataRows;
+import static org.opensearch.sql.util.MatcherUtils.verifySchema;
 
 import java.io.IOException;
-import org.junit.Ignore;
+import org.json.JSONObject;
 import org.junit.jupiter.api.Test;
 import org.opensearch.client.Request;
 
@@ -37,602 +41,265 @@ public void testInvalidTable() {
 
   @Test
   public void testSourceQuery() {
-    String actual = execute("source=test");
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"name\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"age\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"hello\",\n"
-            + "      20\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"world\",\n"
-            + "      30\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 2,\n"
-            + "  \"size\": 2\n"
-            + "}",
-        actual);
+    JSONObject actual = executeQuery("source=test");
+    verifySchema(actual, schema("name", "string"), schema("age", "long"));
+    verifyDataRows(actual, rows("hello", 20), rows("world", 30));
   }
 
   @Test
   public void testMultipleSourceQuery() {
-    String actual = execute("source=test, test");
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"name\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"age\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"hello\",\n"
-            + "      20\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"world\",\n"
-            + "      30\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"hello\",\n"
-            + "      20\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"world\",\n"
-            + "      30\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 4,\n"
-            + "  \"size\": 4\n"
-            + "}",
-        actual);
+    JSONObject actual = executeQuery("source=test, test");
+    verifySchema(actual, schema("name", "string"), schema("age", "long"));
+    verifyDataRows(
+        actual, rows("hello", 20), rows("world", 30), rows("hello", 20), rows("world", 30));
   }
 
   @Test
   public void testSourceFieldQuery() {
-    String actual = execute("source=test | fields name");
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"name\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"hello\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"world\"\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 2,\n"
-            + "  \"size\": 2\n"
-            + "}",
-        actual);
+    JSONObject actual = executeQuery("source=test | fields name");
+    verifySchema(actual, schema("name", "string"));
+    verifyDataRows(actual, rows("hello"), rows("world"));
   }
 
   @Test
   public void testFilterQuery1() {
-    String actual = execute("source=test | where age = 30 | fields name, age");
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"name\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"age\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"world\",\n"
-            + "      30\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 1,\n"
-            + "  \"size\": 1\n"
-            + "}",
-        actual);
+    JSONObject actual = executeQuery("source=test | where age = 30 | fields name, age");
+    verifySchema(actual, schema("name", "string"), schema("age", "long"));
+    verifyDataRows(actual, rows("world", 30));
   }
 
   @Test
   public void testFilterQuery2() {
-    String actual = execute("source=test | where age = 20 | fields name, age");
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"name\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"age\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"hello\",\n"
-            + "      20\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 1,\n"
-            + "  \"size\": 1\n"
-            + "}",
-        actual);
+    JSONObject actual = executeQuery("source=test | where age = 20 | fields name, age");
+    verifySchema(actual, schema("name", "string"), schema("age", "long"));
+    verifyDataRows(actual, rows("hello", 20));
   }
 
   @Test
   public void testFilterQuery3() {
-    String actual = execute("source=test | where age > 10 AND age < 100 | fields name, age");
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"name\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"age\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"hello\",\n"
-            + "      20\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"world\",\n"
-            + "      30\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 2,\n"
-            + "  \"size\": 2\n"
-            + "}",
-        actual);
+    JSONObject actual =
+        executeQuery("source=test | where age > 10 AND age < 100 | fields name, age");
+    verifySchema(actual, schema("name", "string"), schema("age", "long"));
+    verifyDataRows(actual, rows("hello", 20), rows("world", 30));
   }
 
-  // TODO fail after merged https://github.com/opensearch-project/sql/pull/3327
-  @Ignore
   @Test
   public void testFilterQueryWithOr() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
-                "source=%s | where (account_number = 25 or balance > 10000) and gender = 'M' |"
+                "source=%s | where (account_number = 20 or city = 'Brogan') and balance > 10000 |"
                     + " fields firstname, lastname",
                 TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"firstname\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"lastname\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"Amber JOHnny\",\n"
-            + "      \"Duke Willmington\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Elinor\",\n"
-            + "      \"Ratliff\"\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 2,\n"
-            + "  \"size\": 2\n"
-            + "}",
-        actual);
+    verifySchema(actual, schema("firstname", "string"), schema("lastname", "string"));
+    verifyDataRows(actual, rows("Amber JOHnny", "Duke Willmington"), rows("Elinor", "Ratliff"));
   }
 
-  // TODO fail after merged https://github.com/opensearch-project/sql/pull/3327
-  @Ignore
   @Test
   public void testFilterQueryWithOr2() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
-                "source=%s (account_number = 25 or balance > 10000) and gender = 'M' |"
+                "source=%s (account_number = 20 or city = 'Brogan') and balance > 10000 |"
                     + " fields firstname, lastname",
                 TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"firstname\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"lastname\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"Amber JOHnny\",\n"
-            + "      \"Duke Willmington\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Elinor\",\n"
-            + "      \"Ratliff\"\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 2,\n"
-            + "  \"size\": 2\n"
-            + "}",
-        actual);
+    verifySchema(actual, schema("firstname", "string"), schema("lastname", "string"));
+    verifyDataRows(actual, rows("Amber JOHnny", "Duke Willmington"), rows("Elinor", "Ratliff"));
   }
 
   @Test
   public void testQueryMinusFields() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format("source=%s | fields - firstname, lastname, birthdate", TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"account_number\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"address\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"gender\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"city\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"balance\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"employer\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"state\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"age\",\n"
-            + "      \"type\": \"integer\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"email\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"male\",\n"
-            + "      \"type\": \"boolean\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      1,\n"
-            + "      \"880 Holmes Lane\",\n"
-            + "      \"M\",\n"
-            + "      \"Brogan\",\n"
-            + "      39225,\n"
-            + "      \"Pyrami\",\n"
-            + "      \"IL\",\n"
-            + "      32,\n"
-            + "      \"amberduke@pyrami.com\",\n"
-            + "      true\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      6,\n"
-            + "      \"671 Bristol Street\",\n"
-            + "      \"M\",\n"
-            + "      \"Dante\",\n"
-            + "      5686,\n"
-            + "      \"Netagy\",\n"
-            + "      \"TN\",\n"
-            + "      36,\n"
-            + "      \"hattiebond@netagy.com\",\n"
-            + "      true\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      13,\n"
-            + "      \"789 Madison Street\",\n"
-            + "      \"F\",\n"
-            + "      \"Nogal\",\n"
-            + "      32838,\n"
-            + "      \"Quility\",\n"
-            + "      \"VA\",\n"
-            + "      28,\n"
-            + "      \"nanettebates@quility.com\",\n"
-            + "      false\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      18,\n"
-            + "      \"467 Hutchinson Court\",\n"
-            + "      \"M\",\n"
-            + "      \"Orick\",\n"
-            + "      4180,\n"
-            + "      \"Boink\",\n"
-            + "      \"MD\",\n"
-            + "      33,\n"
-            + "      \"daleadams@boink.com\",\n"
-            + "      true\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      20,\n"
-            + "      \"282 Kings Place\",\n"
-            + "      \"M\",\n"
-            + "      \"Ribera\",\n"
-            + "      16418,\n"
-            + "      \"Scentric\",\n"
-            + "      \"WA\",\n"
-            + "      36,\n"
-            + "      \"elinorratliff@scentric.com\",\n"
-            + "      true\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      25,\n"
-            + "      \"171 Putnam Avenue\",\n"
-            + "      \"F\",\n"
-            + "      \"Nicholson\",\n"
-            + "      40540,\n"
-            + "      \"Filodyne\",\n"
-            + "      \"PA\",\n"
-            + "      39,\n"
-            + "      \"virginiaayala@filodyne.com\",\n"
-            + "      false\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      32,\n"
-            + "      \"702 Quentin Street\",\n"
-            + "      \"F\",\n"
-            + "      \"Veguita\",\n"
-            + "      48086,\n"
-            + "      \"Quailcom\",\n"
-            + "      \"IN\",\n"
-            + "      34,\n"
-            + "      \"dillardmcpherson@quailcom.com\",\n"
-            + "      false\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 7,\n"
-            + "  \"size\": 7\n"
-            + "}",
-        actual);
+    verifySchema(
+        actual,
+        schema("account_number", "long"),
+        schema("address", "string"),
+        schema("gender", "string"),
+        schema("city", "string"),
+        schema("balance", "long"),
+        schema("employer", "string"),
+        schema("state", "string"),
+        schema("age", "integer"),
+        schema("email", "string"),
+        schema("male", "boolean"));
+    verifyDataRows(
+        actual,
+        rows(
+            1,
+            "880 Holmes Lane",
+            "M",
+            "Brogan",
+            39225,
+            "Pyrami",
+            "IL",
+            32,
+            "amberduke@pyrami.com",
+            true),
+        rows(
+            6,
+            "671 Bristol Street",
+            "M",
+            "Dante",
+            5686,
+            "Netagy",
+            "TN",
+            36,
+            "hattiebond@netagy.com",
+            true),
+        rows(
+            13,
+            "789 Madison Street",
+            "F",
+            "Nogal",
+            32838,
+            "Quility",
+            "VA",
+            28,
+            "nanettebates@quility.com",
+            false),
+        rows(
+            18,
+            "467 Hutchinson Court",
+            "M",
+            "Orick",
+            4180,
+            "Boink",
+            "MD",
+            33,
+            "daleadams@boink.com",
+            true),
+        rows(
+            20,
+            "282 Kings Place",
+            "M",
+            "Ribera",
+            16418,
+            "Scentric",
+            "WA",
+            36,
+            "elinorratliff@scentric.com",
+            true),
+        rows(
+            25,
+            "171 Putnam Avenue",
+            "F",
+            "Nicholson",
+            40540,
+            "Filodyne",
+            "PA",
+            39,
+            "virginiaayala@filodyne.com",
+            false),
+        rows(
+            32,
+            "702 Quentin Street",
+            "F",
+            "Veguita",
+            48086,
+            "Quailcom",
+            "IN",
+            34,
+            "dillardmcpherson@quailcom.com",
+            false));
   }
 
-  // TODO bug: shouldn't return empty
-  @Ignore
+  @Test
   public void testQueryMinusFieldsWithFilter() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
-                "source=%s | where (account_number = 25 or balance > 10000) and gender = 'M' |"
+                "source=%s | where (account_number = 20 or city = 'Brogan') and balance > 10000 |"
                     + " fields - firstname, lastname",
                 TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"account_number\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"address\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"birthdate\",\n"
-            + "      \"type\": \"timestamp\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"gender\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"city\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"balance\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"employer\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"state\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"age\",\n"
-            + "      \"type\": \"integer\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"email\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"male\",\n"
-            + "      \"type\": \"boolean\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [],\n"
-            + "  \"total\": 0,\n"
-            + "  \"size\": 0\n"
-            + "}",
-        actual);
+    verifySchema(
+        actual,
+        schema("account_number", "long"),
+        schema("address", "string"),
+        schema("birthdate", "timestamp"),
+        schema("gender", "string"),
+        schema("city", "string"),
+        schema("balance", "long"),
+        schema("employer", "string"),
+        schema("state", "string"),
+        schema("age", "integer"),
+        schema("email", "string"),
+        schema("male", "boolean"));
+    verifyDataRows(
+        actual,
+        rows(
+            1,
+            "880 Holmes Lane",
+            "2017-10-23 00:00:00",
+            "M",
+            "Brogan",
+            39225,
+            "Pyrami",
+            "IL",
+            32,
+            "amberduke@pyrami.com",
+            true),
+        rows(
+            20,
+            "282 Kings Place",
+            "2018-06-27 00:00:00",
+            "M",
+            "Ribera",
+            16418,
+            "Scentric",
+            "WA",
+            36,
+            "elinorratliff@scentric.com",
+            true));
   }
 
   @Test
   public void testFieldsPlusThenMinus() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source=%s | fields + firstname, lastname, account_number | fields - firstname,"
                     + " lastname",
                 TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"account_number\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      1\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      6\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      13\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      18\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      20\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      25\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      32\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 7,\n"
-            + "  \"size\": 7\n"
-            + "}",
-        actual);
+    verifySchema(actual, schema("account_number", "long"));
+    verifyDataRows(actual, rows(1), rows(6), rows(13), rows(18), rows(20), rows(25), rows(32));
   }
 
-  // TODO fail after merged https://github.com/opensearch-project/sql/pull/3327
-  @Ignore
   @Test
   public void testMultipleTables() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format("source=%s, %s | stats count() as c", TEST_INDEX_BANK, TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"c\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      14\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 1,\n"
-            + "  \"size\": 1\n"
-            + "}",
-        actual);
+    verifySchema(actual, schema("c", "long"));
+    verifyDataRows(actual, rows(14));
   }
 
-  // TODO fail after merged https://github.com/opensearch-project/sql/pull/3327
-  @Ignore
   @Test
   public void testMultipleTablesAndFilters() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source=%s, %s gender = 'F' | stats count() as c",
                 TEST_INDEX_BANK, TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"c\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      6\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 1,\n"
-            + "  \"size\": 1\n"
-            + "}",
-        actual);
+    verifySchema(actual, schema("c", "long"));
+    verifyDataRows(actual, rows(6));
   }
 
   @Test
   public void testSelectDateTypeField() {
-    String actual = execute(String.format("source=%s | fields birthdate", TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"birthdate\",\n"
-            + "      \"type\": \"timestamp\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"2017-10-23 00:00:00\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"2017-11-20 00:00:00\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"2018-06-23 00:00:00\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"2018-11-13 23:33:20\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"2018-06-27 00:00:00\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"2018-08-19 00:00:00\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"2018-08-11 00:00:00\"\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 7,\n"
-            + "  \"size\": 7\n"
-            + "}",
-        actual);
+    JSONObject actual =
+        executeQuery(String.format("source=%s | fields birthdate", TEST_INDEX_BANK));
+    verifySchema(actual, schema("birthdate", "timestamp"));
+    verifyDataRows(
+        actual,
+        rows("2017-10-23 00:00:00"),
+        rows("2017-11-20 00:00:00"),
+        rows("2018-06-23 00:00:00"),
+        rows("2018-11-13 23:33:20"),
+        rows("2018-06-27 00:00:00"),
+        rows("2018-08-19 00:00:00"),
+        rows("2018-08-11 00:00:00"));
   }
 
   @Test
@@ -641,23 +308,8 @@ public void testAllFieldsInTable() throws IOException {
     request.setJsonEntity("{\"name\": \"hello\"}");
     client().performRequest(request);
 
-    String actual = execute("source=a | fields name");
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"name\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"hello\"\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 1,\n"
-            + "  \"size\": 1\n"
-            + "}",
-        actual);
+    JSONObject actual = executeQuery("source=a | fields name");
+    verifySchema(actual, schema("name", "string"));
+    verifyDataRows(actual, rows("hello"));
   }
 }
diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLInSubqueryIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLInSubqueryIT.java
new file mode 100644
index 00000000000..250b4a0eb70
--- /dev/null
+++ b/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLInSubqueryIT.java
@@ -0,0 +1,353 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.opensearch.sql.calcite.standalone;
+
+import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_OCCUPATION;
+import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_WORKER;
+import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_WORK_INFORMATION;
+import static org.opensearch.sql.util.MatcherUtils.rows;
+import static org.opensearch.sql.util.MatcherUtils.schema;
+import static org.opensearch.sql.util.MatcherUtils.verifyDataRows;
+import static org.opensearch.sql.util.MatcherUtils.verifySchema;
+
+import java.io.IOException;
+import org.json.JSONObject;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class CalcitePPLInSubqueryIT extends CalcitePPLIntegTestCase {
+
+  @Override
+  public void init() throws IOException {
+    super.init();
+
+    loadIndex(Index.WORKER);
+    loadIndex(Index.WORK_INFORMATION);
+    loadIndex(Index.OCCUPATION);
+  }
+
+  @Test
+  public void testSelfInSubquery() {
+    JSONObject result =
+        executeQuery(
+            String.format(
+                """
+                   source = %s
+                   | where id in [
+                       source = %s
+                       | where country = 'USA'
+                       | fields id
+                     ]
+                   """,
+                TEST_INDEX_WORKER, TEST_INDEX_WORKER));
+    verifySchema(
+        result,
+        schema("name", "string"),
+        schema("country", "string"),
+        schema("occupation", "string"),
+        schema("id", "integer"),
+        schema("salary", "integer"));
+    verifyDataRows(
+        result,
+        rows("Hello", "USA", "Artist", 1001, 70000),
+        rows("Tommy", "USA", "Teacher", 1006, 30000));
+  }
+
+  @Test
+  public void testWhereInSubquery() {
+    JSONObject result =
+        executeQuery(
+            String.format(
+                """
+                   source = %s
+                   | where id in [
+                       source = %s | fields uid
+                     ]
+                   | sort  - salary
+                   | fields id, name, salary
+                   """,
+                TEST_INDEX_WORKER, TEST_INDEX_WORK_INFORMATION));
+    verifySchema(
+        result, schema("id", "integer"), schema("name", "string"), schema("salary", "integer"));
+    verifyDataRows(
+        result,
+        rows(1003, "David", 120000),
+        rows(1002, "John", 120000),
+        rows(1000, "Jake", 100000),
+        rows(1005, "Jane", 90000),
+        rows(1006, "Tommy", 30000));
+  }
+
+  @Test
+  public void testFilterInSubquery() {
+    JSONObject result =
+        executeQuery(
+            String.format(
+                """
+                   source = %s id in [
+                       source = %s | fields uid
+                     ]
+                   | sort  - salary
+                   | fields id, name, salary
+                   """,
+                TEST_INDEX_WORKER, TEST_INDEX_WORK_INFORMATION));
+    verifySchema(
+        result, schema("id", "integer"), schema("name", "string"), schema("salary", "integer"));
+    verifyDataRows(
+        result,
+        rows(1003, "David", 120000),
+        rows(1002, "John", 120000),
+        rows(1000, "Jake", 100000),
+        rows(1005, "Jane", 90000),
+        rows(1006, "Tommy", 30000));
+  }
+
+  @Test
+  public void testInSubqueryWithParentheses() {
+    JSONObject result1 =
+        executeQuery(
+            String.format(
+                """
+                   source = %s
+                   | where (id) in [
+                       source = %s | fields uid
+                     ]
+                   | sort  - salary
+                   | fields id, name, salary
+                   """,
+                TEST_INDEX_WORKER, TEST_INDEX_WORK_INFORMATION));
+    JSONObject result2 =
+        executeQuery(
+            String.format(
+                """
+                   source = %s (id) in [
+                       source = %s | fields uid
+                     ]
+                   | sort  - salary
+                   | fields id, name, salary
+                   """,
+                TEST_INDEX_WORKER, TEST_INDEX_WORK_INFORMATION));
+    verifySchema(
+        result1, schema("id", "integer"), schema("name", "string"), schema("salary", "integer"));
+    verifySchema(
+        result2, schema("id", "integer"), schema("name", "string"), schema("salary", "integer"));
+    verifyDataRows(
+        result1,
+        rows(1003, "David", 120000),
+        rows(1002, "John", 120000),
+        rows(1000, "Jake", 100000),
+        rows(1005, "Jane", 90000),
+        rows(1006, "Tommy", 30000));
+    verifyDataRows(
+        result2,
+        rows(1003, "David", 120000),
+        rows(1002, "John", 120000),
+        rows(1000, "Jake", 100000),
+        rows(1005, "Jane", 90000),
+        rows(1006, "Tommy", 30000));
+  }
+
+  @Test
+  public void testTwoExpressionsInSubquery() {
+    JSONObject result =
+        executeQuery(
+            String.format(
+                """
+                   source = %s
+                   | where (id, name) in [
+                       source = %s | fields uid, name
+                     ]
+                   | sort  - salary
+                   | fields id, name, salary
+                   """,
+                TEST_INDEX_WORKER, TEST_INDEX_WORK_INFORMATION));
+    verifySchema(
+        result, schema("id", "integer"), schema("name", "string"), schema("salary", "integer"));
+    verifyDataRows(
+        result,
+        rows(1003, "David", 120000),
+        rows(1002, "John", 120000),
+        rows(1000, "Jake", 100000),
+        rows(1005, "Jane", 90000));
+  }
+
+  @Test
+  public void testWhereNotInSubquery() {
+    JSONObject result =
+        executeQuery(
+            String.format(
+                """
+                   source = %s
+                   | where id not in [
+                       source = %s | fields uid
+                     ]
+                   | sort  - salary
+                   | fields id, name, salary
+                   """,
+                TEST_INDEX_WORKER, TEST_INDEX_WORK_INFORMATION));
+    verifySchema(
+        result, schema("id", "integer"), schema("name", "string"), schema("salary", "integer"));
+    verifyDataRows(result, rows(1001, "Hello", 70000), rows(1004, "David", 0));
+  }
+
+  @Test
+  public void testFilterNotInSubquery() {
+    JSONObject result =
+        executeQuery(
+            String.format(
+                """
+                   source = %s id not in [
+                       source = %s | fields uid
+                     ]
+                   | sort  - salary
+                   | fields id, name, salary
+                   """,
+                TEST_INDEX_WORKER, TEST_INDEX_WORK_INFORMATION));
+    verifySchema(
+        result, schema("id", "integer"), schema("name", "string"), schema("salary", "integer"));
+    verifyDataRows(result, rows(1001, "Hello", 70000), rows(1004, "David", 0));
+  }
+
+  @Test
+  public void testTwoExpressionsNotInSubquery() {
+    JSONObject result =
+        executeQuery(
+            String.format(
+                """
+                   source = %s
+                   | where (id, name) not in [
+                       source = %s | fields uid, name
+                     ]
+                   | sort  - salary
+                   | fields id, name, salary
+                   """,
+                TEST_INDEX_WORKER, TEST_INDEX_WORK_INFORMATION));
+    verifySchema(
+        result, schema("id", "integer"), schema("name", "string"), schema("salary", "integer"));
+    verifyDataRows(
+        result, rows(1001, "Hello", 70000), rows(1004, "David", 0), rows(1006, "Tommy", 30000));
+  }
+
+  @Test
+  public void testEmptyInSubquery() {
+    JSONObject result =
+        executeQuery(
+            String.format(
+                """
+                   source = %s id not in [
+                       source = %s | where uid = 0000 | fields uid
+                     ]
+                   | sort  - salary
+                   | fields id, name, salary
+                   """,
+                TEST_INDEX_WORKER, TEST_INDEX_WORK_INFORMATION));
+    verifySchema(
+        result, schema("id", "integer"), schema("name", "string"), schema("salary", "integer"));
+    verifyDataRows(
+        result,
+        rows(1000, "Jake", 100000),
+        rows(1001, "Hello", 70000),
+        rows(1002, "John", 120000),
+        rows(1003, "David", 120000),
+        rows(1004, "David", 0),
+        rows(1005, "Jane", 90000),
+        rows(1006, "Tommy", 30000));
+  }
+
+  @Test
+  public void testNestedInSubquery() {
+    JSONObject result =
+        executeQuery(
+            String.format(
+                """
+                   source = %s
+                   | where id in [
+                       source = %s
+                       | where occupation in [
+                           source = %s
+                           | where occupation != 'Engineer'
+                           | fields occupation
+                         ]
+                       | fields uid
+                     ]
+                   | sort  - salary
+                   | fields id, name, salary
+                   """,
+                TEST_INDEX_WORKER, TEST_INDEX_WORK_INFORMATION, TEST_INDEX_OCCUPATION));
+    verifySchema(
+        result, schema("id", "integer"), schema("name", "string"), schema("salary", "integer"));
+    verifyDataRows(
+        result,
+        rows(1003, "David", 120000),
+        rows(1002, "John", 120000),
+        rows(1006, "Tommy", 30000));
+  }
+
+  @Ignore // TODO bug? fail in execution, the plan converted is correct
+  public void testInSubqueryAsJoinFilter() {
+    JSONObject result =
+        executeQuery(
+            String.format(
+                """
+                   source = %s
+                   | inner join left=a, right=b
+                       ON a.id = b.uid AND b.occupation in [
+                         source = %s | where occupation != 'Engineer' | fields occupation
+                       ]
+                       %s
+                   | fields a.id, a.name, a.salary, b.occupation
+                   """,
+                TEST_INDEX_WORKER, TEST_INDEX_OCCUPATION, TEST_INDEX_WORK_INFORMATION));
+    verifySchema(
+        result, schema("id", "integer"), schema("name", "string"), schema("salary", "integer"));
+    verifyDataRows(
+        result,
+        rows(1003, "David", 120000),
+        rows(1002, "John", 120000),
+        rows(1006, "Tommy", 30000));
+  }
+
+  @Test
+  public void failWhenNumOfColumnsNotMatchOutputOfSubquery() {
+    assertThrows(
+        "The number of columns in the left hand side of an IN subquery does not match the number of"
+            + " columns in the output of subquery",
+        AssertionError.class,
+        () ->
+            executeQuery(
+                String.format(
+                    """
+                   source = %s
+                   | where id in [
+                       source = %s | fields uid, department
+                     ]
+                   | sort  - salary
+                   | fields id, name, salary
+                   """,
+                    TEST_INDEX_WORKER, TEST_INDEX_WORK_INFORMATION)));
+  }
+
+  @Test
+  public void testInSubqueryWithTableAlias() {
+    JSONObject result =
+        executeQuery(
+            String.format(
+                """
+                   source = %s as o
+                   | where id in [
+                       source = %s as i
+                       | where i.department = 'DATA'
+                       | fields uid
+                     ]
+                   | sort - o.salary
+                   | fields o.id, o.name, o.salary
+                   """,
+                TEST_INDEX_WORKER, TEST_INDEX_WORK_INFORMATION));
+    verifySchema(
+        result, schema("id", "integer"), schema("name", "string"), schema("salary", "integer"));
+    verifyDataRows(result, rows(1002, "John", 120000), rows(1005, "Jane", 90000));
+  }
+}
diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLIntegTestCase.java b/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLIntegTestCase.java
index 9703dfaa631..05a4794c9aa 100644
--- a/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLIntegTestCase.java
+++ b/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLIntegTestCase.java
@@ -17,6 +17,7 @@
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicReference;
 import lombok.RequiredArgsConstructor;
+import org.json.JSONObject;
 import org.opensearch.client.RestClient;
 import org.opensearch.client.RestHighLevelClient;
 import org.opensearch.common.inject.AbstractModule;
@@ -140,6 +141,28 @@ public void onFailure(Exception e) {
     return actual.get();
   }
 
+  @Override
+  protected JSONObject executeQuery(String query) {
+    AtomicReference<JSONObject> actual = new AtomicReference<>();
+    pplService.execute(
+        new PPLQueryRequest(query, null, null),
+        new ResponseListener<ExecutionEngine.QueryResponse>() {
+
+          @Override
+          public void onResponse(ExecutionEngine.QueryResponse response) {
+            QueryResult result = new QueryResult(response.getSchema(), response.getResults());
+            String json = new SimpleJsonResponseFormatter(PRETTY).format(result);
+            actual.set(jsonify(json));
+          }
+
+          @Override
+          public void onFailure(Exception e) {
+            throw new IllegalStateException("Exception happened during execution", e);
+          }
+        });
+    return actual.get();
+  }
+
   public static DataSourceMetadataStorage getDataSourceMetadataStorage() {
     return new DataSourceMetadataStorage() {
       @Override
diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLJoinIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLJoinIT.java
index fe0904b2027..b92d5bece10 100644
--- a/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLJoinIT.java
+++ b/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLJoinIT.java
@@ -8,8 +8,13 @@
 import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_HOBBIES;
 import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_OCCUPATION;
 import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_STATE_COUNTRY;
+import static org.opensearch.sql.util.MatcherUtils.rows;
+import static org.opensearch.sql.util.MatcherUtils.schema;
+import static org.opensearch.sql.util.MatcherUtils.verifyDataRows;
+import static org.opensearch.sql.util.MatcherUtils.verifySchema;
 
 import java.io.IOException;
+import org.json.JSONObject;
 import org.junit.Ignore;
 import org.junit.Test;
 
@@ -26,908 +31,287 @@ public void init() throws IOException {
 
   @Test
   public void testJoinWithCondition() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source=%s | inner join left=a, right=b ON a.name = b.name AND a.year = 2023"
                     + " AND a.month = 4 AND b.year = 2023 AND b.month = 4 %s | fields a.name,"
                     + " a.age, a.state, a.country, b.occupation, b.country, b.salary",
                 TEST_INDEX_STATE_COUNTRY, TEST_INDEX_OCCUPATION));
-    assertEquals(
-        ""
-            + "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"name\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"age\",\n"
-            + "      \"type\": \"integer\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"state\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"country\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"occupation\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"country0\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"salary\",\n"
-            + "      \"type\": \"integer\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"Jake\",\n"
-            + "      70,\n"
-            + "      \"California\",\n"
-            + "      \"USA\",\n"
-            + "      \"Engineer\",\n"
-            + "      \"England\",\n"
-            + "      100000\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Hello\",\n"
-            + "      30,\n"
-            + "      \"New York\",\n"
-            + "      \"USA\",\n"
-            + "      \"Artist\",\n"
-            + "      \"USA\",\n"
-            + "      70000\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"John\",\n"
-            + "      25,\n"
-            + "      \"Ontario\",\n"
-            + "      \"Canada\",\n"
-            + "      \"Doctor\",\n"
-            + "      \"Canada\",\n"
-            + "      120000\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Jane\",\n"
-            + "      20,\n"
-            + "      \"Quebec\",\n"
-            + "      \"Canada\",\n"
-            + "      \"Scientist\",\n"
-            + "      \"Canada\",\n"
-            + "      90000\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"David\",\n"
-            + "      40,\n"
-            + "      \"Washington\",\n"
-            + "      \"USA\",\n"
-            + "      \"Doctor\",\n"
-            + "      \"USA\",\n"
-            + "      120000\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"David\",\n"
-            + "      40,\n"
-            + "      \"Washington\",\n"
-            + "      \"USA\",\n"
-            + "      \"Unemployed\",\n"
-            + "      \"Canada\",\n"
-            + "      0\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 6,\n"
-            + "  \"size\": 6\n"
-            + "}",
-        actual);
+    verifySchema(
+        actual,
+        schema("name", "string"),
+        schema("age", "integer"),
+        schema("state", "string"),
+        schema("country", "string"),
+        schema("occupation", "string"),
+        schema("country0", "string"),
+        schema("salary", "integer"));
+    verifyDataRows(
+        actual,
+        rows("Jake", 70, "California", "USA", "Engineer", "England", 100000),
+        rows("Hello", 30, "New York", "USA", "Artist", "USA", 70000),
+        rows("John", 25, "Ontario", "Canada", "Doctor", "Canada", 120000),
+        rows("Jane", 20, "Quebec", "Canada", "Scientist", "Canada", 90000),
+        rows("David", 40, "Washington", "USA", "Doctor", "USA", 120000),
+        rows("David", 40, "Washington", "USA", "Unemployed", "Canada", 0));
   }
 
   @Test
   public void testJoinWithTwoJoinConditions() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source = %s | inner join left=a, right=b ON a.name = b.name AND a.country ="
                     + " b.country AND a.year = 2023 AND a.month = 4 AND b.year = 2023 AND b.month ="
                     + " 4 %s | fields a.name, a.age, a.state, a.country, b.occupation, b.country,"
                     + " b.salary",
                 TEST_INDEX_STATE_COUNTRY, TEST_INDEX_OCCUPATION));
-    assertEquals(
-        ""
-            + "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"name\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"age\",\n"
-            + "      \"type\": \"integer\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"state\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"country\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"occupation\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"country0\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"salary\",\n"
-            + "      \"type\": \"integer\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"Hello\",\n"
-            + "      30,\n"
-            + "      \"New York\",\n"
-            + "      \"USA\",\n"
-            + "      \"Artist\",\n"
-            + "      \"USA\",\n"
-            + "      70000\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"John\",\n"
-            + "      25,\n"
-            + "      \"Ontario\",\n"
-            + "      \"Canada\",\n"
-            + "      \"Doctor\",\n"
-            + "      \"Canada\",\n"
-            + "      120000\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Jane\",\n"
-            + "      20,\n"
-            + "      \"Quebec\",\n"
-            + "      \"Canada\",\n"
-            + "      \"Scientist\",\n"
-            + "      \"Canada\",\n"
-            + "      90000\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"David\",\n"
-            + "      40,\n"
-            + "      \"Washington\",\n"
-            + "      \"USA\",\n"
-            + "      \"Doctor\",\n"
-            + "      \"USA\",\n"
-            + "      120000\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 4,\n"
-            + "  \"size\": 4\n"
-            + "}",
-        actual);
+    verifySchema(
+        actual,
+        schema("name", "string"),
+        schema("age", "integer"),
+        schema("state", "string"),
+        schema("country", "string"),
+        schema("occupation", "string"),
+        schema("country0", "string"),
+        schema("salary", "integer"));
+    verifyDataRows(
+        actual,
+        rows("Hello", 30, "New York", "USA", "Artist", "USA", 70000),
+        rows("John", 25, "Ontario", "Canada", "Doctor", "Canada", 120000),
+        rows("Jane", 20, "Quebec", "Canada", "Scientist", "Canada", 90000),
+        rows("David", 40, "Washington", "USA", "Doctor", "USA", 120000));
   }
 
   @Test
   public void testJoinTwoColumnsAndDisjointFilters() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source = %s | inner join left=a, right=b ON a.name = b.name AND a.country ="
                     + " b.country AND a.year = 2023 AND a.month = 4 AND b.salary > 100000 %s |"
                     + " fields a.name, a.age, a.state, a.country, b.occupation, b.country,"
                     + " b.salary",
                 TEST_INDEX_STATE_COUNTRY, TEST_INDEX_OCCUPATION));
-    assertEquals(
-        ""
-            + "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"name\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"age\",\n"
-            + "      \"type\": \"integer\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"state\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"country\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"occupation\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"country0\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"salary\",\n"
-            + "      \"type\": \"integer\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"John\",\n"
-            + "      25,\n"
-            + "      \"Ontario\",\n"
-            + "      \"Canada\",\n"
-            + "      \"Doctor\",\n"
-            + "      \"Canada\",\n"
-            + "      120000\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"David\",\n"
-            + "      40,\n"
-            + "      \"Washington\",\n"
-            + "      \"USA\",\n"
-            + "      \"Doctor\",\n"
-            + "      \"USA\",\n"
-            + "      120000\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 2,\n"
-            + "  \"size\": 2\n"
-            + "}",
-        actual);
+    verifySchema(
+        actual,
+        schema("name", "string"),
+        schema("age", "integer"),
+        schema("state", "string"),
+        schema("country", "string"),
+        schema("occupation", "string"),
+        schema("country0", "string"),
+        schema("salary", "integer"));
+    verifyDataRows(
+        actual,
+        rows("John", 25, "Ontario", "Canada", "Doctor", "Canada", 120000),
+        rows("David", 40, "Washington", "USA", "Doctor", "USA", 120000));
   }
 
   @Test
   public void testJoinThenStats() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source = %s | inner join left=a, right=b ON a.name = b.name %s | stats avg(salary)"
                     + " by span(age, 10) as age_span",
                 TEST_INDEX_STATE_COUNTRY, TEST_INDEX_OCCUPATION));
-    assertEquals(
-        ""
-            + "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"age_span\",\n"
-            + "      \"type\": \"double\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"avg(salary)\",\n"
-            + "      \"type\": \"double\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      70.0,\n"
-            + "      100000.0\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      40.0,\n"
-            + "      60000.0\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      20.0,\n"
-            + "      105000.0\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      30.0,\n"
-            + "      70000.0\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 4,\n"
-            + "  \"size\": 4\n"
-            + "}",
-        actual);
+    verifySchema(actual, schema("age_span", "double"), schema("avg(salary)", "double"));
+    verifyDataRows(
+        actual,
+        rows(70.0, 100000.0),
+        rows(40.0, 60000.0),
+        rows(20.0, 105000.0),
+        rows(30.0, 70000.0));
   }
 
   @Test
   public void testJoinThenStatsWithGroupBy() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source = %s | inner join left=a, right=b ON a.name = b.name %s | stats avg(salary)"
                     + " by span(age, 10) as age_span, b.country",
                 TEST_INDEX_STATE_COUNTRY, TEST_INDEX_OCCUPATION));
-    assertEquals(
-        ""
-            + "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"b.country\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"age_span\",\n"
-            + "      \"type\": \"double\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"avg(salary)\",\n"
-            + "      \"type\": \"double\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"Canada\",\n"
-            + "      40.0,\n"
-            + "      0.0\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Canada\",\n"
-            + "      20.0,\n"
-            + "      105000.0\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"USA\",\n"
-            + "      40.0,\n"
-            + "      120000.0\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"England\",\n"
-            + "      70.0,\n"
-            + "      100000.0\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"USA\",\n"
-            + "      30.0,\n"
-            + "      70000.0\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 5,\n"
-            + "  \"size\": 5\n"
-            + "}",
-        actual);
+    verifySchema(
+        actual,
+        schema("b.country", "string"),
+        schema("age_span", "double"),
+        schema("avg(salary)", "double"));
+    verifyDataRows(
+        actual,
+        rows("Canada", 40.0, 0.0),
+        rows("Canada", 20.0, 105000.0),
+        rows("USA", 40.0, 120000.0),
+        rows("England", 70.0, 100000.0),
+        rows("USA", 30.0, 70000.0));
   }
 
   @Test
   public void testComplexInnerJoin() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source = %s | where country = 'USA' OR country = 'England' | inner join left=a,"
                     + " right=b ON a.name = b.name %s | stats avg(salary) by span(age, 10) as"
                     + " age_span, b.country",
                 TEST_INDEX_STATE_COUNTRY, TEST_INDEX_OCCUPATION));
-    assertEquals(
-        ""
-            + "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"b.country\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"age_span\",\n"
-            + "      \"type\": \"double\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"avg(salary)\",\n"
-            + "      \"type\": \"double\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"Canada\",\n"
-            + "      40.0,\n"
-            + "      0.0\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"USA\",\n"
-            + "      40.0,\n"
-            + "      120000.0\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"England\",\n"
-            + "      70.0,\n"
-            + "      100000.0\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"USA\",\n"
-            + "      30.0,\n"
-            + "      70000.0\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 4,\n"
-            + "  \"size\": 4\n"
-            + "}",
-        actual);
+    verifySchema(
+        actual,
+        schema("b.country", "string"),
+        schema("age_span", "double"),
+        schema("avg(salary)", "double"));
+    verifyDataRows(
+        actual,
+        rows("Canada", 40.0, 0.0),
+        rows("USA", 40.0, 120000.0),
+        rows("England", 70.0, 100000.0),
+        rows("USA", 30.0, 70000.0));
   }
 
   @Test
   public void testComplexLeftJoin() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source = %s | where country = 'Canada' OR country = 'England' | left join left=a,"
                     + " right=b ON a.name = b.name %s | sort a.age | fields a.name, a.age, a.state,"
                     + " a.country, b.occupation, b.country, b.salary",
                 TEST_INDEX_STATE_COUNTRY, TEST_INDEX_OCCUPATION));
-    assertEquals(
-        ""
-            + "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"name\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"age\",\n"
-            + "      \"type\": \"integer\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"state\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"country\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"occupation\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"country0\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"salary\",\n"
-            + "      \"type\": \"integer\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"Jane\",\n"
-            + "      20,\n"
-            + "      \"Quebec\",\n"
-            + "      \"Canada\",\n"
-            + "      \"Scientist\",\n"
-            + "      \"Canada\",\n"
-            + "      90000\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"John\",\n"
-            + "      25,\n"
-            + "      \"Ontario\",\n"
-            + "      \"Canada\",\n"
-            + "      \"Doctor\",\n"
-            + "      \"Canada\",\n"
-            + "      120000\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Jim\",\n"
-            + "      27,\n"
-            + "      \"B.C\",\n"
-            + "      \"Canada\",\n"
-            + "      null,\n"
-            + "      null,\n"
-            + "      0\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Peter\",\n"
-            + "      57,\n"
-            + "      \"B.C\",\n"
-            + "      \"Canada\",\n"
-            + "      null,\n"
-            + "      null,\n"
-            + "      0\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Rick\",\n"
-            + "      70,\n"
-            + "      \"B.C\",\n"
-            + "      \"Canada\",\n"
-            + "      null,\n"
-            + "      null,\n"
-            + "      0\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 5,\n"
-            + "  \"size\": 5\n"
-            + "}",
-        actual);
+    verifySchema(
+        actual,
+        schema("name", "string"),
+        schema("age", "integer"),
+        schema("state", "string"),
+        schema("country", "string"),
+        schema("occupation", "string"),
+        schema("country0", "string"),
+        schema("salary", "integer"));
+    verifyDataRows(
+        actual,
+        rows("Jane", 20, "Quebec", "Canada", "Scientist", "Canada", 90000),
+        rows("John", 25, "Ontario", "Canada", "Doctor", "Canada", 120000),
+        rows("Jim", 27, "B.C", "Canada", null, null, 0),
+        rows("Peter", 57, "B.C", "Canada", null, null, 0),
+        rows("Rick", 70, "B.C", "Canada", null, null, 0));
   }
 
   @Test
   public void testComplexRightJoin() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source = %s | where country = 'Canada' OR country = 'England' | right join left=a,"
                     + " right=b ON a.name = b.name %s | sort a.age | fields a.name, a.age, a.state,"
                     + " a.country, b.occupation, b.country, b.salary",
                 TEST_INDEX_STATE_COUNTRY, TEST_INDEX_OCCUPATION));
-    assertEquals(
-        ""
-            + "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"name\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"age\",\n"
-            + "      \"type\": \"integer\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"state\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"country\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"occupation\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"country0\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"salary\",\n"
-            + "      \"type\": \"integer\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"Jane\",\n"
-            + "      20,\n"
-            + "      \"Quebec\",\n"
-            + "      \"Canada\",\n"
-            + "      \"Scientist\",\n"
-            + "      \"Canada\",\n"
-            + "      90000\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"John\",\n"
-            + "      25,\n"
-            + "      \"Ontario\",\n"
-            + "      \"Canada\",\n"
-            + "      \"Doctor\",\n"
-            + "      \"Canada\",\n"
-            + "      120000\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      null,\n"
-            + "      0,\n"
-            + "      null,\n"
-            + "      null,\n"
-            + "      \"Engineer\",\n"
-            + "      \"England\",\n"
-            + "      100000\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      null,\n"
-            + "      0,\n"
-            + "      null,\n"
-            + "      null,\n"
-            + "      \"Artist\",\n"
-            + "      \"USA\",\n"
-            + "      70000\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      null,\n"
-            + "      0,\n"
-            + "      null,\n"
-            + "      null,\n"
-            + "      \"Doctor\",\n"
-            + "      \"USA\",\n"
-            + "      120000\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      null,\n"
-            + "      0,\n"
-            + "      null,\n"
-            + "      null,\n"
-            + "      \"Unemployed\",\n"
-            + "      \"Canada\",\n"
-            + "      0\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 6,\n"
-            + "  \"size\": 6\n"
-            + "}",
-        actual);
+    verifySchema(
+        actual,
+        schema("name", "string"),
+        schema("age", "integer"),
+        schema("state", "string"),
+        schema("country", "string"),
+        schema("occupation", "string"),
+        schema("country0", "string"),
+        schema("salary", "integer"));
+    verifyDataRows(
+        actual,
+        rows("Jane", 20, "Quebec", "Canada", "Scientist", "Canada", 90000),
+        rows("John", 25, "Ontario", "Canada", "Doctor", "Canada", 120000),
+        rows(null, 0, null, null, "Engineer", "England", 100000),
+        rows(null, 0, null, null, "Artist", "USA", 70000),
+        rows(null, 0, null, null, "Doctor", "USA", 120000),
+        rows(null, 0, null, null, "Unemployed", "Canada", 0));
   }
 
   @Test
   public void testComplexSemiJoin() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source = %s | where country = 'Canada' OR country = 'England' | left semi join"
                     + " left=a, right=b ON a.name = b.name %s | sort a.age",
                 TEST_INDEX_STATE_COUNTRY, TEST_INDEX_OCCUPATION));
-    assertEquals(
-        ""
-            + "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"name\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"country\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"state\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"month\",\n"
-            + "      \"type\": \"integer\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"year\",\n"
-            + "      \"type\": \"integer\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"age\",\n"
-            + "      \"type\": \"integer\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"Jane\",\n"
-            + "      \"Canada\",\n"
-            + "      \"Quebec\",\n"
-            + "      4,\n"
-            + "      2023,\n"
-            + "      20\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"John\",\n"
-            + "      \"Canada\",\n"
-            + "      \"Ontario\",\n"
-            + "      4,\n"
-            + "      2023,\n"
-            + "      25\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 2,\n"
-            + "  \"size\": 2\n"
-            + "}",
-        actual);
+    verifySchema(
+        actual,
+        schema("name", "string"),
+        schema("country", "string"),
+        schema("state", "string"),
+        schema("month", "integer"),
+        schema("year", "integer"),
+        schema("age", "integer"));
+    verifyDataRows(
+        actual,
+        rows("Jane", "Canada", "Quebec", 4, 2023, 20),
+        rows("John", "Canada", "Ontario", 4, 2023, 25));
   }
 
   @Test
   public void testComplexAntiJoin() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source = %s | where country = 'Canada' OR country = 'England' | left anti join"
                     + " left=a, right=b ON a.name = b.name %s | sort a.age",
                 TEST_INDEX_STATE_COUNTRY, TEST_INDEX_OCCUPATION));
-    assertEquals(
-        ""
-            + "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"name\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"country\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"state\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"month\",\n"
-            + "      \"type\": \"integer\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"year\",\n"
-            + "      \"type\": \"integer\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"age\",\n"
-            + "      \"type\": \"integer\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"Jim\",\n"
-            + "      \"Canada\",\n"
-            + "      \"B.C\",\n"
-            + "      4,\n"
-            + "      2023,\n"
-            + "      27\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Peter\",\n"
-            + "      \"Canada\",\n"
-            + "      \"B.C\",\n"
-            + "      4,\n"
-            + "      2023,\n"
-            + "      57\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Rick\",\n"
-            + "      \"Canada\",\n"
-            + "      \"B.C\",\n"
-            + "      4,\n"
-            + "      2023,\n"
-            + "      70\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 3,\n"
-            + "  \"size\": 3\n"
-            + "}",
-        actual);
+    verifySchema(
+        actual,
+        schema("name", "string"),
+        schema("country", "string"),
+        schema("state", "string"),
+        schema("month", "integer"),
+        schema("year", "integer"),
+        schema("age", "integer"));
+    verifyDataRows(
+        actual,
+        rows("Jim", "Canada", "B.C", 4, 2023, 27),
+        rows("Peter", "Canada", "B.C", 4, 2023, 57),
+        rows("Rick", "Canada", "B.C", 4, 2023, 70));
   }
 
   @Test
   public void testComplexCrossJoin() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source = %s | where country = 'Canada' OR country = 'England' | join left=a,"
                     + " right=b %s | sort a.age | stats count()",
                 TEST_INDEX_STATE_COUNTRY, TEST_INDEX_OCCUPATION));
-    assertEquals(
-        ""
-            + "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"count()\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      30\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 1,\n"
-            + "  \"size\": 1\n"
-            + "}",
-        actual);
+    verifySchema(actual, schema("count()", "long"));
+    verifyDataRows(actual, rows(30));
   }
 
   @Test
   public void testNonEquiJoin() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source = %s | where country = 'USA' OR country = 'England' | inner join left=a,"
                     + " right=b ON age < salary %s |  where occupation = 'Doctor' OR occupation ="
                     + " 'Engineer' | fields a.name, age, state, a.country, occupation, b.country,"
                     + " salary",
                 TEST_INDEX_STATE_COUNTRY, TEST_INDEX_OCCUPATION));
-    assertEquals(
-        ""
-            + "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"name\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"age\",\n"
-            + "      \"type\": \"integer\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"state\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"country\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"occupation\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"country0\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"salary\",\n"
-            + "      \"type\": \"integer\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"Jake\",\n"
-            + "      70,\n"
-            + "      \"California\",\n"
-            + "      \"USA\",\n"
-            + "      \"Engineer\",\n"
-            + "      \"England\",\n"
-            + "      100000\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Jake\",\n"
-            + "      70,\n"
-            + "      \"California\",\n"
-            + "      \"USA\",\n"
-            + "      \"Doctor\",\n"
-            + "      \"Canada\",\n"
-            + "      120000\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Jake\",\n"
-            + "      70,\n"
-            + "      \"California\",\n"
-            + "      \"USA\",\n"
-            + "      \"Doctor\",\n"
-            + "      \"USA\",\n"
-            + "      120000\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Hello\",\n"
-            + "      30,\n"
-            + "      \"New York\",\n"
-            + "      \"USA\",\n"
-            + "      \"Engineer\",\n"
-            + "      \"England\",\n"
-            + "      100000\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Hello\",\n"
-            + "      30,\n"
-            + "      \"New York\",\n"
-            + "      \"USA\",\n"
-            + "      \"Doctor\",\n"
-            + "      \"Canada\",\n"
-            + "      120000\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Hello\",\n"
-            + "      30,\n"
-            + "      \"New York\",\n"
-            + "      \"USA\",\n"
-            + "      \"Doctor\",\n"
-            + "      \"USA\",\n"
-            + "      120000\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"David\",\n"
-            + "      40,\n"
-            + "      \"Washington\",\n"
-            + "      \"USA\",\n"
-            + "      \"Engineer\",\n"
-            + "      \"England\",\n"
-            + "      100000\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"David\",\n"
-            + "      40,\n"
-            + "      \"Washington\",\n"
-            + "      \"USA\",\n"
-            + "      \"Doctor\",\n"
-            + "      \"Canada\",\n"
-            + "      120000\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"David\",\n"
-            + "      40,\n"
-            + "      \"Washington\",\n"
-            + "      \"USA\",\n"
-            + "      \"Doctor\",\n"
-            + "      \"USA\",\n"
-            + "      120000\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 9,\n"
-            + "  \"size\": 9\n"
-            + "}",
-        actual);
+    verifySchema(
+        actual,
+        schema("name", "string"),
+        schema("age", "integer"),
+        schema("state", "string"),
+        schema("country", "string"),
+        schema("occupation", "string"),
+        schema("country0", "string"),
+        schema("salary", "integer"));
+    verifyDataRows(
+        actual,
+        rows("Jake", 70, "California", "USA", "Engineer", "England", 100000),
+        rows("Jake", 70, "California", "USA", "Doctor", "Canada", 120000),
+        rows("Jake", 70, "California", "USA", "Doctor", "USA", 120000),
+        rows("Hello", 30, "New York", "USA", "Engineer", "England", 100000),
+        rows("Hello", 30, "New York", "USA", "Doctor", "Canada", 120000),
+        rows("Hello", 30, "New York", "USA", "Doctor", "USA", 120000),
+        rows("David", 40, "Washington", "USA", "Engineer", "England", 100000),
+        rows("David", 40, "Washington", "USA", "Doctor", "Canada", 120000),
+        rows("David", 40, "Washington", "USA", "Doctor", "USA", 120000));
   }
 
   @Test
@@ -949,8 +333,8 @@ public void testCrossJoinWithJoinCriteriaFallbackToInnerJoin() {
 
   @Ignore // TODO seems a calcite bug
   public void testMultipleJoins() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 """
                    source = %s
@@ -985,13 +369,12 @@ public void testMultipleJoins() {
                 TEST_INDEX_HOBBIES,
                 TEST_INDEX_OCCUPATION,
                 TEST_INDEX_STATE_COUNTRY));
-    assertEquals("30", actual);
   }
 
   @Test
   public void testMultipleJoinsWithoutTableAliases() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source = %s | JOIN ON %s.name = %s.name %s | JOIN ON %s.name = %s.name %s | fields"
                     + " %s.name, %s.name, %s.name",
@@ -1005,128 +388,42 @@ public void testMultipleJoinsWithoutTableAliases() {
                 TEST_INDEX_STATE_COUNTRY,
                 TEST_INDEX_OCCUPATION,
                 TEST_INDEX_HOBBIES));
-    assertEquals(
-        ""
-            + "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"name\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"name0\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"name1\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"David\",\n"
-            + "      \"David\",\n"
-            + "      \"David\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"David\",\n"
-            + "      \"David\",\n"
-            + "      \"David\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Hello\",\n"
-            + "      \"Hello\",\n"
-            + "      \"Hello\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Jake\",\n"
-            + "      \"Jake\",\n"
-            + "      \"Jake\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Jane\",\n"
-            + "      \"Jane\",\n"
-            + "      \"Jane\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"John\",\n"
-            + "      \"John\",\n"
-            + "      \"John\"\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 6,\n"
-            + "  \"size\": 6\n"
-            + "}",
-        actual);
+    verifySchema(
+        actual, schema("name", "string"), schema("name0", "string"), schema("name1", "string"));
+    verifyDataRows(
+        actual,
+        rows("David", "David", "David"),
+        rows("David", "David", "David"),
+        rows("Hello", "Hello", "Hello"),
+        rows("Jake", "Jake", "Jake"),
+        rows("Jane", "Jane", "Jane"),
+        rows("John", "John", "John"));
   }
 
   @Test
   public void testMultipleJoinsWithPartTableAliases() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source = %s | JOIN left = t1 right = t2 ON t1.name = t2.name %s | JOIN right = t3"
                     + " ON t1.name = t3.name %s | fields t1.name, t2.name, t3.name",
                 TEST_INDEX_STATE_COUNTRY, TEST_INDEX_OCCUPATION, TEST_INDEX_HOBBIES));
-    assertEquals(
-        ""
-            + "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"name\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"name0\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"name1\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"David\",\n"
-            + "      \"David\",\n"
-            + "      \"David\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"David\",\n"
-            + "      \"David\",\n"
-            + "      \"David\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Hello\",\n"
-            + "      \"Hello\",\n"
-            + "      \"Hello\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Jake\",\n"
-            + "      \"Jake\",\n"
-            + "      \"Jake\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Jane\",\n"
-            + "      \"Jane\",\n"
-            + "      \"Jane\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"John\",\n"
-            + "      \"John\",\n"
-            + "      \"John\"\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 6,\n"
-            + "  \"size\": 6\n"
-            + "}",
-        actual);
+    verifySchema(
+        actual, schema("name", "string"), schema("name0", "string"), schema("name1", "string"));
+    verifyDataRows(
+        actual,
+        rows("David", "David", "David"),
+        rows("David", "David", "David"),
+        rows("Hello", "Hello", "Hello"),
+        rows("Jake", "Jake", "Jake"),
+        rows("Jane", "Jane", "Jane"),
+        rows("John", "John", "John"));
   }
 
   @Test
   public void testMultipleJoinsWithSelfJoin1() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source = %s | JOIN left = t1 right = t2 ON t1.name = t2.name %s | JOIN right = t3"
                     + " ON t1.name = t3.name %s | JOIN right = t4 ON t1.name = t4.name %s | fields"
@@ -1135,75 +432,26 @@ public void testMultipleJoinsWithSelfJoin1() {
                 TEST_INDEX_OCCUPATION,
                 TEST_INDEX_HOBBIES,
                 TEST_INDEX_STATE_COUNTRY));
-    assertEquals(
-        ""
-            + "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"name\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"name0\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"name1\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"name2\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"David\",\n"
-            + "      \"David\",\n"
-            + "      \"David\",\n"
-            + "      \"David\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"David\",\n"
-            + "      \"David\",\n"
-            + "      \"David\",\n"
-            + "      \"David\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Hello\",\n"
-            + "      \"Hello\",\n"
-            + "      \"Hello\",\n"
-            + "      \"Hello\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Jake\",\n"
-            + "      \"Jake\",\n"
-            + "      \"Jake\",\n"
-            + "      \"Jake\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Jane\",\n"
-            + "      \"Jane\",\n"
-            + "      \"Jane\",\n"
-            + "      \"Jane\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"John\",\n"
-            + "      \"John\",\n"
-            + "      \"John\",\n"
-            + "      \"John\"\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 6,\n"
-            + "  \"size\": 6\n"
-            + "}",
-        actual);
+    verifySchema(
+        actual,
+        schema("name", "string"),
+        schema("name0", "string"),
+        schema("name1", "string"),
+        schema("name2", "string"));
+    verifyDataRows(
+        actual,
+        rows("David", "David", "David", "David"),
+        rows("David", "David", "David", "David"),
+        rows("Hello", "Hello", "Hello", "Hello"),
+        rows("Jake", "Jake", "Jake", "Jake"),
+        rows("Jane", "Jane", "Jane", "Jane"),
+        rows("John", "John", "John", "John"));
   }
 
-  @Ignore // TODO subquery not support
+  @Ignore // TODO table subquery not support
   public void testMultipleJoinsWithSelfJoin2() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source = %s | JOIN left = t1 right = t2 ON t1.name = t2.name %s | JOIN right = t3"
                     + " ON t1.name = t3.name %s | JOIN ON t1.name = t4.name [ source = %s ] as t4 |"
@@ -1212,7 +460,6 @@ public void testMultipleJoinsWithSelfJoin2() {
                 TEST_INDEX_OCCUPATION,
                 TEST_INDEX_HOBBIES,
                 TEST_INDEX_STATE_COUNTRY));
-    assertEquals("", actual);
   }
 
   @Test
@@ -1252,7 +499,7 @@ public void testCheckAccessTheReferenceByAliases1() {
     assertEquals(res4, res5);
   }
 
-  @Ignore // TODO subquery not support
+  @Ignore // TODO table subquery not support
   public void testCheckAccessTheReferenceByAliases2() {
     String res1 =
         execute(
diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLSortIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLSortIT.java
index 545bed6e1a8..eb582f10882 100644
--- a/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLSortIT.java
+++ b/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLSortIT.java
@@ -6,8 +6,13 @@
 package org.opensearch.sql.calcite.standalone;
 
 import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_BANK;
+import static org.opensearch.sql.util.MatcherUtils.rows;
+import static org.opensearch.sql.util.MatcherUtils.schema;
+import static org.opensearch.sql.util.MatcherUtils.verifyDataRows;
+import static org.opensearch.sql.util.MatcherUtils.verifySchema;
 
 import java.io.IOException;
+import org.json.JSONObject;
 import org.junit.jupiter.api.Test;
 
 public class CalcitePPLSortIT extends CalcitePPLIntegTestCase {
@@ -21,530 +26,169 @@ public void init() throws IOException {
 
   @Test
   public void testFieldsAndSort1() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source=%s | fields + firstname, gender, account_number | sort - account_number",
                 TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"firstname\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"gender\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"account_number\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"Dillard\",\n"
-            + "      \"F\",\n"
-            + "      32\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Virginia\",\n"
-            + "      \"F\",\n"
-            + "      25\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Elinor\",\n"
-            + "      \"M\",\n"
-            + "      20\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Dale\",\n"
-            + "      \"M\",\n"
-            + "      18\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Nanette\",\n"
-            + "      \"F\",\n"
-            + "      13\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Hattie\",\n"
-            + "      \"M\",\n"
-            + "      6\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Amber JOHnny\",\n"
-            + "      \"M\",\n"
-            + "      1\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 7,\n"
-            + "  \"size\": 7\n"
-            + "}",
-        actual);
+    verifySchema(
+        actual,
+        schema("firstname", "string"),
+        schema("gender", "string"),
+        schema("account_number", "long"));
+    verifyDataRows(
+        actual,
+        rows("Dillard", "F", 32),
+        rows("Virginia", "F", 25),
+        rows("Elinor", "M", 20),
+        rows("Dale", "M", 18),
+        rows("Nanette", "F", 13),
+        rows("Hattie", "M", 6),
+        rows("Amber JOHnny", "M", 1));
   }
 
   @Test
   public void testFieldsAndSort2() {
-    String actual = execute(String.format("source=%s | fields age | sort - age", TEST_INDEX_BANK));
-    String expected =
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"age\",\n"
-            + "      \"type\": \"integer\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      39\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      36\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      36\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      34\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      33\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      32\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      28\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 7,\n"
-            + "  \"size\": 7\n"
-            + "}";
-    assertEquals(expected, actual);
+    JSONObject actual =
+        executeQuery(String.format("source=%s | fields age | sort - age", TEST_INDEX_BANK));
+    verifySchema(actual, schema("age", "integer"));
+    verifyDataRows(actual, rows(39), rows(36), rows(36), rows(34), rows(33), rows(32), rows(28));
   }
 
   @Test
   public void testFieldsAndSortTwoFields() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source=%s | fields + firstname, gender, account_number | sort + gender, -"
                     + " account_number",
                 TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"firstname\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"gender\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"account_number\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"Dillard\",\n"
-            + "      \"F\",\n"
-            + "      32\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Virginia\",\n"
-            + "      \"F\",\n"
-            + "      25\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Nanette\",\n"
-            + "      \"F\",\n"
-            + "      13\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Elinor\",\n"
-            + "      \"M\",\n"
-            + "      20\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Dale\",\n"
-            + "      \"M\",\n"
-            + "      18\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Hattie\",\n"
-            + "      \"M\",\n"
-            + "      6\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Amber JOHnny\",\n"
-            + "      \"M\",\n"
-            + "      1\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 7,\n"
-            + "  \"size\": 7\n"
-            + "}",
-        actual);
+    verifySchema(
+        actual,
+        schema("firstname", "string"),
+        schema("gender", "string"),
+        schema("account_number", "long"));
+    verifyDataRows(
+        actual,
+        rows("Dillard", "F", 32),
+        rows("Virginia", "F", 25),
+        rows("Nanette", "F", 13),
+        rows("Elinor", "M", 20),
+        rows("Dale", "M", 18),
+        rows("Hattie", "M", 6),
+        rows("Amber JOHnny", "M", 1));
   }
 
   @Test
   public void testFieldsAndSortWithDescAndLimit() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source=%s | fields + firstname, gender, account_number | sort + gender, -"
                     + " account_number | head 5",
                 TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"firstname\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"gender\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"account_number\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"Dillard\",\n"
-            + "      \"F\",\n"
-            + "      32\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Virginia\",\n"
-            + "      \"F\",\n"
-            + "      25\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Nanette\",\n"
-            + "      \"F\",\n"
-            + "      13\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Elinor\",\n"
-            + "      \"M\",\n"
-            + "      20\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Dale\",\n"
-            + "      \"M\",\n"
-            + "      18\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 5,\n"
-            + "  \"size\": 5\n"
-            + "}",
-        actual);
+    verifySchema(
+        actual,
+        schema("firstname", "string"),
+        schema("gender", "string"),
+        schema("account_number", "long"));
+    verifyDataRows(
+        actual,
+        rows("Dillard", "F", 32),
+        rows("Virginia", "F", 25),
+        rows("Nanette", "F", 13),
+        rows("Elinor", "M", 20),
+        rows("Dale", "M", 18));
   }
 
   @Test
   public void testSortAccountAndFieldsAccount() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source=%s | sort - account_number | fields account_number", TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"account_number\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      32\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      25\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      20\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      18\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      13\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      6\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      1\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 7,\n"
-            + "  \"size\": 7\n"
-            + "}",
-        actual);
+    verifySchema(actual, schema("account_number", "long"));
+    verifyDataRows(actual, rows(32), rows(25), rows(20), rows(18), rows(13), rows(6), rows(1));
   }
 
   @Test
   public void testSortAccountAndFieldsNameAccount() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source=%s | sort - account_number | fields firstname, account_number",
                 TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"firstname\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"account_number\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"Dillard\",\n"
-            + "      32\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Virginia\",\n"
-            + "      25\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Elinor\",\n"
-            + "      20\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Dale\",\n"
-            + "      18\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Nanette\",\n"
-            + "      13\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Hattie\",\n"
-            + "      6\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Amber JOHnny\",\n"
-            + "      1\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 7,\n"
-            + "  \"size\": 7\n"
-            + "}",
-        actual);
+    verifySchema(actual, schema("firstname", "string"), schema("account_number", "long"));
+    verifyDataRows(
+        actual,
+        rows("Dillard", 32),
+        rows("Virginia", 25),
+        rows("Elinor", 20),
+        rows("Dale", 18),
+        rows("Nanette", 13),
+        rows("Hattie", 6),
+        rows("Amber JOHnny", 1));
   }
 
   @Test
   public void testSortAccountAndFieldsAccountName() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source=%s | sort - account_number | fields account_number, firstname",
                 TEST_INDEX_BANK));
-    assertEquals(
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"account_number\",\n"
-            + "      \"type\": \"long\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"firstname\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      32,\n"
-            + "      \"Dillard\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      25,\n"
-            + "      \"Virginia\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      20,\n"
-            + "      \"Elinor\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      18,\n"
-            + "      \"Dale\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      13,\n"
-            + "      \"Nanette\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      6,\n"
-            + "      \"Hattie\"\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      1,\n"
-            + "      \"Amber JOHnny\"\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 7,\n"
-            + "  \"size\": 7\n"
-            + "}",
-        actual);
+    verifySchema(actual, schema("account_number", "long"), schema("firstname", "string"));
+    verifyDataRows(
+        actual,
+        rows(32, "Dillard"),
+        rows(25, "Virginia"),
+        rows(20, "Elinor"),
+        rows(18, "Dale"),
+        rows(13, "Nanette"),
+        rows(6, "Hattie"),
+        rows(1, "Amber JOHnny"));
   }
 
   @Test
   public void testSortAgeAndFieldsAge() {
-    String actual = execute(String.format("source=%s | sort - age | fields age", TEST_INDEX_BANK));
-    String expected =
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"age\",\n"
-            + "      \"type\": \"integer\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      39\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      36\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      36\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      34\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      33\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      32\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      28\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 7,\n"
-            + "  \"size\": 7\n"
-            + "}";
-    assertEquals(expected, actual);
+    JSONObject actual =
+        executeQuery(String.format("source=%s | sort - age | fields age", TEST_INDEX_BANK));
+    verifySchema(actual, schema("age", "integer"));
+    verifyDataRows(actual, rows(39), rows(36), rows(36), rows(34), rows(33), rows(32), rows(28));
   }
 
   @Test
   public void testSortAgeAndFieldsNameAge() {
-    String actual =
-        execute(String.format("source=%s | sort - age | fields firstname, age", TEST_INDEX_BANK));
-    String expected =
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"firstname\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"age\",\n"
-            + "      \"type\": \"integer\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"Virginia\",\n"
-            + "      39\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Hattie\",\n"
-            + "      36\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Elinor\",\n"
-            + "      36\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Dillard\",\n"
-            + "      34\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Dale\",\n"
-            + "      33\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Amber JOHnny\",\n"
-            + "      32\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Nanette\",\n"
-            + "      28\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 7,\n"
-            + "  \"size\": 7\n"
-            + "}";
-    assertEquals(expected, actual);
+    JSONObject actual =
+        executeQuery(
+            String.format("source=%s | sort - age | fields firstname, age", TEST_INDEX_BANK));
+    verifySchema(actual, schema("firstname", "string"), schema("age", "integer"));
+    verifyDataRows(
+        actual,
+        rows("Virginia", 39),
+        rows("Hattie", 36),
+        rows("Elinor", 36),
+        rows("Dillard", 34),
+        rows("Dale", 33),
+        rows("Amber JOHnny", 32),
+        rows("Nanette", 28));
   }
 
   @Test
   public void testSortAgeNameAndFieldsNameAge() {
-    String actual =
-        execute(
+    JSONObject actual =
+        executeQuery(
             String.format(
                 "source=%s | sort - age, - firstname | fields firstname, age", TEST_INDEX_BANK));
-    String expected =
-        "{\n"
-            + "  \"schema\": [\n"
-            + "    {\n"
-            + "      \"name\": \"firstname\",\n"
-            + "      \"type\": \"string\"\n"
-            + "    },\n"
-            + "    {\n"
-            + "      \"name\": \"age\",\n"
-            + "      \"type\": \"integer\"\n"
-            + "    }\n"
-            + "  ],\n"
-            + "  \"datarows\": [\n"
-            + "    [\n"
-            + "      \"Virginia\",\n"
-            + "      39\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Hattie\",\n"
-            + "      36\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Elinor\",\n"
-            + "      36\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Dillard\",\n"
-            + "      34\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Dale\",\n"
-            + "      33\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Amber JOHnny\",\n"
-            + "      32\n"
-            + "    ],\n"
-            + "    [\n"
-            + "      \"Nanette\",\n"
-            + "      28\n"
-            + "    ]\n"
-            + "  ],\n"
-            + "  \"total\": 7,\n"
-            + "  \"size\": 7\n"
-            + "}";
-    assertEquals(expected, actual);
+    verifySchema(actual, schema("firstname", "string"), schema("age", "integer"));
+    verifyDataRows(
+        actual,
+        rows("Virginia", 39),
+        rows("Hattie", 36),
+        rows("Elinor", 36),
+        rows("Dillard", 34),
+        rows("Dale", 33),
+        rows("Amber JOHnny", 32),
+        rows("Nanette", 28));
   }
 }
diff --git a/integ-test/src/test/java/org/opensearch/sql/legacy/SQLIntegTestCase.java b/integ-test/src/test/java/org/opensearch/sql/legacy/SQLIntegTestCase.java
index 13fbcf678b6..f0a82ab5a36 100644
--- a/integ-test/src/test/java/org/opensearch/sql/legacy/SQLIntegTestCase.java
+++ b/integ-test/src/test/java/org/opensearch/sql/legacy/SQLIntegTestCase.java
@@ -37,6 +37,8 @@
 import static org.opensearch.sql.legacy.TestUtils.getStringIndexMapping;
 import static org.opensearch.sql.legacy.TestUtils.getUnexpandedObjectIndexMapping;
 import static org.opensearch.sql.legacy.TestUtils.getWeblogsIndexMapping;
+import static org.opensearch.sql.legacy.TestUtils.getWorkInformationIndexMapping;
+import static org.opensearch.sql.legacy.TestUtils.getWorkerIndexMapping;
 import static org.opensearch.sql.legacy.TestUtils.isIndexExist;
 import static org.opensearch.sql.legacy.TestUtils.loadDataByRestClient;
 import static org.opensearch.sql.legacy.plugin.RestSqlAction.CURSOR_CLOSE_ENDPOINT;
@@ -763,7 +765,17 @@ public enum Index {
         TestsConstants.TEST_INDEX_HOBBIES,
         "hobbies",
         getHobbiesIndexMapping(),
-        "src/test/resources/hobbies.json");
+        "src/test/resources/hobbies.json"),
+    WORKER(
+        TestsConstants.TEST_INDEX_WORKER,
+        "worker",
+        getWorkerIndexMapping(),
+        "src/test/resources/worker.json"),
+    WORK_INFORMATION(
+        TestsConstants.TEST_INDEX_WORK_INFORMATION,
+        "work_information",
+        getWorkInformationIndexMapping(),
+        "src/test/resources/work_information.json");
 
     private final String name;
     private final String type;
diff --git a/integ-test/src/test/java/org/opensearch/sql/legacy/TestUtils.java b/integ-test/src/test/java/org/opensearch/sql/legacy/TestUtils.java
index 7f056e6fd0a..3fd0de2e69c 100644
--- a/integ-test/src/test/java/org/opensearch/sql/legacy/TestUtils.java
+++ b/integ-test/src/test/java/org/opensearch/sql/legacy/TestUtils.java
@@ -265,6 +265,16 @@ public static String getHobbiesIndexMapping() {
     return getMappingFile(mappingFile);
   }
 
+  public static String getWorkerIndexMapping() {
+    String mappingFile = "worker_index_mapping.json";
+    return getMappingFile(mappingFile);
+  }
+
+  public static String getWorkInformationIndexMapping() {
+    String mappingFile = "work_information_index_mapping.json";
+    return getMappingFile(mappingFile);
+  }
+
   public static void loadBulk(Client client, String jsonPath, String defaultIndex)
       throws Exception {
     System.out.println(String.format("Loading file %s into opensearch cluster", jsonPath));
diff --git a/integ-test/src/test/java/org/opensearch/sql/legacy/TestsConstants.java b/integ-test/src/test/java/org/opensearch/sql/legacy/TestsConstants.java
index cb99849db85..653c525d317 100644
--- a/integ-test/src/test/java/org/opensearch/sql/legacy/TestsConstants.java
+++ b/integ-test/src/test/java/org/opensearch/sql/legacy/TestsConstants.java
@@ -62,6 +62,8 @@ public class TestsConstants {
   public static final String TEST_INDEX_STATE_COUNTRY = TEST_INDEX + "_state_country";
   public static final String TEST_INDEX_OCCUPATION = TEST_INDEX + "_occupation";
   public static final String TEST_INDEX_HOBBIES = TEST_INDEX + "_hobbies";
+  public static final String TEST_INDEX_WORKER = TEST_INDEX + "_worker";
+  public static final String TEST_INDEX_WORK_INFORMATION = TEST_INDEX + "_work_information";
 
   public static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
   public static final String TS_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS";
diff --git a/integ-test/src/test/java/org/opensearch/sql/ppl/PPLIntegTestCase.java b/integ-test/src/test/java/org/opensearch/sql/ppl/PPLIntegTestCase.java
index bc1863949aa..90a058e44ce 100644
--- a/integ-test/src/test/java/org/opensearch/sql/ppl/PPLIntegTestCase.java
+++ b/integ-test/src/test/java/org/opensearch/sql/ppl/PPLIntegTestCase.java
@@ -104,7 +104,7 @@ public String toString() {
     }
   }
 
-  private JSONObject jsonify(String text) {
+  protected JSONObject jsonify(String text) {
     try {
       return new JSONObject(text);
     } catch (JSONException e) {
diff --git a/integ-test/src/test/resources/indexDefinitions/work_information_index_mapping.json b/integ-test/src/test/resources/indexDefinitions/work_information_index_mapping.json
new file mode 100644
index 00000000000..080d9d32401
--- /dev/null
+++ b/integ-test/src/test/resources/indexDefinitions/work_information_index_mapping.json
@@ -0,0 +1,18 @@
+{
+  "mappings": {
+    "properties": {
+      "uid": {
+        "type": "integer"
+      },
+      "name": {
+        "type": "keyword"
+      },
+      "department": {
+        "type": "text"
+      },
+      "occupation": {
+        "type": "text"
+      }
+    }
+  }
+}
diff --git a/integ-test/src/test/resources/indexDefinitions/worker_index_mapping.json b/integ-test/src/test/resources/indexDefinitions/worker_index_mapping.json
new file mode 100644
index 00000000000..a506e2ca6da
--- /dev/null
+++ b/integ-test/src/test/resources/indexDefinitions/worker_index_mapping.json
@@ -0,0 +1,21 @@
+{
+  "mappings": {
+    "properties": {
+      "id": {
+        "type": "integer"
+      },
+      "name": {
+        "type": "keyword"
+      },
+      "occupation": {
+        "type": "text"
+      },
+      "country": {
+        "type": "text"
+      },
+      "salary": {
+        "type": "integer"
+      }
+    }
+  }
+}
diff --git a/integ-test/src/test/resources/work_information.json b/integ-test/src/test/resources/work_information.json
new file mode 100644
index 00000000000..372521bfa72
--- /dev/null
+++ b/integ-test/src/test/resources/work_information.json
@@ -0,0 +1,10 @@
+{"index":{"_id":"1"}}
+{"uid":1000,"name":"Jake","department":"IT","occupation":"Engineer"}
+{"index":{"_id":"2"}}
+{"uid":1002,"name":"John","department":"DATA","occupation":"Scientist"}
+{"index":{"_id":"3"}}
+{"uid":1003,"name":"David","department":"HR","occupation":"Doctor"}
+{"index":{"_id":"4"}}
+{"uid":1005,"name":"Jane","department":"DATA","occupation":"Engineer"}
+{"index":{"_id":"5"}}
+{"uid":1006,"name":"Tom","department":"SALES","occupation":"Artist"}
diff --git a/integ-test/src/test/resources/worker.json b/integ-test/src/test/resources/worker.json
new file mode 100644
index 00000000000..222decb6e6f
--- /dev/null
+++ b/integ-test/src/test/resources/worker.json
@@ -0,0 +1,14 @@
+{"index":{"_id":"1"}}
+{"id":1000,"name":"Jake","occupation":"Engineer","country":"England","salary":100000}
+{"index":{"_id":"2"}}
+{"id":1001,"name":"Hello","occupation":"Artist","country":"USA","salary":70000}
+{"index":{"_id":"3"}}
+{"id":1002,"name":"John","occupation":"Doctor","country":"Canada","salary":120000}
+{"index":{"_id":"4"}}
+{"id":1003,"name":"David","occupation":"Doctor","salary":120000}
+{"index":{"_id":"5"}}
+{"id":1004,"name":"David","country":"Canada","salary":0}
+{"index":{"_id":"6"}}
+{"id":1005,"name":"Jane","occupation":"Scientist","country":"Canada","salary":90000}
+{"index":{"_id":"7"}}
+{"id":1006,"name":"Tommy","occupation":"Teacher","country":"USA","salary":30000}
diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/OpenSearchIndexEnumerator.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/OpenSearchIndexEnumerator.java
index 6e778422db2..8ef87e227c4 100644
--- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/OpenSearchIndexEnumerator.java
+++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/OpenSearchIndexEnumerator.java
@@ -11,6 +11,7 @@
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
 import org.apache.calcite.linq4j.Enumerator;
+import org.opensearch.sql.data.model.ExprNullValue;
 import org.opensearch.sql.data.model.ExprValue;
 import org.opensearch.sql.opensearch.client.OpenSearchClient;
 import org.opensearch.sql.opensearch.request.OpenSearchRequest;
@@ -65,9 +66,14 @@ public Object current() {
      * See {@link PhysTypeImpl}
      */
     if (fields.size() == 1) {
-      return current.tupleValue().get(fields.getFirst()).valueForCalcite();
+      return current
+          .tupleValue()
+          .getOrDefault(fields.getFirst(), ExprNullValue.of())
+          .valueForCalcite();
     }
-    return fields.stream().map(k -> current.tupleValue().get(k).valueForCalcite()).toArray();
+    return fields.stream()
+        .map(k -> current.tupleValue().getOrDefault(k, ExprNullValue.of()).valueForCalcite())
+        .toArray();
   }
 
   @Override
diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/util/JdbcOpenSearchDataTypeConvertor.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/util/JdbcOpenSearchDataTypeConvertor.java
index a0431535eac..83468ef1e4b 100644
--- a/opensearch/src/main/java/org/opensearch/sql/opensearch/util/JdbcOpenSearchDataTypeConvertor.java
+++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/util/JdbcOpenSearchDataTypeConvertor.java
@@ -13,6 +13,7 @@
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.opensearch.sql.data.model.ExprDateValue;
+import org.opensearch.sql.data.model.ExprNullValue;
 import org.opensearch.sql.data.model.ExprTimeValue;
 import org.opensearch.sql.data.model.ExprTimestampValue;
 import org.opensearch.sql.data.model.ExprValue;
@@ -79,11 +80,14 @@ public static ExprValue getExprValueFromSqlType(
         value = rs.getFloat(i);
         break;
       case Types.DATE:
-        return new ExprDateValue(rs.getString(i));
+        value = rs.getString(i);
+        return value == null ? ExprNullValue.of() : new ExprDateValue((String) value);
       case Types.TIME:
-        return new ExprTimeValue(rs.getString(i));
+        value = rs.getString(i);
+        return value == null ? ExprNullValue.of() : new ExprTimeValue((String) value);
       case Types.TIMESTAMP:
-        return new ExprTimestampValue(rs.getString(i));
+        value = rs.getString(i);
+        return value == null ? ExprNullValue.of() : new ExprTimestampValue((String) value);
       case Types.BOOLEAN:
         value = rs.getBoolean(i);
         break;
@@ -94,6 +98,6 @@ public static ExprValue getExprValueFromSqlType(
             sqlType,
             value.getClass().getTypeName());
     }
-    return ExprValueUtils.fromObjectValue(value);
+    return value == null ? ExprNullValue.of() : ExprValueUtils.fromObjectValue(value);
   }
 }
diff --git a/ppl/src/main/antlr/OpenSearchPPLParser.g4 b/ppl/src/main/antlr/OpenSearchPPLParser.g4
index 24dcaef15a2..17801cc7ead 100644
--- a/ppl/src/main/antlr/OpenSearchPPLParser.g4
+++ b/ppl/src/main/antlr/OpenSearchPPLParser.g4
@@ -25,6 +25,10 @@ queryStatement
    : pplCommands (PIPE commands)*
    ;
 
+subSearch
+   : searchCommand (PIPE commands)*
+   ;
+
 // commands
 pplCommands
    : searchCommand
@@ -334,6 +338,12 @@ logicalExpression
 comparisonExpression
    : left = valueExpression comparisonOperator right = valueExpression  # compareExpr
    | valueExpression IN valueList                                       # inExpr
+   | valueExpressionList NOT? IN LT_SQR_PRTHS subSearch RT_SQR_PRTHS    # inSubqueryExpr
+   ;
+
+valueExpressionList
+   : valueExpression
+   | LT_PRTHS valueExpression (COMMA valueExpression)* RT_PRTHS
    ;
 
 valueExpression
@@ -996,4 +1006,13 @@ keywordsCanBeId
    | SPARKLINE
    | C
    | DC
+   // JOIN TYPE
+   | OUTER
+   | INNER
+   | CROSS
+   | LEFT
+   | RIGHT
+   | FULL
+   | SEMI
+   | ANTI
    ;
diff --git a/ppl/src/main/java/org/opensearch/sql/ppl/parser/AstBuilder.java b/ppl/src/main/java/org/opensearch/sql/ppl/parser/AstBuilder.java
index ffae07bf63f..37f6696e0fc 100644
--- a/ppl/src/main/java/org/opensearch/sql/ppl/parser/AstBuilder.java
+++ b/ppl/src/main/java/org/opensearch/sql/ppl/parser/AstBuilder.java
@@ -101,6 +101,12 @@ public UnresolvedPlan visitQueryStatement(OpenSearchPPLParser.QueryStatementCont
     return ctx.commands().stream().map(this::visit).reduce(pplCommand, (r, e) -> e.attach(r));
   }
 
+  @Override
+  public UnresolvedPlan visitSubSearch(OpenSearchPPLParser.SubSearchContext ctx) {
+    UnresolvedPlan searchCommand = visit(ctx.searchCommand());
+    return ctx.commands().stream().map(this::visit).reduce(searchCommand, (r, e) -> e.attach(r));
+  }
+
   /** Search command. */
   @Override
   public UnresolvedPlan visitSearchFrom(SearchFromContext ctx) {
diff --git a/ppl/src/main/java/org/opensearch/sql/ppl/parser/AstExpressionBuilder.java b/ppl/src/main/java/org/opensearch/sql/ppl/parser/AstExpressionBuilder.java
index 697da32523f..562eef02d20 100644
--- a/ppl/src/main/java/org/opensearch/sql/ppl/parser/AstExpressionBuilder.java
+++ b/ppl/src/main/java/org/opensearch/sql/ppl/parser/AstExpressionBuilder.java
@@ -53,6 +53,7 @@
 import org.antlr.v4.runtime.RuleContext;
 import org.opensearch.sql.ast.dsl.AstDSL;
 import org.opensearch.sql.ast.expression.*;
+import org.opensearch.sql.ast.expression.subquery.InSubquery;
 import org.opensearch.sql.ast.tree.Trendline;
 import org.opensearch.sql.common.antlr.SyntaxCheckException;
 import org.opensearch.sql.common.utils.StringUtils;
@@ -72,7 +73,7 @@ public class AstExpressionBuilder extends OpenSearchPPLParserBaseVisitor<Unresol
           .put("isnotnull", IS_NOT_NULL.getName().getFunctionName())
           .build();
 
-  private AstBuilder astBuilder;
+  private final AstBuilder astBuilder;
 
   public AstExpressionBuilder(AstBuilder astBuilder) {
     this.astBuilder = astBuilder;
@@ -401,6 +402,29 @@ public UnresolvedExpression visitSpanClause(SpanClauseContext ctx) {
     return new Span(visit(ctx.fieldExpression()), visit(ctx.value), SpanUnit.of(unit));
   }
 
+  @Override
+  public UnresolvedExpression visitLeftHint(OpenSearchPPLParser.LeftHintContext ctx) {
+    return new EqualTo(
+        new Literal(ctx.leftHintKey.getText(), DataType.STRING), visit(ctx.leftHintValue));
+  }
+
+  @Override
+  public UnresolvedExpression visitRightHint(OpenSearchPPLParser.RightHintContext ctx) {
+    return new EqualTo(
+        new Literal(ctx.rightHintKey.getText(), DataType.STRING), visit(ctx.rightHintValue));
+  }
+
+  @Override
+  public UnresolvedExpression visitInSubqueryExpr(OpenSearchPPLParser.InSubqueryExprContext ctx) {
+    UnresolvedExpression expr =
+        new InSubquery(
+            ctx.valueExpressionList().valueExpression().stream()
+                .map(this::visit)
+                .collect(Collectors.toList()),
+            astBuilder.visitSubSearch(ctx.subSearch()));
+    return ctx.NOT() != null ? new Not(expr) : expr;
+  }
+
   private QualifiedName visitIdentifiers(List<? extends ParserRuleContext> ctx) {
     return new QualifiedName(
         ctx.stream()
diff --git a/ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLInSubqueryTest.java b/ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLInSubqueryTest.java
new file mode 100644
index 00000000000..738d5ba8257
--- /dev/null
+++ b/ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLInSubqueryTest.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.opensearch.sql.ppl.calcite;
+
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.test.CalciteAssert;
+import org.junit.Test;
+
+public class CalcitePPLInSubqueryTest extends CalcitePPLAbstractTest {
+
+  public CalcitePPLInSubqueryTest() {
+    super(CalciteAssert.SchemaSpec.SCOTT_WITH_TEMPORAL);
+  }
+
+  @Test
+  public void testInSubquery() {
+    String ppl =
+        """
+        source=EMP | where DEPTNO in [ source=DEPT | fields DEPTNO ]
+        | sort - EMPNO | fields EMPNO, ENAME
+        """;
+    RelNode root = getRelNode(ppl);
+    String expectedLogical =
+        ""
+            + "LogicalProject(EMPNO=[$0], ENAME=[$1])\n"
+            + "  LogicalSort(sort0=[$0], dir0=[DESC])\n"
+            + "    LogicalFilter(condition=[IN($7, {\n"
+            + "LogicalProject(DEPTNO=[$0])\n"
+            + "  LogicalTableScan(table=[[scott, DEPT]])\n"
+            + "})])\n"
+            + "      LogicalTableScan(table=[[scott, EMP]])\n";
+    verifyLogical(root, expectedLogical);
+
+    String expectedSparkSql =
+        ""
+            + "SELECT `EMPNO`, `ENAME`\n"
+            + "FROM `scott`.`EMP`\n"
+            + "WHERE `DEPTNO` IN (SELECT `DEPTNO`\n"
+            + "FROM `scott`.`DEPT`)\n"
+            + "ORDER BY `EMPNO` DESC NULLS FIRST";
+    verifyPPLToSparkSQL(root, expectedSparkSql);
+  }
+
+  @Test
+  public void testSelfInSubquery() {
+    String ppl =
+        """
+        source=EMP | where MGR in [
+            source=EMP
+            | where DEPTNO = 10
+            | fields MGR
+          ]
+        | fields MGR
+        """;
+    RelNode root = getRelNode(ppl);
+    String expectedLogical =
+        ""
+            + "LogicalProject(MGR=[$3])\n"
+            + "  LogicalFilter(condition=[IN($3, {\n"
+            + "LogicalProject(MGR=[$3])\n"
+            + "  LogicalFilter(condition=[=($7, 10)])\n"
+            + "    LogicalTableScan(table=[[scott, EMP]])\n"
+            + "})])\n"
+            + "    LogicalTableScan(table=[[scott, EMP]])\n";
+    verifyLogical(root, expectedLogical);
+
+    String expectedSparkSql =
+        ""
+            + "SELECT `MGR`\n"
+            + "FROM `scott`.`EMP`\n"
+            + "WHERE `MGR` IN (SELECT `MGR`\n"
+            + "FROM `scott`.`EMP`\n"
+            + "WHERE `DEPTNO` = 10)";
+    verifyPPLToSparkSQL(root, expectedSparkSql);
+  }
+
+  @Test
+  public void testTwoExpressionsInSubquery() {
+    String ppl =
+        """
+        source=EMP | where (DEPTNO, ENAME) in [ source=DEPT | fields DEPTNO, DNAME ]
+        | sort - EMPNO | fields EMPNO, ENAME
+        """;
+    RelNode root = getRelNode(ppl);
+    String expectedLogical =
+        ""
+            + "LogicalProject(EMPNO=[$0], ENAME=[$1])\n"
+            + "  LogicalSort(sort0=[$0], dir0=[DESC])\n"
+            + "    LogicalFilter(condition=[IN($7, $1, {\n"
+            + "LogicalProject(DEPTNO=[$0], DNAME=[$1])\n"
+            + "  LogicalTableScan(table=[[scott, DEPT]])\n"
+            + "})])\n"
+            + "      LogicalTableScan(table=[[scott, EMP]])\n";
+    verifyLogical(root, expectedLogical);
+
+    String expectedSparkSql =
+        ""
+            + "SELECT `EMPNO`, `ENAME`\n"
+            + "FROM `scott`.`EMP`\n"
+            + "WHERE (`DEPTNO`, `ENAME`) IN (SELECT `DEPTNO`, `DNAME`\n"
+            + "FROM `scott`.`DEPT`)\n"
+            + "ORDER BY `EMPNO` DESC NULLS FIRST";
+    verifyPPLToSparkSQL(root, expectedSparkSql);
+  }
+
+  @Test
+  public void testFilterInSubquery() {
+    String ppl =
+        """
+        source=EMP (DEPTNO, ENAME) in [ source=DEPT | fields DEPTNO, DNAME ]
+        | sort - EMPNO | fields EMPNO, ENAME
+        """;
+    RelNode root = getRelNode(ppl);
+    String expectedLogical =
+        ""
+            + "LogicalProject(EMPNO=[$0], ENAME=[$1])\n"
+            + "  LogicalSort(sort0=[$0], dir0=[DESC])\n"
+            + "    LogicalFilter(condition=[IN($7, $1, {\n"
+            + "LogicalProject(DEPTNO=[$0], DNAME=[$1])\n"
+            + "  LogicalTableScan(table=[[scott, DEPT]])\n"
+            + "})])\n"
+            + "      LogicalTableScan(table=[[scott, EMP]])\n";
+    verifyLogical(root, expectedLogical);
+
+    String expectedSparkSql =
+        ""
+            + "SELECT `EMPNO`, `ENAME`\n"
+            + "FROM `scott`.`EMP`\n"
+            + "WHERE (`DEPTNO`, `ENAME`) IN (SELECT `DEPTNO`, `DNAME`\n"
+            + "FROM `scott`.`DEPT`)\n"
+            + "ORDER BY `EMPNO` DESC NULLS FIRST";
+    verifyPPLToSparkSQL(root, expectedSparkSql);
+  }
+
+  @Test
+  public void testNotInSubquery() {
+    String ppl =
+        """
+        source=EMP | where (DEPTNO, ENAME) not in [ source=DEPT | fields DEPTNO, DNAME ]
+        | sort - EMPNO | fields EMPNO, ENAME
+        """;
+    RelNode root = getRelNode(ppl);
+    String expectedLogical =
+        ""
+            + "LogicalProject(EMPNO=[$0], ENAME=[$1])\n"
+            + "  LogicalSort(sort0=[$0], dir0=[DESC])\n"
+            + "    LogicalFilter(condition=[NOT(IN($7, $1, {\n"
+            + "LogicalProject(DEPTNO=[$0], DNAME=[$1])\n"
+            + "  LogicalTableScan(table=[[scott, DEPT]])\n"
+            + "}))])\n"
+            + "      LogicalTableScan(table=[[scott, EMP]])\n";
+    verifyLogical(root, expectedLogical);
+
+    String expectedSparkSql =
+        ""
+            + "SELECT `EMPNO`, `ENAME`\n"
+            + "FROM `scott`.`EMP`\n"
+            + "WHERE (`DEPTNO`, `ENAME`) NOT IN (SELECT `DEPTNO`, `DNAME`\n"
+            + "FROM `scott`.`DEPT`)\n"
+            + "ORDER BY `EMPNO` DESC NULLS FIRST";
+    verifyPPLToSparkSQL(root, expectedSparkSql);
+  }
+
+  @Test
+  public void testNestedSubquery() {
+    String ppl =
+        """
+        source=DEPT | where DEPTNO in [ source=EMP | where ENAME in [ source=BONUS | fields ENAME ] | fields DEPTNO ]
+        | sort - DEPTNO | fields DNAME, LOC
+        """;
+    RelNode root = getRelNode(ppl);
+    String expectedLogical =
+        ""
+            + "LogicalProject(DNAME=[$1], LOC=[$2])\n"
+            + "  LogicalSort(sort0=[$0], dir0=[DESC])\n"
+            + "    LogicalFilter(condition=[IN($0, {\n"
+            + "LogicalProject(DEPTNO=[$7])\n"
+            + "  LogicalFilter(condition=[IN($1, {\n"
+            + "LogicalProject(ENAME=[$0])\n"
+            + "  LogicalTableScan(table=[[scott, BONUS]])\n"
+            + "})])\n"
+            + "    LogicalTableScan(table=[[scott, EMP]])\n"
+            + "})])\n"
+            + "      LogicalTableScan(table=[[scott, DEPT]])\n";
+    verifyLogical(root, expectedLogical);
+
+    String expectedSparkSql =
+        ""
+            + "SELECT `DNAME`, `LOC`\n"
+            + "FROM `scott`.`DEPT`\n"
+            + "WHERE `DEPTNO` IN (SELECT `DEPTNO`\n"
+            + "FROM `scott`.`EMP`\n"
+            + "WHERE `ENAME` IN (SELECT `ENAME`\n"
+            + "FROM `scott`.`BONUS`))\n"
+            + "ORDER BY `DEPTNO` DESC NULLS FIRST";
+    verifyPPLToSparkSQL(root, expectedSparkSql);
+  }
+
+  @Test
+  public void testInSubqueryAsJoinFilter() {
+    String ppl =
+        """
+        source=EMP | inner join left=e, right=d
+          ON e.DEPTNO = d.DEPTNO AND e.ENAME in [
+            source=BONUS | WHERE SAL > 1000 | fields ENAME
+          ]
+          DEPT
+        | sort - e.EMPNO | fields e.EMPNO, e.ENAME
+        """;
+    RelNode root = getRelNode(ppl);
+    String expectedLogical =
+        ""
+            + "LogicalProject(EMPNO=[$0], ENAME=[$1])\n"
+            + "  LogicalSort(sort0=[$0], dir0=[DESC])\n"
+            + "    LogicalJoin(condition=[AND(=($7, $8), IN($1, {\n"
+            + "LogicalProject(ENAME=[$0])\n"
+            + "  LogicalFilter(condition=[>($2, 1000)])\n"
+            + "    LogicalTableScan(table=[[scott, BONUS]])\n"
+            + "}))], joinType=[inner])\n"
+            + "      LogicalTableScan(table=[[scott, EMP]])\n"
+            + "      LogicalTableScan(table=[[scott, DEPT]])\n";
+    verifyLogical(root, expectedLogical);
+
+    String expectedSparkSql =
+        "SELECT `EMP`.`EMPNO`, `EMP`.`ENAME`\n"
+            + "FROM `scott`.`EMP`\n"
+            + "INNER JOIN `scott`.`DEPT` ON `EMP`.`DEPTNO` = `DEPT`.`DEPTNO` AND `EMP`.`ENAME` IN"
+            + " (SELECT `ENAME`\n"
+            + "FROM `scott`.`BONUS`\n"
+            + "WHERE `SAL` > 1000)\n"
+            + "ORDER BY `EMP`.`EMPNO` DESC NULLS FIRST";
+    verifyPPLToSparkSQL(root, expectedSparkSql);
+  }
+}

From 82284f612f4dce1f341b1f216c71ec3b7a02b46a Mon Sep 17 00:00:00 2001
From: Lantao Jin <ltjin@amazon.com>
Date: Tue, 4 Mar 2025 11:08:40 +0800
Subject: [PATCH 2/2] Convert the Calcite AssertError to
 SemanticCheckException.class

Signed-off-by: Lantao Jin <ltjin@amazon.com>
---
 .../sql/calcite/CalciteRexNodeVisitor.java    | 29 ++++++++++++-------
 .../standalone/CalcitePPLInSubqueryIT.java    | 26 +++++++++++++++--
 .../standalone/CalcitePPLIntegTestCase.java   | 16 +++++++++-
 .../ppl/calcite/CalcitePPLInSubqueryTest.java | 20 +++++++++++++
 4 files changed, 76 insertions(+), 15 deletions(-)

diff --git a/core/src/main/java/org/opensearch/sql/calcite/CalciteRexNodeVisitor.java b/core/src/main/java/org/opensearch/sql/calcite/CalciteRexNodeVisitor.java
index 781193d0e02..7516b2bd5d3 100644
--- a/core/src/main/java/org/opensearch/sql/calcite/CalciteRexNodeVisitor.java
+++ b/core/src/main/java/org/opensearch/sql/calcite/CalciteRexNodeVisitor.java
@@ -43,6 +43,7 @@
 import org.opensearch.sql.ast.expression.subquery.InSubquery;
 import org.opensearch.sql.ast.tree.UnresolvedPlan;
 import org.opensearch.sql.calcite.utils.BuiltinFunctionUtils;
+import org.opensearch.sql.exception.SemanticCheckException;
 
 @RequiredArgsConstructor
 public class CalciteRexNodeVisitor extends AbstractNodeVisitor<RexNode, CalcitePlanContext> {
@@ -262,16 +263,22 @@ public RexNode visitInSubquery(InSubquery node, CalcitePlanContext context) {
     UnresolvedPlan subquery = node.getQuery();
     RelNode subqueryRel = subquery.accept(planVisitor, context);
     context.relBuilder.build();
-    return context.relBuilder.in(subqueryRel, nodes);
-    // TODO
-    // The {@link org.apache.calcite.tools.RelBuilder#in(RexNode,java.util.function.Function)}
-    // only support one expression. Change to follow code after calcite fixed.
-    //    return context.relBuilder.in(
-    //        nodes.getFirst(),
-    //        b -> {
-    //          RelNode subqueryRel = subquery.accept(planVisitor, context);
-    //          b.build();
-    //          return subqueryRel;
-    //        });
+    try {
+      return context.relBuilder.in(subqueryRel, nodes);
+      // TODO
+      // The {@link org.apache.calcite.tools.RelBuilder#in(RexNode,java.util.function.Function)}
+      // only support one expression. Change to follow code after calcite fixed.
+      //    return context.relBuilder.in(
+      //        nodes.getFirst(),
+      //        b -> {
+      //          RelNode subqueryRel = subquery.accept(planVisitor, context);
+      //          b.build();
+      //          return subqueryRel;
+      //        });
+    } catch (AssertionError e) {
+      throw new SemanticCheckException(
+          "The number of columns in the left hand side of an IN subquery does not match the number"
+              + " of columns in the output of subquery");
+    }
   }
 }
diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLInSubqueryIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLInSubqueryIT.java
index 250b4a0eb70..4acac3f3ed1 100644
--- a/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLInSubqueryIT.java
+++ b/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLInSubqueryIT.java
@@ -17,6 +17,7 @@
 import org.json.JSONObject;
 import org.junit.Ignore;
 import org.junit.Test;
+import org.opensearch.sql.exception.SemanticCheckException;
 
 public class CalcitePPLInSubqueryIT extends CalcitePPLIntegTestCase {
 
@@ -29,7 +30,8 @@ public void init() throws IOException {
     loadIndex(Index.OCCUPATION);
   }
 
-  @Test
+  // TODO https://github.com/opensearch-project/sql/issues/3373
+  @Ignore
   public void testSelfInSubquery() {
     JSONObject result =
         executeQuery(
@@ -315,7 +317,7 @@ public void failWhenNumOfColumnsNotMatchOutputOfSubquery() {
     assertThrows(
         "The number of columns in the left hand side of an IN subquery does not match the number of"
             + " columns in the output of subquery",
-        AssertionError.class,
+        SemanticCheckException.class,
         () ->
             executeQuery(
                 String.format(
@@ -328,9 +330,27 @@ public void failWhenNumOfColumnsNotMatchOutputOfSubquery() {
                    | fields id, name, salary
                    """,
                     TEST_INDEX_WORKER, TEST_INDEX_WORK_INFORMATION)));
+
+    assertThrows(
+        "The number of columns in the left hand side of an IN subquery does not match the number of"
+            + " columns in the output of subquery",
+        SemanticCheckException.class,
+        () ->
+            executeQuery(
+                String.format(
+                    """
+                   source = %s
+                   | where (id, name, salary) in [
+                       source = %s | fields uid, department
+                     ]
+                   | sort  - salary
+                   | fields id, name, salary
+                   """,
+                    TEST_INDEX_WORKER, TEST_INDEX_WORK_INFORMATION)));
   }
 
-  @Test
+  // TODO https://github.com/opensearch-project/sql/issues/3373
+  @Ignore
   public void testInSubqueryWithTableAlias() {
     JSONObject result =
         executeQuery(
diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLIntegTestCase.java b/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLIntegTestCase.java
index 05a4794c9aa..35c46db3c79 100644
--- a/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLIntegTestCase.java
+++ b/integ-test/src/test/java/org/opensearch/sql/calcite/standalone/CalcitePPLIntegTestCase.java
@@ -28,6 +28,7 @@
 import org.opensearch.sql.analysis.Analyzer;
 import org.opensearch.sql.analysis.ExpressionAnalyzer;
 import org.opensearch.sql.calcite.CalciteRelNodeVisitor;
+import org.opensearch.sql.common.antlr.SyntaxCheckException;
 import org.opensearch.sql.common.response.ResponseListener;
 import org.opensearch.sql.common.setting.Settings;
 import org.opensearch.sql.datasource.DataSourceService;
@@ -35,6 +36,9 @@
 import org.opensearch.sql.datasources.auth.DataSourceUserAuthorizationHelper;
 import org.opensearch.sql.datasources.service.DataSourceMetadataStorage;
 import org.opensearch.sql.datasources.service.DataSourceServiceImpl;
+import org.opensearch.sql.exception.NoCursorException;
+import org.opensearch.sql.exception.QueryEngineException;
+import org.opensearch.sql.exception.UnsupportedCursorRequestException;
 import org.opensearch.sql.executor.ExecutionEngine;
 import org.opensearch.sql.executor.QueryManager;
 import org.opensearch.sql.executor.QueryService;
@@ -157,7 +161,17 @@ public void onResponse(ExecutionEngine.QueryResponse response) {
 
           @Override
           public void onFailure(Exception e) {
-            throw new IllegalStateException("Exception happened during execution", e);
+            if (e instanceof SyntaxCheckException) {
+              throw (SyntaxCheckException) e;
+            } else if (e instanceof QueryEngineException) {
+              throw (QueryEngineException) e;
+            } else if (e instanceof UnsupportedCursorRequestException) {
+              throw (UnsupportedCursorRequestException) e;
+            } else if (e instanceof NoCursorException) {
+              throw (NoCursorException) e;
+            } else {
+              throw new IllegalStateException("Exception happened during execution", e);
+            }
           }
         });
     return actual.get();
diff --git a/ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLInSubqueryTest.java b/ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLInSubqueryTest.java
index 738d5ba8257..5f46101a7c9 100644
--- a/ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLInSubqueryTest.java
+++ b/ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLInSubqueryTest.java
@@ -5,9 +5,12 @@
 
 package org.opensearch.sql.ppl.calcite;
 
+import static org.junit.Assert.assertThrows;
+
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.test.CalciteAssert;
 import org.junit.Test;
+import org.opensearch.sql.exception.SemanticCheckException;
 
 public class CalcitePPLInSubqueryTest extends CalcitePPLAbstractTest {
 
@@ -234,4 +237,21 @@ public void testInSubqueryAsJoinFilter() {
             + "ORDER BY `EMP`.`EMPNO` DESC NULLS FIRST";
     verifyPPLToSparkSQL(root, expectedSparkSql);
   }
+
+  @Test
+  public void failWhenNumOfColumnsNotMatchOutputOfSubquery() {
+    String less =
+        """
+            source=EMP | where (DEPTNO) in [ source=DEPT | fields DEPTNO, DNAME ]
+            | sort - EMPNO | fields EMPNO, ENAME
+            """;
+    assertThrows(SemanticCheckException.class, () -> getRelNode(less));
+
+    String more =
+        """
+            source=EMP | where (DEPTNO, ENAME) in [ source=DEPT | fields DEPTNO, DNAME, LOC ]
+            | sort - EMPNO | fields EMPNO, ENAME
+            """;
+    assertThrows(SemanticCheckException.class, () -> getRelNode(more));
+  }
 }
