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]