[
https://issues.apache.org/jira/browse/CALCITE-7829?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18120753#comment-18120753
]
Tomasz Gawęda commented on CALCITE-7829:
----------------------------------------
{code:java}
package reproducer;
import org.apache.calcite.adapter.java.AbstractQueryableTable;
import org.apache.calcite.jdbc.CalciteConnection;
import org.apache.calcite.linq4j.Enumerator;
import org.apache.calcite.linq4j.Linq4j;
import org.apache.calcite.linq4j.QueryProvider;
import org.apache.calcite.linq4j.Queryable;
import org.apache.calcite.plan.RelOptCluster;
import org.apache.calcite.plan.RelOptTable;
import org.apache.calcite.plan.RelOptUtil;
import org.apache.calcite.prepare.Prepare;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.core.Project;
import org.apache.calcite.rel.core.TableModify;
import org.apache.calcite.rel.core.Values;
import org.apache.calcite.rel.logical.LogicalTableModify;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.rex.RexCall;
import org.apache.calcite.rex.RexInputRef;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.schema.ModifiableTable;
import org.apache.calcite.schema.SchemaPlus;
import org.apache.calcite.schema.impl.AbstractTableQueryable;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.sql2rel.SqlToRelConverter;
import org.apache.calcite.tools.Frameworks;
import org.apache.calcite.tools.Planner;
import org.junit.jupiter.api.Test;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
class TimeAssignmentTest {
private static final String SQL =
"INSERT INTO T (ID, PAYLOAD) VALUES (0, CAST('01:42:00' AS TIME))";
@Test
void insertTimeIntoObjectShouldPreserveTemporalType() throws Exception {
ObjectTable table = new ObjectTable();
try (Connection connection =
DriverManager.getConnection("jdbc:calcite:");
Statement statement = connection.createStatement()) {
CalciteConnection calcite =
connection.unwrap(CalciteConnection.class);
SchemaPlus schema = calcite.getRootSchema();
schema.add("T", table);
assertEquals(Object.class, calcite.getTypeFactory().getJavaClass(
table.getRowType(calcite.getTypeFactory()).getFieldList().get(1).getType()));
// Isolate SQL-to-rel conversion, before any physical optimizer
rules.
RexNode withoutSimplification = assignmentSource(convert(schema,
false));
RexNode withSimplification = assignmentSource(convert(schema,
true));
System.out.println("Without project simplification: " +
withoutSimplification
+ " : " + withoutSimplification.getType());
System.out.println("With project simplification: " +
withSimplification
+ " : " + withSimplification.getType());
assertEquals(SqlTypeName.TIME,
withoutSimplification.getType().getSqlTypeName());
// Execute the same INSERT and inspect storage, without a JDBC
readback.
assertEquals(1, statement.executeUpdate(SQL));
assertEquals(1, table.rows.size());
assertEquals(0, table.rows.get(0)[0]);
Object stored = table.rows.get(0)[1];
assertNotNull(stored);
System.out.printf("Stored value=%s, class=%s%n", stored,
stored.getClass().getName());
assertEquals(6_120_000L, assertInstanceOf(Number.class,
stored).longValue());
// ANY must not erase the temporal type of the expression assigned
to it.
assertAll(
() -> assertEquals(SqlTypeName.TIME,
withSimplification.getType().getSqlTypeName(),
() -> "SQL-to-rel conversion replaced TIME with " +
withSimplification),
// Stock Enumerable's TIME representation is Integer, not
LocalTime.
() -> assertEquals(Integer.class, stored.getClass(),
"Stored temporal representation"));
}
}
private static RelNode convert(SchemaPlus schema, boolean simplify) throws
Exception {
try (Planner planner =
Frameworks.getPlanner(Frameworks.newConfigBuilder()
.defaultSchema(schema)
.sqlToRelConverterConfig(SqlToRelConverter.config()
.withRelBuilderConfigTransform(config ->
config.withSimplify(simplify)))
.build())) {
SqlNode validated = planner.validate(planner.parse(SQL));
RelNode plan = planner.rel(validated).rel;
System.out.println("SQL-to-rel plan (simplify=" + simplify + "):\n"
+ RelOptUtil.toString(plan));
return plan;
}
}
private static RexNode assignmentSource(RelNode plan) {
TableModify insert = assertInstanceOf(TableModify.class, plan);
RelNode input = insert.getInput();
int column = 1;
RexNode value;
while (true) {
if (input instanceof Project) {
value = ((Project) input).getProjects().get(column);
if (value instanceof RexInputRef) {
column = ((RexInputRef) value).getIndex();
input = ((Project) input).getInput();
continue;
}
} else {
value = assertInstanceOf(Values.class,
input).getTuples().get(0).get(column);
}
break;
}
// Unwrap only assignment casts to ANY, retaining the source SQL type.
while (value.getKind() == SqlKind.CAST &&
value.getType().getSqlTypeName() == SqlTypeName.ANY) {
value = ((RexCall) value).getOperands().get(0);
}
return value;
}
/**
* A table that stores exactly the rows supplied by Calcite's INSERT
operator.
*/
public static class ObjectTable extends AbstractQueryableTable implements
ModifiableTable {
final List<Object[]> rows = new ArrayList<>();
public ObjectTable() {
super(Object[].class);
}
@Override
public RelDataType getRowType(RelDataTypeFactory typeFactory) {
return typeFactory.builder().add("ID", SqlTypeName.INTEGER)
// Calcite's built-in SQL type for an arbitrary
Java Object is ANY.
.add("PAYLOAD",
SqlTypeName.ANY).nullable(true).build();
}
@Override
public Collection<Object[]> getModifiableCollection() {
return rows;
}
@Override
public TableModify toModificationRel(RelOptCluster cluster, RelOptTable
table, Prepare.CatalogReader catalogReader,
RelNode child,
TableModify.Operation operation, List<String> updateColumnList,
List<RexNode>
sourceExpressionList, boolean flattened) {
return LogicalTableModify.create(table, catalogReader, child,
operation, updateColumnList, sourceExpressionList,
flattened);
}
@Override
public <T> Queryable<T> asQueryable(QueryProvider queryProvider,
SchemaPlus schema, String tableName) {
return new AbstractTableQueryable<T>(queryProvider, schema, this,
tableName) {
@Override
@SuppressWarnings("unchecked")
public Enumerator<T> enumerator() {
return (Enumerator<T>) Linq4j.enumerator(rows);
}
};
}
}
}
{code}
> RexSimplify rewrites Date to int
> --------------------------------
>
> Key: CALCITE-7829
> URL: https://issues.apache.org/jira/browse/CALCITE-7829
> Project: Calcite
> Issue Type: Bug
> Reporter: Tomasz Gawęda
> Priority: Major
> Attachments: ObjectTable.java, TimeAssignmentTest.java,
> calcite-7829-reproducer.zip
>
>
> In Hazelcast we have a test called RowAssignmentTypeCoercionTest. When trying
> to upgrade to Calcite 1.42 we've noticed, that some cases fail (despite
> working previously).
>
> For example, queries:
> {code:sql}
> CREATE MAPPING m type IMap OPTIONS('keyFormat'='int', 'valueFormat'='java',
> 'valueJavaClass'='com.hazelcast.jet.sql.impl.support.expressions.ExpressionValue$ObjectVal')
> SINK INTO m VALUES(0, cast('01:42:00' as time), 0)
> {code}
> translate to:
> {code:java}
> LogicalTableSink(table=[[hazelcast, public, m[projects=[$0, $1, $2, $3]]]],
> operation=[INSERT], flattened=[false])
> LogicalCalc(expr#0=[\{inputs}], expr#1=[0], expr#2=[6120000],
> expr#3=[CAST($t2):OBJECT], expr#4=[null:OBJECT], __key=[$t1], field1=[$t3],
> key=[$t1], this=[$t4])
> LogicalValues(tuples=[[\{ 0 }]])
> {code}
> As you can see, `01:42:00` was translated to 6120000. This integer value is
> then written to a field of type OBJECT.
> Previously, in 1.39 dates and times landed in "object" field with their real
> types, not converted to integer, which makes me believe, that RexSimplify
> tries. to simplify a bit too much.
> (cast(cast('01:42:00' as time) as object) = time written to object column,
> not integer value.
> More from trace logs from this test:
> {code:java}
> 16:13:53,549 TRACE |test_insertValues[TestParams{id=2014, srcType=TIME,
> targetType=OBJECT, srcValue=01:42}]| - [parser] test_insertValues - After
> unconditional rewrite: SINK INTO `m`
> VALUES ROW(0, CAST('01:42:00' AS TIME), 0)
> 16:13:53,576 TRACE |test_insertValues[TestParams{id=2014, srcType=TIME,
> targetType=OBJECT, srcValue=01:42}]| - [parser] test_insertValues - After
> validation: SINK INTO `hazelcast`.`public`.`m` (`__key`, `field1`, `key`)
> VALUES ROW(CAST(0 AS INTEGER), CAST('01:42:00' AS TIME), CAST(0 AS INTEGER))
> 16:13:53,759 DEBUG |test_insertValues[TestParams{id=2014, srcType=TIME,
> targetType=OBJECT, srcValue=01:42}]| - [sql2rel] test_insertValues - Plan
> after converting SqlNode to RelNode
> LogicalTableSink(table=[[hazelcast, public, m[projects=[$0, $1, $2, $3]]]],
> operation=[INSERT], flattened=[false])
> LogicalProject(__key=[0], field1=[CAST(6120000):OBJECT], key=[0],
> this=[null:OBJECT])
> LogicalValues(tuples=[[{ 0 }]])
> {code}
>
--
This message was sent by Atlassian Jira
(v8.20.10#820010)