Skip to content
Merged
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
2 changes: 1 addition & 1 deletion dingo-calcite/src/main/codegen/templates/Parser.jj
Original file line number Diff line number Diff line change
Expand Up @@ -6861,7 +6861,7 @@ SqlNode BuiltinFunctionCall() :
]
<RPAREN> {
SqlIdentifier charFunName = new SqlIdentifier(
charUsingCharset ? (charUsingBinary ? "char_binary" : "char_charset") : "char", s.end(this));
charUsingCharset ? (charUsingBinary ? "char_binary" : "char_charset") : "char_default", s.end(this));
SqlNode sqlNode = createCall(charFunName, s.end(this), SqlFunctionCategory.STRING, null, args);
sqlNode.putAlias("aliasName", alias);
return sqlNode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,12 @@
import org.apache.calcite.sql.SqlSelect;
import org.apache.calcite.sql.SqlSelectKeyword;
import org.apache.calcite.sql.SqlUtil;
import org.apache.calcite.sql.fun.SqlCase;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.apache.calcite.sql.parser.SqlParserUtil;
import org.apache.calcite.sql.type.BasicSqlType;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.sql.type.SqlTypeUtil;
import org.apache.calcite.sql.validate.SqlValidator;
import org.apache.calcite.sql.validate.SqlValidatorScope;
import org.apache.calcite.sql.validate.SqlValidatorUtil;
Expand Down Expand Up @@ -141,6 +143,46 @@ public DingoSqlToRelConverter(

@Override
protected @Nullable RexNode convertExtendedExpression(@NonNull SqlNode node, Blackboard bb) {
if (node.getKind() == SqlKind.CASE) {
SqlCase caseCall = (SqlCase) node;
RelDataType resultType = bb.getValidator().getValidatedNodeType(caseCall);
if (resultType.getCharset() != null) {
boolean mixed = false;
for (SqlNode branch : caseCall.getThenOperands()) {
if (!SqlUtil.isNullLiteral(branch, false)) {
RelDataType branchType = bb.getValidator().getValidatedNodeType(branch);
mixed |= SqlTypeUtil.isCharacter(branchType)
&& !resultType.getCharset().equals(branchType.getCharset());
}
}
SqlNode otherwise = caseCall.getElseOperand();
if (otherwise != null && !SqlUtil.isNullLiteral(otherwise, false)) {
RelDataType branchType = bb.getValidator().getValidatedNodeType(otherwise);
mixed |= SqlTypeUtil.isCharacter(branchType)
&& !resultType.getCharset().equals(branchType.getCharset());
}
if (mixed) {
// Calcite's convertCase repeats the SQL type-name-only fast path and
// casts every Rex branch to the first branch's charset.
RexBuilder rb = bb.getRexBuilder();
List<RexNode> operands = new ArrayList<>(caseCall.getWhenOperands().size() * 2 + 1);
for (int i = 0; i < caseCall.getWhenOperands().size(); i++) {
SqlNode when = caseCall.getWhenOperands().get(i);
operands.add(SqlUtil.isNullLiteral(when, false)
? rb.makeNullLiteral(rb.getTypeFactory().createSqlType(SqlTypeName.BOOLEAN))
: bb.convertExpression(when));
SqlNode branch = caseCall.getThenOperands().get(i);
operands.add(SqlUtil.isNullLiteral(branch, false)
? rb.makeNullLiteral(resultType)
: rb.ensureType(resultType, bb.convertExpression(branch), false));
}
operands.add(otherwise == null || SqlUtil.isNullLiteral(otherwise, false)
? rb.makeNullLiteral(resultType)
: rb.ensureType(resultType, bb.convertExpression(otherwise), false));
return rb.makeCall(resultType, SqlStdOperatorTable.CASE, operands);
}
}
}
// MySQL dialect
if (node.getKind() == SqlKind.OTHER_FUNCTION) {
SqlOperator operator = ((SqlCall) node).getOperator();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@
import org.checkerframework.checker.nullness.qual.Nullable;

import java.math.BigDecimal;
import java.nio.charset.Charset;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.EnumSet;
import java.util.List;
Expand Down Expand Up @@ -117,6 +119,8 @@ public static TypeCoercion createTypeCoercion(RelDataTypeFactory typeFactory,
) {
super(
SqlOperatorTables.chain(
// Override only CONCAT: Calcite's version drops its operand charset.
SqlOperatorTables.of(DingoOperatorTable.instance().concatFunction()),
SqlStdOperatorTable.instance(),
SqlLibraryOperatorTableFactory.INSTANCE
.getOperatorTable(SqlLibrary.MYSQL),
Expand All @@ -137,6 +141,87 @@ public static TypeCoercion createTypeCoercion(RelDataTypeFactory typeFactory,
@Override
public void validateCall(SqlCall call, SqlValidatorScope scope) {
super.validateCall(call, scope);
if (call.getKind() != SqlKind.CASE) {
return;
}
// Calcite's CASE fast path compares only SQL type names, so a mixed-charset
// VARCHAR result inherits the first branch's charset without merging them.
org.apache.calcite.sql.fun.SqlCase caseCall = (org.apache.calcite.sql.fun.SqlCase) call;
List<RelDataType> branches = new ArrayList<>();
boolean nullable = false;
for (SqlNode branch : caseCall.getThenOperands()) {
if (SqlUtil.isNullLiteral(branch, false)) {
nullable = true;
} else {
RelDataType branchType = deriveType(scope, branch);
branches.add(branchType);
nullable |= branchType.isNullable();
}
}
SqlNode otherwise = caseCall.getElseOperand();
nullable |= otherwise == null || SqlUtil.isNullLiteral(otherwise, false);
if (otherwise != null && !SqlUtil.isNullLiteral(otherwise, false)) {
RelDataType branchType = deriveType(scope, otherwise);
branches.add(branchType);
nullable |= branchType.isNullable();
}
if (branches.size() < 2) {
return;
}
Charset charset = branches.get(0).getCharset();
if (charset == null) {
return;
}
boolean mixed = false;
for (RelDataType branch : branches) {
if (!SqlTypeUtil.isCharacter(branch) || branch.getCharset() == null) {
return;
}
mixed |= !charset.equals(branch.getCharset());
}
if (mixed) {
RelDataType merged = typeFactory.leastRestrictive(branches);
if (merged != null) {
setValidatedNodeType(call, typeFactory.createTypeWithNullability(merged, nullable));
}
}
}

@Override
protected void validateSelect(SqlSelect select, RelDataType targetRowType, boolean skipMeasure) {
super.validateSelect(select, targetRowType, skipMeasure);
SqlValidatorNamespace namespace = getNamespace(select);
RelDataType rowType = namespace.getRowType();
SqlNodeList selectList = select.getSelectList();
if (selectList.size() != rowType.getFieldCount()) {
return;
}
RelDataTypeFactory.Builder corrected = null;
for (int i = 0; i < selectList.size(); i++) {
SqlNode expression = selectList.get(i);
if (expression.getKind() == SqlKind.AS) {
expression = ((SqlCall) expression).operand(0);
}
RelDataTypeField field = rowType.getFieldList().get(i);
RelDataType validated = expression.getKind() == SqlKind.CASE
? getValidatedNodeTypeIfKnown(expression) : null;
if (validated != null && !validated.equals(field.getType())) {
if (corrected == null) {
corrected = typeFactory.builder();
for (int j = 0; j < i; j++) {
corrected.add(rowType.getFieldList().get(j));
}
}
corrected.add(field.getName(), validated);
} else if (corrected != null) {
corrected.add(field);
}
}
if (corrected != null) {
RelDataType result = corrected.build();
namespace.setType(result);
setValidatedNodeType(select, result);
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,20 @@
import lombok.EqualsAndHashCode;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.sql.SqlCall;
import org.apache.calcite.sql.SqlFunction;
import org.apache.calcite.sql.SqlCollation;
import org.apache.calcite.sql.SqlFunctionCategory;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlOperator;
import org.apache.calcite.sql.SqlOperatorBinding;
import org.apache.calcite.sql.type.SqlOperandTypeChecker;
import org.apache.calcite.sql.type.SqlOperandTypeInference;
import org.apache.calcite.sql.type.SqlReturnTypeInference;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.sql.validate.SqlValidator;
import org.apache.calcite.sql.validate.SqlValidatorScope;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

import java.nio.charset.Charset;

@EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
public class DingoConcatFunction extends DingoSqlFunction {
public DingoConcatFunction(
Expand All @@ -44,7 +44,7 @@ public DingoConcatFunction(
) {
super(
name,
returnTypeInference,
returnTypeInference == null ? null : binding -> inferCharsetReturnType(binding, returnTypeInference),
operandTypeInference,
operandTypeChecker,
category
Expand All @@ -63,9 +63,69 @@ public void validateCall(
super.validateCall(call, validator, scope, operandScope);
}

private static RelDataType inferCharsetReturnType(
SqlOperatorBinding binding, SqlReturnTypeInference returnTypeInference
) {
RelDataType result = returnTypeInference.inferReturnType(binding);
RelDataType chosen = null;
int chosenIndex = -1;
for (int i = 0; i < binding.getOperandCount(); i++) {
RelDataType operand = binding.getOperandType(i);
if (operand.getCharset() == null || operand.getCollation() == null) {
continue;
}
if (chosen != null
&& !chosen.getCharset().equals(operand.getCharset())
&& chosen.getCollation().getCoercibility() == operand.getCollation().getCoercibility()) {
Charset left = chosen.getCharset();
Charset right = operand.getCharset();
boolean leftLiteral = binding.isOperandLiteral(chosenIndex, false);
boolean rightLiteral = binding.isOperandLiteral(i, false);
if (leftLiteral != rightLiteral) {
String literal = binding.getOperandLiteralValue(
leftLiteral ? chosenIndex : i, String.class
);
Charset target = leftLiteral ? right : left;
if (literal != null && target.newEncoder().canEncode(literal)) {
// Literals are weaker than character expressions when conversion is lossless.
if (leftLiteral) {
chosen = operand;
chosenIndex = i;
}
continue;
}
}
if (isUnicode(left) != isUnicode(right)) {
// At equal coercibility MySQL promotes non-Unicode text to Unicode.
if (isUnicode(right)) {
chosen = operand;
chosenIndex = i;
}
} else if (!left.contains(right)) {
if (right.contains(left)) {
chosen = operand;
chosenIndex = i;
} else {
throw new IllegalArgumentException("Incompatible CONCAT character sets: "
+ left.name() + " and " + right.name());
}
}
continue;
}
if (chosen == null || SqlCollation.getCoercibilityDyadicOperator(
chosen.getCollation(), operand.getCollation()
) == operand.getCollation()) {
chosen = operand;
chosenIndex = i;
}
}
return chosen == null ? result : binding.getTypeFactory().createTypeWithCharsetAndCollation(
result, chosen.getCharset(), chosen.getCollation()
);
}

public RelDataType inferReturnType(SqlOperatorBinding opBinding) {
return opBinding.getTypeFactory().createSqlType(SqlTypeName.VARCHAR);
private static boolean isUnicode(Charset charset) {
return charset.name().startsWith("UTF-");
}

}
Loading
Loading