This is an automated email from the ASF dual-hosted git repository.

Jackie-Jiang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new 3a6b6943a4a Add POST /query/sql/validateSyntax broker endpoint 
(#17615) (#18281)
3a6b6943a4a is described below

commit 3a6b6943a4aad94d26f430019a450103e65e27d7
Author: Deep Patel <[email protected]>
AuthorDate: Fri May 29 17:27:06 2026 -0400

    Add POST /query/sql/validateSyntax broker endpoint (#17615) (#18281)
---
 .../broker/api/resources/PinotClientRequest.java   | 48 +++++++++++++++++
 .../api/resources/SqlSyntaxValidationResponse.java | 61 ++++++++++++++++++++++
 .../api/resources/PinotClientRequestTest.java      | 59 +++++++++++++++++++++
 3 files changed, 168 insertions(+)

diff --git 
a/pinot-broker/src/main/java/org/apache/pinot/broker/api/resources/PinotClientRequest.java
 
b/pinot-broker/src/main/java/org/apache/pinot/broker/api/resources/PinotClientRequest.java
index 97743be79c5..445eb946e9b 100644
--- 
a/pinot-broker/src/main/java/org/apache/pinot/broker/api/resources/PinotClientRequest.java
+++ 
b/pinot-broker/src/main/java/org/apache/pinot/broker/api/resources/PinotClientRequest.java
@@ -268,6 +268,43 @@ public class PinotClientRequest {
     }
   }
 
+  @POST
+  @Produces(MediaType.APPLICATION_JSON)
+  @Path("query/sql/validateSyntax")
+  @ApiOperation(value = "Validate the syntax of a SQL query without executing 
it",
+      notes = "Parses the query using Pinot's Calcite-based SQL parser. No 
table metadata or "
+          + "schema validation is performed, and the query is not executed. 
Supports both "
+          + "single-stage and multi-stage queries. Returns HTTP 200 in both 
the valid and invalid "
+          + "cases; clients should inspect the `valid` field of the response 
body.")
+  @ApiResponses(value = {
+      @ApiResponse(code = 200, message = "Syntax validation result"),
+      @ApiResponse(code = 400, message = "Bad Request"),
+      @ApiResponse(code = 500, message = "Internal Server Error")
+  })
+  @ManualAuthorization
+  public Response validateSqlSyntax(String query,
+      @Context org.glassfish.grizzly.http.server.Request requestContext,
+      @Context HttpHeaders httpHeaders) {
+    try {
+      JsonNode requestJson = JsonUtils.stringToJsonNode(query);
+      if (!requestJson.has(Request.SQL)) {
+        return Response.status(Response.Status.BAD_REQUEST)
+            .entity("{\"error\": \"Payload is missing the query string field 
'sql'\"}")
+            .build();
+      }
+      return Response.ok(validateSqlSyntax((ObjectNode) requestJson)).build();
+    } catch (WebApplicationException wae) {
+      
_brokerMetrics.addMeteredGlobalValue(BrokerMeter.WEB_APPLICATION_EXCEPTIONS, 
1L);
+      throw wae;
+    } catch (Exception e) {
+      LOGGER.error("Caught exception while validating SQL syntax for POST 
request", e);
+      
_brokerMetrics.addMeteredGlobalValue(BrokerMeter.UNCAUGHT_POST_EXCEPTIONS, 1L);
+      return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+          .entity("{\"error\": \"" + e.getMessage() + "\"}")
+          .build();
+    }
+  }
+
   @GET
   @ManagedAsync
   @Produces(MediaType.APPLICATION_JSON)
@@ -675,6 +712,17 @@ public class PinotClientRequest {
     }
   }
 
+  @VisibleForTesting
+  SqlSyntaxValidationResponse validateSqlSyntax(ObjectNode sqlRequestJson) {
+    try {
+      SqlNodeAndOptions sqlNodeAndOptions =
+          RequestUtils.parseQuery(sqlRequestJson.get(Request.SQL).asText(), 
sqlRequestJson);
+      return 
SqlSyntaxValidationResponse.valid(sqlNodeAndOptions.getSqlType().name());
+    } catch (Exception e) {
+      return SqlSyntaxValidationResponse.invalid(e.getMessage());
+    }
+  }
+
   private QueryFingerprint generateQueryFingerprint(ObjectNode sqlRequestJson) 
throws Exception {
     SqlNodeAndOptions sqlNodeAndOptions;
     try {
diff --git 
a/pinot-broker/src/main/java/org/apache/pinot/broker/api/resources/SqlSyntaxValidationResponse.java
 
b/pinot-broker/src/main/java/org/apache/pinot/broker/api/resources/SqlSyntaxValidationResponse.java
new file mode 100644
index 00000000000..7eb8ce298c1
--- /dev/null
+++ 
b/pinot-broker/src/main/java/org/apache/pinot/broker/api/resources/SqlSyntaxValidationResponse.java
@@ -0,0 +1,61 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.broker.api.resources;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+
+/**
+ * Response for the {@code POST /query/sql/validateSyntax} broker endpoint. 
Reports whether a SQL
+ * query was accepted by Pinot's Calcite-based parser, along with an error 
message when parsing
+ * failed.
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class SqlSyntaxValidationResponse {
+
+  private final boolean _valid;
+  private final String _sqlType;
+  private final String _errorMessage;
+
+  public SqlSyntaxValidationResponse(boolean valid, String sqlType, String 
errorMessage) {
+    _valid = valid;
+    _sqlType = sqlType;
+    _errorMessage = errorMessage;
+  }
+
+  public static SqlSyntaxValidationResponse valid(String sqlType) {
+    return new SqlSyntaxValidationResponse(true, sqlType, null);
+  }
+
+  public static SqlSyntaxValidationResponse invalid(String errorMessage) {
+    return new SqlSyntaxValidationResponse(false, null, errorMessage);
+  }
+
+  public boolean isValid() {
+    return _valid;
+  }
+
+  public String getSqlType() {
+    return _sqlType;
+  }
+
+  public String getErrorMessage() {
+    return _errorMessage;
+  }
+}
diff --git 
a/pinot-broker/src/test/java/org/apache/pinot/broker/api/resources/PinotClientRequestTest.java
 
b/pinot-broker/src/test/java/org/apache/pinot/broker/api/resources/PinotClientRequestTest.java
index 5ca530e8bb3..0c8dc42e362 100644
--- 
a/pinot-broker/src/test/java/org/apache/pinot/broker/api/resources/PinotClientRequestTest.java
+++ 
b/pinot-broker/src/test/java/org/apache/pinot/broker/api/resources/PinotClientRequestTest.java
@@ -435,6 +435,65 @@ public class PinotClientRequestTest {
     verify(_brokerMetrics, 
never()).addMeteredGlobalValue(BrokerMeter.UNCAUGHT_POST_EXCEPTIONS, 1L);
   }
 
+  @Test
+  public void testValidateSqlSyntaxSingleStage() throws Exception {
+    Request request = mock(Request.class);
+    when(request.getRequestURL()).thenReturn(new StringBuilder());
+
+    String requestJson = "{\"sql\": \"SELECT * FROM myTable WHERE id > 10\"}";
+    Response response = _pinotClientRequest.validateSqlSyntax(requestJson, 
request, null);
+
+    assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+    SqlSyntaxValidationResponse body = (SqlSyntaxValidationResponse) 
response.getEntity();
+    Assert.assertTrue(body.isValid(), "Valid single-stage query should parse 
successfully");
+    assertEquals(body.getSqlType(), "DQL");
+    Assert.assertNull(body.getErrorMessage());
+  }
+
+  @Test
+  public void testValidateSqlSyntaxMultiStage() throws Exception {
+    Request request = mock(Request.class);
+    when(request.getRequestURL()).thenReturn(new StringBuilder());
+
+    String requestJson = "{\"sql\": \"SET useMultistageEngine=true; \\n"
+        + "SELECT * FROM t1 LEFT JOIN t2 ON t1.id = t2.id WHERE t1.col1 > 
100\"}";
+    Response response = _pinotClientRequest.validateSqlSyntax(requestJson, 
request, null);
+
+    assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+    SqlSyntaxValidationResponse body = (SqlSyntaxValidationResponse) 
response.getEntity();
+    Assert.assertTrue(body.isValid(), "Valid multi-stage query should parse 
successfully");
+    assertEquals(body.getSqlType(), "DQL");
+    Assert.assertNull(body.getErrorMessage());
+  }
+
+  @Test
+  public void testValidateSqlSyntaxWithInvalidSql() throws Exception {
+    Request request = mock(Request.class);
+    when(request.getRequestURL()).thenReturn(new StringBuilder());
+
+    String requestJson = "{\"sql\": \"SELECT FROM WHERE\"}";
+    Response response = _pinotClientRequest.validateSqlSyntax(requestJson, 
request, null);
+
+    assertEquals(response.getStatus(), Response.Status.OK.getStatusCode(),
+        "Invalid SQL should still return HTTP 200 with valid=false in body");
+    SqlSyntaxValidationResponse body = (SqlSyntaxValidationResponse) 
response.getEntity();
+    assertFalse(body.isValid(), "Unparseable query should report valid=false");
+    Assert.assertNull(body.getSqlType());
+    Assert.assertNotNull(body.getErrorMessage(), "Failed validation should 
include an error message");
+  }
+
+  @Test
+  public void testValidateSqlSyntaxMissingSqlField() throws Exception {
+    Request request = mock(Request.class);
+    when(request.getRequestURL()).thenReturn(new StringBuilder());
+
+    String requestJson = "{\"notSql\": \"SELECT * FROM myTable\"}";
+    Response response = _pinotClientRequest.validateSqlSyntax(requestJson, 
request, null);
+
+    assertEquals(response.getStatus(), 
Response.Status.BAD_REQUEST.getStatusCode(),
+        "Payload missing the 'sql' field should return BAD_REQUEST");
+  }
+
   @Test
   public void testQueryResponseSizeMetric()
       throws Exception {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to