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
23 changes: 19 additions & 4 deletions babel/src/main/codegen/includes/parserImpls.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -197,17 +197,32 @@ SqlCreate SqlCreateTable(Span s, boolean replace) :
void InfixCast(List<Object> list, ExprContext exprContext, Span s) :
{
final SqlDataTypeSpec dt;
SqlNode e, p;
}
{
<INFIX_CAST> {
checkNonQueryExpression(exprContext);
}
dt = DataType() {
list.add(
new SqlParserUtil.ToTreeListItem(SqlLibraryOperators.INFIX_CAST,
s.pos()));
list.add(dt);
SqlNode leftOperand = SqlParserUtil.toTree(list);
list.clear();
SqlNode castNode = SqlLibraryOperators.INFIX_CAST.createCall(
s.pos(), leftOperand, dt);
list.add(castNode);
}
( <LBRACKET>
e = Expression(ExprContext.ACCEPT_SUB_QUERY)
<RBRACKET> {
SqlNode current = (SqlNode) list.remove(list.size() - 1);
list.add(SqlStdOperatorTable.ITEM.createCall(getPos(), current, e));
}
|
<DOT>
p = SimpleIdentifier() {
SqlNode current = (SqlNode) list.remove(list.size() - 1);
list.add(SqlStdOperatorTable.DOT.createCall(getPos(), current, p));
}
)*
}

/** Parses the NULL-safe "<=>" equal operator used in MySQL. */
Expand Down
57 changes: 50 additions & 7 deletions babel/src/test/java/org/apache/calcite/test/BabelParserTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
* limitations under the License.
*/
package org.apache.calcite.test;
import org.apache.calcite.sql.SqlBasicCall;
import org.apache.calcite.sql.SqlDialect;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlSelect;
import org.apache.calcite.sql.dialect.MysqlSqlDialect;
import org.apache.calcite.sql.dialect.PostgresqlSqlDialect;
import org.apache.calcite.sql.dialect.SparkSqlDialect;
Expand Down Expand Up @@ -290,11 +294,50 @@ class BabelParserTest extends SqlParserTest {
final String sql = "select -('12' || '.34')::VARCHAR(30)::INTEGER as x\n"
+ "from t";
final String expected = ""
+ "SELECT (- ('12' || '.34') :: VARCHAR(30) :: INTEGER) AS `X`\n"
+ "SELECT (- ('12' || '.34')) :: VARCHAR(30) :: INTEGER AS `X`\n"
+ "FROM `T`";
sql(sql).ok(expected);
}

/**
* Test case for
* <a href="https://issues.apache.org/jira/browse/CALCITE-7475">[CALCITE-7475]
* Babel parser allows postfix access after PostgreSQL-style {@code ::} infix cast</a>.
*
* <p>Verifies that PostgreSQL-style infix cast ({@code ::}) correctly binds
* tighter than postfix access operators such as array indexing ({@code []})
* and field access ({@code .}).
*/
@Test void testParseInfixCastWithPostfixAccess() {
final String sql = "select 'test'::varchar array[1].field";

// 1. Verify the unparsed SQL string.
// Calcite's unparser adds parentheses to reflect the correct AST precedence.
final String expected = "SELECT ('test' :: VARCHAR ARRAY[1].`FIELD`)";
sql(sql).ok(expected);

// 2. Verify the internal AST structure.
SqlNode node = sql(sql).node();
SqlSelect select = (SqlSelect) node;
SqlNode firstItem = select.getSelectList().get(0);

// The top-level operator should be DOT (.)
assertThat(firstItem.getKind(), is(SqlKind.DOT));
SqlBasicCall dotCall = (SqlBasicCall) firstItem;

// The left operand of DOT should be ITEM ([])
SqlNode dotLeft = dotCall.operand(0);
assertThat(((SqlBasicCall) dotLeft).getOperator().getName(), is("ITEM"));

// The left operand of ITEM should be the INFIX_CAST (::)
SqlNode itemLeft = ((SqlBasicCall) dotLeft).operand(0);
assertThat(itemLeft.getKind(), is(SqlKind.CAST));

// The right operand of CAST should be exactly 'VARCHAR ARRAY' without any subscripts.
SqlNode castRight = ((SqlBasicCall) itemLeft).operand(1);
assertThat(castRight, hasToString("VARCHAR ARRAY"));
}

