Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b89ff42
integrate syntax errors with error report
ritvibhatt Apr 20, 2026
14475e5
fix tests
ritvibhatt Apr 21, 2026
c9ad1f2
fix tests
ritvibhatt Apr 21, 2026
e473f7f
fix formatting
ritvibhatt Apr 22, 2026
f7cbe56
fix sql fallback logic
ritvibhatt Apr 22, 2026
575202d
fix unified query tests
ritvibhatt Apr 22, 2026
8e8ab9e
fix formatting
ritvibhatt Apr 22, 2026
71ad3bb
move error report
ritvibhatt Apr 22, 2026
bd46b1e
refactor syntax exceptions
ritvibhatt Apr 24, 2026
3cd01e4
add tests
ritvibhatt Apr 24, 2026
078dc07
fix legacy sql tests
ritvibhatt Apr 24, 2026
940310d
fix unit tests
ritvibhatt Apr 24, 2026
51952fb
fix unit tests
ritvibhatt Apr 24, 2026
283ebd9
fix unit tests
ritvibhatt Apr 24, 2026
c1766d5
restore reverted infrastructure changes
ritvibhatt Apr 25, 2026
79f9b23
refactor error listener
ritvibhatt Apr 28, 2026
1f9b08b
Remove accidentally committed files
ritvibhatt Apr 28, 2026
c88bd44
remove unnecessary file
ritvibhatt Apr 28, 2026
2f16563
fix legacy fallback
ritvibhatt Apr 28, 2026
f02c2a9
fix formatting
ritvibhatt Apr 28, 2026
07748d0
add more suggestion patterns
ritvibhatt Apr 28, 2026
24d398c
update providers
ritvibhatt May 5, 2026
5556f7b
Merge branch 'main' into syntax-exception-error-message
ritvibhatt May 7, 2026
f65c32d
fix checks for syntax exceptions
ritvibhatt May 7, 2026
e3547be
fix typo
ritvibhatt May 7, 2026
c0fe174
fix test coverage error
ritvibhatt May 7, 2026
81d2b94
fix compilation error
ritvibhatt May 8, 2026
5620d46
fix formatting
ritvibhatt May 8, 2026
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 @@ -23,6 +23,7 @@
import org.opensearch.sql.ast.tree.UnresolvedPlan;
import org.opensearch.sql.calcite.CalciteRelNodeVisitor;
import org.opensearch.sql.common.antlr.SyntaxCheckException;
import org.opensearch.sql.common.error.ErrorReport;
import org.opensearch.sql.executor.QueryType;

