Skip to content

Commit 6c5b8e5

Browse files
mihaibudiuxiaojun
authored andcommitted
Fix test framework to handle correctly carets everywhere
Signed-off-by: Mihai Budiu <mbudiu@feldera.com>
1 parent 85410dc commit 6c5b8e5

File tree

9 files changed

+120
-140
lines changed

9 files changed

+120
-140
lines changed

core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -742,10 +742,16 @@ public static int lineColToIndex(String sql, int line, int column) {
742742
return i + column;
743743
}
744744

745+
/** Given a string with carets, double each of them.
746+
* (This should probably be more sophisticated, and ignore carets within string literals). */
747+
public static String escapeCarets(String sql) {
748+
return sql.replace("^", "^^");
749+
}
750+
745751
/**
746752
* Converts a string to a string with one or two carets in it. For example,
747753
* <code>addCarets("values (foo)", 1, 9, 1, 12)</code> yields "values
748-
* (^foo^)".
754+
* (^foo^)". Existing carets are escaped by doubling.
749755
*/
750756
public static String addCarets(
751757
String sql,
@@ -755,8 +761,8 @@ public static String addCarets(
755761
int endCol) {
756762
String sqlWithCarets;
757763
int cut = lineColToIndex(sql, line, col);
758-
sqlWithCarets = sql.substring(0, cut) + "^"
759-
+ sql.substring(cut);
764+
sqlWithCarets = escapeCarets(sql.substring(0, cut)) + "^"
765+
+ escapeCarets(sql.substring(cut));
760766
if ((col != endCol) || (line != endLine)) {
761767
cut = lineColToIndex(sqlWithCarets, endLine, endCol);
762768
if (line == endLine) {
@@ -765,7 +771,7 @@ public static String addCarets(
765771
if (cut < sqlWithCarets.length()) {
766772
sqlWithCarets =
767773
sqlWithCarets.substring(0, cut)
768-
+ "^" + sqlWithCarets.substring(cut);
774+
+ "^" + escapeCarets(sqlWithCarets.substring(cut));
769775
} else {
770776
sqlWithCarets += "^";
771777
}

core/src/main/java/org/apache/calcite/sql/parser/StringAndPos.java

Lines changed: 5 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,10 @@
2121
import java.util.Objects;
2222

2323
/**
24-
* Contains a string, the offset of a token within the string, and a parser position containing the
25-
* beginning and end line number.
24+
* Contains a string, the offset of a token within the string, and a parser
25+
* position containing the beginning and end line number.
2626
*/
2727
public class StringAndPos {
28-
2928
public final String sql;
3029
public final int cursor;
3130
public final @Nullable SqlParserPos pos;
@@ -53,66 +52,8 @@ private StringAndPos(String sql, int cursor, @Nullable SqlParserPos pos) {
5352
}
5453

5554
/**
56-
* Checks if the SQL expression is a simple XOR pattern that can be safely processed.
57-
*
58-
* <p>This method provides special handling for the BITXOR_OPERATOR (^) in Apache Calcite
59-
* to identify common XOR patterns that can be evaluated directly without complex rewriting.
60-
*
61-
* <p>Supported patterns include:
62-
* <ul>
63-
* <li><b>Literal numbers:</b> {@code number ^ number}, e.g.,
64-
* {@code "5 ^ 3"}, {@code "-2 ^ 7"}</li>
65-
* <li><b>Null handling:</b>
66-
* <ul>
67-
* <li>{@code NULL ^ number} or {@code CAST(NULL AS TYPE) ^ number}</li>
68-
* <li>{@code number ^ NULL} or {@code number ^ CAST(NULL AS TYPE)}</li>
69-
* </ul>
70-
* </li>
71-
* <li><b>Typed casts:</b> {@code CAST(...) ^ CAST(...)},
72-
* including types with precision or UNSIGNED,
73-
* e.g., {@code CAST(5 AS INTEGER) ^ CAST(3 AS BIGINT)},
74-
* {@code CAST(255 AS INTEGER UNSIGNED) ^ CAST(1 AS INTEGER UNSIGNED)}</li>
75-
* </ul>
76-
*
77-
* @param sql the SQL expression containing the XOR (^) operator
78-
* @return true if the expression matches a simple XOR pattern, false otherwise
79-
*/
80-
private static boolean isSimpleXorPattern(String sql) {
81-
int caretIndex = sql.indexOf('^');
82-
if (caretIndex == -1) {
83-
return false;
84-
}
85-
String before = sql.substring(0, caretIndex).trim();
86-
String after = sql.substring(caretIndex + 1).trim();
87-
88-
// Pattern: number ^ number
89-
if (before.matches(".*-?\\d$") && after.matches("^-?\\d.*")) {
90-
return true;
91-
}
92-
if (before.matches(".*CAST\\s*\\(\\s*NULL\\s+AS\\s+\\w+(\\(\\d+\\))?\\s*\\)$")
93-
&& after.matches("^\\d.*")) {
94-
return true;
95-
}
96-
97-
// number ^ CAST(NULL AS ...)
98-
if (before.matches(".*\\d$")
99-
&& after.matches("^CAST\\s*\\(\\s*NULL\\s+AS\\s+\\w+(\\(\\d+\\))?\\s*\\).*")) {
100-
return true;
101-
}
102-
// Pattern: CAST(...) ^ CAST(...)
103-
String castPattern =
104-
"CAST\\s*\\(.*?AS\\s+[A-Z]+(?:\\s*\\(\\d+(?:,\\d+)?\\))?(?:\\s+UNSIGNED)?\\s*\\)";
105-
if (before.matches(".*" + castPattern + "$")
106-
&& after.matches("^" + castPattern + ".*")) {
107-
return true;
108-
}
109-
110-
return false;
111-
}
112-
113-
/**
114-
* Looks for one or two carets in a SQL string, and if present, converts them into a parser
115-
* position.
55+
* Looks for one or two carets in a SQL string, and if present, converts
56+
* them into a parser position.
11657
*
11758
* <p>Examples:
11859
*
@@ -124,15 +65,10 @@ private static boolean isSimpleXorPattern(String sql) {
12465
* </ul>
12566
*/
12667
public static StringAndPos of(String sql) {
127-
12868
int firstCaret = sql.indexOf('^');
12969
if (firstCaret < 0) {
13070
return new StringAndPos(sql, -1, null);
13171
}
132-
// check for bitxor operator test cases
133-
if (isSimpleXorPattern(sql)) {
134-
return new StringAndPos(sql, -1, null);
135-
}
13672
int secondCaret = sql.indexOf('^', firstCaret + 1);
13773
if (secondCaret == firstCaret + 1) {
13874
// If SQL contains "^^", it does not contain error positions; convert each
@@ -169,7 +105,7 @@ public static StringAndPos of(String sql) {
169105
}
170106

171107
public String addCarets() {
172-
return pos == null ? sql
108+
return pos == null ? SqlParserUtil.escapeCarets(sql)
173109
: SqlParserUtil.addCarets(sql, pos.getLineNum(), pos.getColumnNum(),
174110
pos.getEndLineNum(), pos.getEndColumnNum() + 1);
175111
}

core/src/test/java/org/apache/calcite/test/SqlOperatorUnparseTest.java

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import org.apache.calcite.sql.SqlNode;
2020
import org.apache.calcite.sql.parser.SqlParseException;
2121
import org.apache.calcite.sql.parser.SqlParser;
22+
import org.apache.calcite.sql.parser.SqlParserUtil;
23+
import org.apache.calcite.sql.parser.StringAndPos;
2224
import org.apache.calcite.sql.test.SqlOperatorFixture;
2325
import org.apache.calcite.sql.test.SqlTestFactory;
2426

@@ -77,21 +79,26 @@ TesterImpl withFactory(UnaryOperator<SqlTestFactory> transform) {
7779
return new UnparseTester(transform.apply(this.factory));
7880
}
7981

80-
String rewrite(String sql) throws SqlParseException {
81-
final SqlParser parser = factory.createParser(sql);
82+
String rewrite(StringAndPos sap) throws SqlParseException {
83+
final SqlParser parser = factory.createParser(sap.sql);
8284
final SqlNode sqlNode = parser.parseStmt();
83-
return sqlNode.toSqlString(c -> c.withClauseStartsLine(false)).getSql();
85+
String result = sqlNode.toSqlString(c -> c.withClauseStartsLine(false)).getSql();
86+
return SqlParserUtil.escapeCarets(result);
8487
}
8588

8689
@Override public void forEachQuery(
87-
SqlTestFactory factory, String expression, Consumer<String> consumer) {
88-
consumer.accept(buildQuery2(factory, expression));
90+
SqlTestFactory factory, String expressionWithCarets, Consumer<String> consumer) {
91+
String query = buildQuery2(factory, expressionWithCarets);
92+
query = SqlParserUtil.escapeCarets(query);
93+
consumer.accept(query);
8994
}
9095

91-
@Override public void check(SqlTestFactory factory, String sql, TypeChecker typeChecker,
96+
@Override public void check(
97+
SqlTestFactory factory, String queryWithCarets, TypeChecker typeChecker,
9298
ParameterChecker parameterChecker, ResultChecker resultChecker) {
9399
try {
94-
String optQuery = this.rewrite(sql);
100+
StringAndPos sap = StringAndPos.of(queryWithCarets);
101+
String optQuery = this.rewrite(sap);
95102
super.check(factory, optQuery, typeChecker, parameterChecker, resultChecker);
96103
} catch (SqlParseException e) {
97104
throw new RuntimeException(e);

testkit/src/main/java/org/apache/calcite/sql/test/AbstractSqlTester.java

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ private static ValidatedNodeConsumer checkColumnTypeAction(
222222
}
223223

224224
@Override public void check(SqlTestFactory factory,
225-
String query, TypeChecker typeChecker,
225+
String queryWithCarets, TypeChecker typeChecker,
226226
ParameterChecker parameterChecker, ResultChecker resultChecker) {
227227
// This implementation does NOT check the result!
228228
// All it does is check the return type.
@@ -232,12 +232,14 @@ private static ValidatedNodeConsumer checkColumnTypeAction(
232232

233233
// Parse and validate. There should be no errors.
234234
// There must be 1 column. Get its type.
235-
RelDataType actualType = getColumnType(factory, query);
235+
RelDataType actualType = getColumnType(factory, queryWithCarets);
236236

237237
// Check result type.
238-
typeChecker.checkType(() -> "Query: " + query, actualType);
238+
typeChecker.checkType(() -> "Query: " + queryWithCarets, actualType);
239239

240-
Pair<SqlValidator, SqlNode> p = parseAndValidate(factory, query);
240+
// Unescape the carets that may be present in the query
241+
final StringAndPos pos = StringAndPos.of(queryWithCarets);
242+
Pair<SqlValidator, SqlNode> p = parseAndValidate(factory, pos.sql);
241243
SqlValidator validator = p.left;
242244
SqlNode n = p.right;
243245
final RelDataType parameterRowType = validator.getParameterRowType(n);
@@ -265,12 +267,12 @@ private static ValidatedNodeConsumer checkColumnTypeAction(
265267
if (runtime) {
266268
// We need to test that the expression fails at runtime.
267269
// Ironically, that means that it must succeed at prepare time.
268-
final String sql = buildQuery(sap.addCarets());
270+
final String sql = buildQuery(sap);
269271
Pair<SqlValidator, SqlNode> p = parseAndValidate(factory, sql);
270272
SqlNode n = p.right;
271273
assertNotNull(n);
272274
} else {
273-
StringAndPos sap1 = StringAndPos.of(buildQuery(sap.addCarets()));
275+
StringAndPos sap1 = StringAndPos.of(buildQuery(sap));
274276
checkQueryFails(factory, sap1, expectedError);
275277
}
276278
}
@@ -296,8 +298,23 @@ private static ValidatedNodeConsumer checkColumnTypeAction(
296298
}
297299
}
298300

299-
public static String buildQuery(String expression) {
300-
return "values (" + expression + ")";
301+
/**
302+
* Build a legal query from an expression with carets.
303+
*
304+
* <p>@deprecated Please use {@link AbstractSqlTester#buildQuery(StringAndPos)}
305+
*/
306+
@Deprecated
307+
public static String buildQuery(String expressionWithCarets) {
308+
StringAndPos sap = StringAndPos.of(expressionWithCarets);
309+
return buildQuery(sap);
310+
}
311+
312+
public static String buildQuery(StringAndPos sap) {
313+
return "values (" + sap.sql + ")";
314+
}
315+
316+
public static StringAndPos buildQueryWithPos(StringAndPos sap) {
317+
return StringAndPos.of("values (" + sap.addCarets() + ")");
301318
}
302319

303320
public static String buildQueryAgg(String expression) {
@@ -322,21 +339,24 @@ public static String buildQueryAgg(String expression) {
322339
* {@code CASE 1 WHEN 2 THEN 'a' ELSE NULL END} are left as is.
323340
*
324341
* @param factory Test factory
325-
* @param expression Scalar expression
342+
* @param expressionWithCarets Scalar expression that may contain carets
326343
* @return Query that evaluates a scalar expression
327344
*/
328-
protected String buildQuery2(SqlTestFactory factory, String expression) {
329-
if (expression.matches("(?i).*(percentile_(cont|disc)|convert|sort_array|cast)\\(.*")) {
345+
protected String buildQuery2(SqlTestFactory factory, String expressionWithCarets) {
346+
if (expressionWithCarets.matches(
347+
"(?i).*(percentile_(cont|disc)|convert|sort_array|cast)\\(.*")) {
330348
// PERCENTILE_CONT requires its argument to be a literal,
331349
// so converting its argument to a column will cause false errors.
332350
// Similarly, MSSQL-style CONVERT.
333-
return buildQuery(expression);
351+
return buildQuery(expressionWithCarets);
334352
}
335353
// "values (1 < 5)"
336354
// becomes
337355
// "select p0 < p1 from (values (1, 5)) as t(p0, p1)"
338356
SqlNode x;
339-
final String sql = "values (" + expression + ")";
357+
// Expression may contain escaped carets; unescape them.
358+
final StringAndPos sap = StringAndPos.of(expressionWithCarets);
359+
final String sql = "values (" + sap.sql + ")";
340360
try {
341361
x = parseQuery(factory, sql);
342362
} catch (SqlParseException e) {
@@ -447,12 +467,13 @@ private boolean isNull(SqlNode sqlNode) {
447467
}
448468

449469
@Override public void forEachQuery(SqlTestFactory factory,
450-
String expression, Consumer<String> consumer) {
470+
String expressionWithCarets, Consumer<String> consumer) {
451471
// Why not return a list? If there is a syntax error in the expression, the
452472
// consumer will discover it before we try to parse it to do substitutions
453473
// on the parse tree.
454-
consumer.accept("values (" + expression + ")");
455-
consumer.accept(buildQuery2(factory, expression));
474+
consumer.accept("values (" + expressionWithCarets + ")");
475+
final String query2 = buildQuery2(factory, expressionWithCarets);
476+
consumer.accept(SqlParserUtil.escapeCarets(query2));
456477
}
457478

458479
@Override public void assertConvertsTo(SqlTestFactory factory,

testkit/src/main/java/org/apache/calcite/sql/test/SqlTester.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,10 @@ enum VmName {
6262
* evaluate it, and calls a given action with each.
6363
*
6464
* @param factory Factory
65-
* @param expression Scalar expression
65+
* @param expressionWithCarets Scalar expression, possibly with carets
6666
* @param consumer Action to be called for each query
6767
*/
68-
void forEachQuery(SqlTestFactory factory, String expression,
68+
void forEachQuery(SqlTestFactory factory, String expressionWithCarets,
6969
Consumer<String> consumer);
7070

7171
/** Parses a query. */
@@ -165,14 +165,14 @@ default void check(SqlTestFactory factory,
165165
* and {@link ResultChecker} functors.
166166
*
167167
* @param factory Factory
168-
* @param query SQL query
168+
* @param queryWithCarets SQL query, possibly with carets
169169
* @param typeChecker Checks whether the result is the expected type
170170
* @param parameterChecker Checks whether the parameters are of expected
171171
* types
172172
* @param resultChecker Checks whether the result has the expected value
173173
*/
174174
void check(SqlTestFactory factory,
175-
String query,
175+
String queryWithCarets,
176176
TypeChecker typeChecker,
177177
ParameterChecker parameterChecker,
178178
ResultChecker resultChecker);

testkit/src/main/java/org/apache/calcite/test/SqlOperatorFixtureImpl.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,15 +156,16 @@ void forEachQueryValidateAndThen(StringAndPos expression,
156156

157157
@Override public void checkFails(StringAndPos sap, String expectedError,
158158
boolean runtime) {
159-
final String sql = "values (" + sap.addCarets() + ")";
160159
if (runtime) {
161160
// We need to test that the expression fails at runtime.
162161
// Ironically, that means that it must succeed at prepare time.
162+
final String sql = "values (" + sap.sql + ")";
163163
SqlValidator validator = factory.createValidator();
164164
SqlNode n = parseAndValidate(validator, sql);
165165
assertNotNull(n);
166166
tester.checkFails(factory, sap, expectedError, runtime);
167167
} else {
168+
final String sql = "values (" + sap.addCarets() + ")";
168169
checkQueryFails(StringAndPos.of(sql),
169170
expectedError);
170171
}

0 commit comments

Comments
 (0)