private void checkParseInfixCast(String sqlType) {
String sql = "SELECT x::" + sqlType + " FROM (VALUES (1, 2)) as tbl(x,y)";
String expected = "SELECT `X` :: " + sqlType.toUpperCase(Locale.ROOT) + "\n"
Expand All @@ -313,9 +356,9 @@ private void checkParseInfixCast(String sqlType) {
// Without parentheses, trailing postfixes after :: are consumed as part of
// the type.
sql("select v::varchar array[1].field from t")
.ok("SELECT `V` :: (VARCHAR ARRAY[1].`FIELD`)\nFROM `T`");
.ok("SELECT (`V` :: VARCHAR ARRAY[1].`FIELD`)\nFROM `T`");
f.sql("select v:field::varchar array[1].field2 from t")
.ok("SELECT (`V`:`field`) :: (VARCHAR ARRAY[1].`FIELD2`)\nFROM `T`");
.ok("SELECT ((`V`:`field`) :: VARCHAR ARRAY[1].`FIELD2`)\nFROM `T`");

// Parenthesizing the cast lets the same postfixes apply to the cast
// result instead.
Expand All @@ -327,10 +370,10 @@ private void checkParseInfixCast(String sqlType) {
// Postfix access is also accepted directly after :: in ordinary field/item
// chains.
sql("select v.field::integer,\n"
+ " arr[1].field::varchar,\n"
+ " v.field.field2::integer,\n"
+ " v.field[2]::integer\n"
+ "from t")
+ " arr[1].field::varchar,\n"
+ " v.field.field2::integer,\n"
+ " v.field[2]::integer\n"
+ "from t")
.ok("SELECT `V`.`FIELD` :: INTEGER,"
+ " (`ARR`[1].`FIELD`) :: VARCHAR,"
+ " `V`.`FIELD`.`FIELD2` :: INTEGER,"
Expand Down
14 changes: 14 additions & 0 deletions core/src/main/java/org/apache/calcite/sql/fun/SqlCastOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@

import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.sql.SqlBinaryOperator;
import org.apache.calcite.sql.SqlCall;
import org.apache.calcite.sql.SqlCallBinding;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlOperandCountRange;
import org.apache.calcite.sql.SqlOperatorBinding;
import org.apache.calcite.sql.SqlWriter;
import org.apache.calcite.sql.type.InferTypes;
import org.apache.calcite.sql.validate.SqlMonotonicity;

Expand All @@ -46,6 +48,18 @@ class SqlCastOperator extends SqlBinaryOperator {
super("::", SqlKind.CAST, 94, true, null, InferTypes.FIRST_KNOWN, null);
}

@Override public void unparse(SqlWriter writer, SqlCall call, int leftPrec, int rightPrec) {
final boolean needParens = leftPrec > getLeftPrec(); // true when ITEM/DOT is parent
if (needParens) {
writer.sep("(");
}
call.operand(0).unparse(writer, leftPrec, getLeftPrec());
writer.keyword("::");
call.operand(1).unparse(writer, getRightPrec(), rightPrec);
if (needParens) {
writer.sep(")");
}
}
@Override public RelDataType inferReturnType(
SqlOperatorBinding opBinding) {
return SqlStdOperatorTable.CAST.inferReturnType(opBinding);
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ org.checkerframework.version=0.5.16
com.github.autostyle.version=3.2
com.github.johnrengelman.shadow.version=5.1.0
com.github.spotbugs.version=2.0.0
com.github.vlsi.vlsi-release-plugins.version=3.0.1
com.github.vlsi.vlsi-release-plugins.version=3.0.2
com.google.protobuf.version=0.8.10
de.thetaphi.forbiddenapis.version=3.10
jacoco.version=0.8.14
Expand Down
Loading