/**
Expand Down Expand Up @@ -71,6 +72,9 @@ public RelNode plan(String query) {
});
} catch (SyntaxCheckException | UnsupportedOperationException e) {
throw e;
} catch (ErrorReport e) {
if (e.getCause() instanceof SyntaxCheckException) throw e;
throw new IllegalStateException("Failed to plan query", e);
} catch (Exception e) {
throw new IllegalStateException("Failed to plan query", e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import org.apache.calcite.schema.Schema;
import org.apache.calcite.schema.impl.AbstractSchema;
import org.junit.Test;
import org.opensearch.sql.common.antlr.SyntaxCheckException;
import org.opensearch.sql.common.error.ErrorReport;
import org.opensearch.sql.executor.QueryType;

public class UnifiedQueryPlannerTest extends UnifiedQueryTestBase {
Expand Down Expand Up @@ -111,8 +111,11 @@ public void testUnsupportedStatementType() {
planner.plan("explain source = catalog.employees"); // explain statement
}

@Test(expected = SyntaxCheckException.class)
@Test
public void testPlanPropagatingSyntaxCheckException() {
planner.plan("source = catalog.employees | eval"); // Trigger syntax error from parser
assertThrows(
ErrorReport.class,
() ->
planner.plan("source = catalog.employees | eval")); // Trigger syntax error from parser
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import org.junit.Test;
import org.opensearch.sql.api.UnifiedQueryTestBase;
import org.opensearch.sql.ast.tree.UnresolvedPlan;
import org.opensearch.sql.common.antlr.SyntaxCheckException;
import org.opensearch.sql.common.error.ErrorReport;

public class UnifiedQueryParserTest extends UnifiedQueryTestBase {

Expand Down Expand Up @@ -77,7 +77,7 @@ public void testParseStats() {

@Test
public void testSyntaxErrorThrows() {
assertThrows(SyntaxCheckException.class, () -> context.getParser().parse("not a valid query"));
assertThrows(ErrorReport.class, () -> context.getParser().parse("not a valid query"));
}

private void assertEqual(String query, UnresolvedPlan expected) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import org.apache.logging.log4j.Logger;
import org.opensearch.sql.common.antlr.CaseInsensitiveCharStream;
import org.opensearch.sql.common.antlr.SyntaxAnalysisErrorListener;
import org.opensearch.sql.common.antlr.SyntaxCheckException;
import org.opensearch.sql.common.error.ErrorReport;
import org.opensearch.sql.spark.antlr.parser.FlintSparkSqlExtensionsBaseVisitor;
import org.opensearch.sql.spark.antlr.parser.FlintSparkSqlExtensionsLexer;
import org.opensearch.sql.spark.antlr.parser.FlintSparkSqlExtensionsParser;
Expand Down Expand Up @@ -85,7 +85,7 @@ public static boolean isFlintExtensionQuery(String sqlQuery) {
try {
flintSparkSqlExtensionsParser.statement();
return true;
} catch (SyntaxCheckException syntaxCheckException) {
} catch (ErrorReport e) {
return false;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,22 @@
package org.opensearch.sql.common.antlr;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import org.antlr.v4.runtime.BaseErrorListener;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.Vocabulary;
import org.antlr.v4.runtime.misc.IntervalSet;
import org.opensearch.sql.common.antlr.suggestion.SyntaxErrorContext;
import org.opensearch.sql.common.antlr.suggestion.SyntaxErrorSuggestionRegistry;
import org.opensearch.sql.common.error.ErrorCode;
import org.opensearch.sql.common.error.ErrorReport;

/**
* Syntax analysis error listener that handles any syntax error by throwing exception with useful
Expand All @@ -39,13 +46,42 @@ public void syntaxError(
Token offendingToken = (Token) offendingSymbol;
String query = tokens.getText();

throw new SyntaxCheckException(
// Build the original error message for backward compatibility
String details =
String.format(
Locale.ROOT,
"[%s] is not a valid term at this part of the query: '%s' <-- HERE. %s",
getOffendingText(offendingToken),
truncateQueryAtOffendingToken(query, offendingToken),
getDetails(recognizer, msg, e)));
getDetails(recognizer, msg, e));

// Create a SyntaxCheckException as the underlying cause
SyntaxCheckException cause = new SyntaxCheckException(details);

// Build position information
Map<String, Object> position = new HashMap<>();
position.put("line", line);
position.put("column", charPositionInLine);

// Build ErrorReport with structured context
ErrorReport.Builder reportBuilder =
ErrorReport.wrap(cause)
.code(ErrorCode.SYNTAX_ERROR)
.location("while parsing the query")
.context("query", query)
.context("position", position)
.context("offending_token", getOffendingText(offendingToken));

// Use the suggestion registry to find pattern-based suggestions
SyntaxErrorContext context =
new SyntaxErrorContext(recognizer, offendingToken, tokens, query, e);
Optional<String> customSuggestion = SyntaxErrorSuggestionRegistry.findSuggestion(context);

if (customSuggestion.isPresent()) {
reportBuilder.suggestion(customSuggestion.get());
}

throw reportBuilder.build();
}

private String getOffendingText(Token offendingToken) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.common.antlr.suggestion;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Vocabulary;
import org.antlr.v4.runtime.misc.IntervalSet;

/** Fallback provider: surface the parser's expected-tokens set. */
public class ExpectedTokensSuggestionProvider implements SyntaxErrorSuggestionProvider {
private static final int MAX = 5;

@Override
public Optional<String> getSuggestion(SyntaxErrorContext ctx) {
RecognitionException e = ctx.getException();
if (e == null) return Optional.empty();
IntervalSet expected = e.getExpectedTokens();
if (expected == null || expected.size() == 0) return Optional.empty();
List<Integer> types = expected.toList();
if (types.isEmpty()) return Optional.empty();
Vocabulary vocab = ctx.getRecognizer().getVocabulary();
List<String> names = new ArrayList<>(MAX);
for (int type : types.subList(0, Math.min(types.size(), MAX))) {
names.add(vocab.getDisplayName(type));
}
String msg =
types.size() > MAX
? String.format(
"Expected one of %d possible tokens. Examples: %s",
types.size(), String.join(", ", names))
: "Expected tokens: " + String.join(", ", names);
return Optional.of(msg);
}

@Override
public int getPriority() {
return Integer.MAX_VALUE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.common.antlr.suggestion;

import java.util.Optional;
import java.util.regex.Pattern;

/** Detects SELECT * FROM pattern in PPL context. */
public class SelectStarSuggestionProvider implements SyntaxErrorSuggestionProvider {
private static final Pattern SELECT_STAR_PATTERN = Pattern.compile("(?i)^\\s*select\\s+\\*.*");
private static final Pattern SELECT_FIELDS_PATTERN =
Pattern.compile("(?i)^\\s*select\\s+[a-zA-Z_][a-zA-Z0-9_]*.*");
// Pattern to extract table name from "SELECT ... FROM table_name" queries
private static final Pattern TABLE_NAME_PATTERN =
Pattern.compile("(?i).*\\bfrom\\s+([a-zA-Z_][a-zA-Z0-9_\\.]*)", Pattern.DOTALL);

@Override
public Optional<String> getSuggestion(SyntaxErrorContext context) {
// Only suggest PPL syntax when the error originated from the PPL parser.
if (context.getRecognizer() == null
|| !context.getRecognizer().getGrammarFileName().contains("PPL")) {
return Optional.empty();
}

// Check if query starts with "select" (case-insensitive)
String query = context.getQuery().trim();
if (!query.toLowerCase().startsWith("select")) {
return Optional.empty();
}

// Extract table name if available
String tableName = extractTableName(query);
String tableRef = tableName != null ? tableName : "index";

// Check if this looks like SQL SELECT * syntax
if (SELECT_STAR_PATTERN.matcher(query).matches()) {
return Optional.of(
"PPL uses 'source="
+ tableRef
+ " | fields *' instead of 'SELECT * FROM "
+ tableRef
+ "'");
}

// Check if this looks like SQL SELECT fields syntax
if (SELECT_FIELDS_PATTERN.matcher(query).matches()) {
return Optional.of(
"PPL uses 'source="
+ tableRef
+ " | fields field1, field2' instead of 'SELECT field1, field2 FROM "
+ tableRef
+ "'");
}

return Optional.empty();
}

private String extractTableName(String query) {
java.util.regex.Matcher matcher = TABLE_NAME_PATTERN.matcher(query);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}

@Override
public int getPriority() {
return 10; // High priority for SQL/PPL syntax confusion
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.common.antlr.suggestion;

import java.util.List;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
import org.antlr.v4.runtime.Token;

/** Context passed to each {@link SyntaxErrorSuggestionProvider} for pattern matching. */
@RequiredArgsConstructor
@Getter
public class SyntaxErrorContext {
private final Recognizer<?, ?> recognizer;
private final Token offendingToken;
private final CommonTokenStream tokens;
private final String query;
private final RecognitionException exception;

public String getOffendingText() {
return offendingToken == null ? "" : offendingToken.getText();
}

/** Text of the query after the offending token (trimmed). */
public String getRemainingQuery() {
if (offendingToken == null) return "";
int end = offendingToken.getStopIndex() + 1;
return end >= query.length() ? "" : query.substring(end);
}

public List<Token> getAllTokens() {
return tokens.getTokens();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.common.antlr.suggestion;

import java.util.Optional;

/**
* Provides syntax error suggestions for a specific pattern. Implementations must be stateless and
* thread-safe.
*/
public interface SyntaxErrorSuggestionProvider {
/** Return a suggestion for the given error context, or empty if no suggestion applies. */
Optional<String> getSuggestion(SyntaxErrorContext context);

/**
* Priority for this provider (lower = higher priority). Providers with lower priority values are
* checked first.
*/
int getPriority();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.common.antlr.suggestion;

import java.util.Arrays;
import java.util.Comparator;
import java.util.Optional;
import java.util.concurrent.CopyOnWriteArrayList;

/** Registry of syntax-error suggestion providers, evaluated in priority order. */
public final class SyntaxErrorSuggestionRegistry {
// CopyOnWriteArrayList: safe iteration during concurrent register() calls.
private static final CopyOnWriteArrayList<SyntaxErrorSuggestionProvider> PROVIDERS =
new CopyOnWriteArrayList<>();

static {
register(
new SelectStarSuggestionProvider(),
new UnmatchedParenthesesSuggestionProvider(),
new ExpectedTokensSuggestionProvider());
}

private SyntaxErrorSuggestionRegistry() {}

public static void register(SyntaxErrorSuggestionProvider... providers) {
PROVIDERS.addAll(Arrays.asList(providers));
PROVIDERS.sort(Comparator.comparingInt(SyntaxErrorSuggestionProvider::getPriority));
}

/** Returns suggestion from the first matching provider (by priority); empty otherwise. */
public static Optional<String> findSuggestion(SyntaxErrorContext context) {
for (SyntaxErrorSuggestionProvider provider : PROVIDERS) {
Optional<String> suggestion = provider.getSuggestion(context);
if (suggestion.isPresent()) {
return suggestion;
}
}
return Optional.empty();
}
}
Loading
Loading