Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Named;
import javax.ws.rs.DELETE;
Expand Down Expand Up @@ -92,6 +93,7 @@
import org.apache.pinot.spi.utils.CommonConstants.Broker.Request;
import org.apache.pinot.spi.utils.JsonUtils;
import org.apache.pinot.sql.parsers.PinotSqlType;
import org.apache.pinot.sql.parsers.SqlCompilationException;
import org.apache.pinot.sql.parsers.SqlNodeAndOptions;
import org.apache.pinot.tsdb.spi.series.TimeSeriesBlock;
import org.glassfish.jersey.server.ManagedAsync;
Expand Down Expand Up @@ -246,6 +248,94 @@ public Response getQueryFingerprint(String query,
}
}

@POST
@Produces(MediaType.APPLICATION_JSON)
@Path("query/sql/validateSyntax")
@ApiOperation(value = "Validate SQL query syntax",
notes = "Validates if the SQL query can be parsed by Calcite without executing it. "
+ "This is useful for validating if queries can be parsed successfully")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "Validation response (check 'valid' field for result)")
})
@ManualAuthorization
public QuerySyntaxValidationResponse validateSqlQuerySyntaxPost(
@ApiParam(value = "JSON with 'sql' field", required = true) String query,
@Context HttpHeaders httpHeaders) {
try {
JsonNode requestJson = JsonUtils.stringToJsonNode(query);
if (!requestJson.has(Request.SQL)) {
return new QuerySyntaxValidationResponse(
false,
"Payload is missing query string field 'sql'",
QueryErrorCode.JSON_PARSING
);
}
String sqlQuery = requestJson.get(Request.SQL).asText();
return performSqlSyntaxValidation(sqlQuery);
} catch (Exception e) {
LOGGER.error("Error parsing validation request", e);
return new QuerySyntaxValidationResponse(
false,
e.getMessage(),
QueryErrorCode.JSON_PARSING
);
}
}

private QuerySyntaxValidationResponse performSqlSyntaxValidation(String sqlQuery) {
try {
// This catches reserved keyword issues and syntax errors
SqlNodeAndOptions sqlNodeAndOptions = RequestUtils.parseQuery(sqlQuery);

return new QuerySyntaxValidationResponse(
true,
null,
null
);
} catch (SqlCompilationException e) {
LOGGER.debug("SQL validation failed for query: {}", sqlQuery, e);
return new QuerySyntaxValidationResponse(
false,
e.getMessage(),
QueryErrorCode.SQL_PARSING
);
} catch (Exception e) {
String msg = "Unexpected error parsing query" + e.getMessage();
LOGGER.error(msg);
return new QuerySyntaxValidationResponse(
false,
msg,
QueryErrorCode.UNKNOWN
);
}
}

public static class QuerySyntaxValidationResponse {
private final boolean _validQuerySyntax;
private final String _errorMessage;
private final QueryErrorCode _errorCode;

public QuerySyntaxValidationResponse(boolean validQuery, String errorMessage, QueryErrorCode errorCode) {
_validQuerySyntax = validQuery;
_errorMessage = errorMessage;
_errorCode = errorCode;
}

public boolean isValidQuerySyntax() {
return _validQuerySyntax;
}

@Nullable
public String getErrorMessage() {
return _errorMessage;
}

@Nullable
public QueryErrorCode getErrorCode() {
return _errorCode;
}
}

@GET
@ManagedAsync
@Produces(MediaType.APPLICATION_JSON)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,4 +311,35 @@ public void testQueryResponseSizeMetric()
assertEquals(sizeCaptor.getValue().longValue(), expectedSize,
"Metric should record the actual response size in bytes");
}

@Test
public void testPinotQueryValidationWithValidQuery() throws Exception {
String validQuery = "{\"sql\":\"SELECT * FROM myTable LIMIT 10\"}";
PinotClientRequest.QuerySyntaxValidationResponse response =
_pinotClientRequest.validateSqlQuerySyntaxPost(validQuery, _httpHeaders);

Assert.assertTrue(response.isValidQuerySyntax(), "Response value should be valid");
Assert.assertNull(response.getErrorMessage());
Assert.assertNull(response.getErrorCode());
}

@Test
public void testPinotQueryValidationWithInvalidQuery() throws Exception {
String invalidQuery = "{\"sql\":\"SELECT select FROM myTable LIMIT 10\"}";

PinotClientRequest.QuerySyntaxValidationResponse response =
_pinotClientRequest.validateSqlQuerySyntaxPost(invalidQuery, _httpHeaders);
Assert.assertFalse(response.isValidQuerySyntax(), "Response value should be invalid");
assertEquals(response.getErrorCode(), QueryErrorCode.SQL_PARSING);
}

@Test
public void testPinotQueryValidationWithInvalidRequestPayload() throws Exception {
String invalidQuery = "{\"query\":\"SELECT select FROM myTable LIMIT 10\"}";

PinotClientRequest.QuerySyntaxValidationResponse response =
_pinotClientRequest.validateSqlQuerySyntaxPost(invalidQuery, _httpHeaders);
Assert.assertFalse(response.isValidQuerySyntax(), "Response value should be invalid");
assertEquals(response.getErrorCode(), QueryErrorCode.JSON_PARSING);
}
}
Loading