Skip to content

Commit 096de8a

Browse files
jogroganCopilot
andauthored
Fix timestamp precision (#255)
* Support timestamp-millis and timestamp-micros independently Timestamp columns surfaced as TIMESTAMP(0) because Avro timestamp logical types were mapped to TIMESTAMP with unspecified precision, and Calcite's default RelDataTypeSystem caps datetime precision at 3 (milliseconds), so micro/nanosecond precision could never survive. - Add HoptimatorTypeSystem, which raises the datetime precision ceiling from 3 to 9 (matching Flink SQL's TIMESTAMP(0..9)). - Map Avro timestamp-millis to TIMESTAMP(3) and timestamp-micros to TIMESTAMP(6) in AvroConverter, and choose the logical type by precision on the reverse path. - Route the JDBC planner/validator through HoptimatorTypeSystem via the Calcite typeSystem connection property, and repoint the standalone type factories in the Avro conversion path. - Carry datetime precision through generated DDL in ScriptImplementor (e.g. TIMESTAMP(6)); a bare TIMESTAMP stays precision-less. Adds unit coverage (type system, Avro mapping both directions, DDL rendering), an end-to-end DDL Quidem test, and an integration script case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Normalize tests onto HoptimatorTypeSystem Swap RelDataTypeSystem.DEFAULT for HoptimatorTypeSystem.INSTANCE in test type factories so tests mirror the type system used in production. Raising the datetime precision ceiling does not affect existing assertions. HoptimatorTypeSystemTest intentionally keeps RelDataTypeSystem.DEFAULT, since it asserts the default's clamping behavior as the premise for the custom type system. The delete-marked planner and catalog modules are left untouched. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Normalize remaining type factories onto HoptimatorTypeSystem Route the remaining standalone type factories through HoptimatorTypeSystem so nothing silently falls back to Calcite's default (which caps datetime precision at 3). - Main code: the compatibility validators (AvroTableValidator, Backward/ ForwardCompatibilityValidator) and RemoteTable/ArrayTable built row types via new JavaTypeFactoryImpl() (default type system). With the default, micro/nanosecond timestamps collapse to TIMESTAMP(3), so the compat validators could not distinguish millis from micros. They now use HoptimatorTypeSystem.INSTANCE. - Tests: normalize the remaining JavaTypeFactoryImpl() and RelDataTypeSystem.DEFAULT usages (demodb, planner, catalog, jdbc), adding a test-scoped hoptimator-avro dependency where needed. HoptimatorTypeSystemTest intentionally still references RelDataTypeSystem.DEFAULT, since it exists to contrast the default's clamping behavior against the custom type system. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Preserve JDBC temporal fractional-seconds precision (MySQL) A JDBC source's temporal columns were mapped using only the SQL type name, so MySqlTable produced TIMESTAMP(0) for a TIMESTAMP(3)/(6) column — the fractional-seconds precision JDBC exposes via DECIMAL_DIGITS was dropped at the JDBC layer. Downstream, the planner's row type showed TIMESTAMP(0) even though the source carried milli/microsecond precision. Read DECIMAL_DIGITS for temporal types (TIME/TIMESTAMP and their local-time-zone variants) and build the column with that precision, so TIMESTAMP(3)/(6) survive. Adds MySqlTableTest cases for millis/micros/time/no-fraction, and an integration script (mysql-timestamp-precision.id) with an `events` source table whose TIMESTAMP(3)/(6) columns are asserted end-to-end via !describe. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Preserve temporal precision across the JDBC federation HoptimatorJdbcTable derived its row type from the wrapped Calcite JdbcTable, whose type comes from DatabaseMetaData. That round-trip drops temporal fractional-seconds precision: CalciteMetaImpl reports a TIMESTAMP's precision in COLUMN_SIZE while JdbcSchema#getRelDataType reads it from DECIMAL_DIGITS (left null) and builds the proto type with RelDataTypeSystem.DEFAULT (capped at millis). The net effect is TIMESTAMP(3)/(6) collapsing to TIMESTAMP(0) in the planner even when the source carries milli/microsecond precision. Calcite exposes no usable hook here (getRelDataType is package-private and its proto is memoized; the THREAD_METADATA/Foo hook is experimental "do not use" and only lists tables). So prefer the upstream table's own row type — the same source of truth already used for the Avro key/value schemas — which preserves precision, falling back to the JDBC-metadata path for non-Calcite upstreams. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Report temporal precision in DECIMAL_DIGITS across the JDBC federation Replaces the upstream-table bypass with a native Calcite metadata fix. The precision was being dropped inside Calcite's own JDBC federation: its metadata producer (CalciteMetaImpl.columns) reports a TIMESTAMP/TIME column's precision in COLUMN_SIZE and leaves DECIMAL_DIGITS null, but its reader (JdbcSchema.getRelDataType) reads temporal precision from DECIMAL_DIGITS. So the two halves disagree and TIMESTAMP(3) surfaces as TIMESTAMP(0). Fix it the way an external JDBC driver would — populate DECIMAL_DIGITS — via Calcite's native metaColumnFactory connection property (honored by Driver. createMeta). HoptimatorMetaColumnFactory mirrors COLUMN_SIZE into DECIMAL_DIGITS for temporal types, and CalciteDriver sets the property so every adapter connection reports it. No federation bypass, no metadata round-trip change. This restores millisecond precision across the federation; precision beyond millis is still clamped by JdbcSchema.getRelDataType's hardcoded RelDataTypeSystem.DEFAULT, which no connection knob can override. Reverts the HoptimatorJdbcTable.getRowType bypass from the previous commit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove flawed timestamp-precision integration scenarios The mysql-timestamp-precision and kafka ts-precision integration scenarios asserted micro/nanosecond precision (TIMESTAMP(6)/(9)) surviving a source read, but that is unattainable through Calcite's JDBC federation: JdbcSchema. getRelDataType builds its proto type with RelDataTypeSystem.DEFAULT, which caps datetime precision at milliseconds. So those assertions could never hold regardless of the metadata fix. The MySQL scenario additionally depended on MySQL Connector/J reporting DECIMAL_DIGITS for a TIMESTAMP(3) column, which is a separate, driver-specific variable from the Calcite-over-Calcite federation fix. Removes both scenarios and the associated events table. The millisecond fix remains covered by unit tests (HoptimatorMetaColumnFactoryTest, MySqlTableTest). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix redundant check * Fix MySQL precision/scale handling + support in quidem * fix up tests --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e9ab47c commit 096de8a

76 files changed

Lines changed: 1106 additions & 385 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎deploy/docker/mysql/init.sql‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ CREATE TABLE IF NOT EXISTS orders (
2020
quantity INT NOT NULL,
2121
price DECIMAL(10, 2) NOT NULL,
2222
order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
23+
order_date_ms TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3),
2324
status VARCHAR(20) DEFAULT 'pending',
2425
FOREIGN KEY (user_id) REFERENCES users(user_id)
2526
);

‎hoptimator-avro/src/main/java/com/linkedin/hoptimator/avro/AvroConverter.java‎

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
package com.linkedin.hoptimator.avro;
22

3+
import org.apache.avro.LogicalType;
34
import org.apache.avro.LogicalTypes;
45
import org.apache.avro.Schema;
56
import org.apache.calcite.rel.type.RelDataType;
67
import org.apache.calcite.rel.type.RelDataTypeFactory;
78
import org.apache.calcite.rel.type.RelDataTypeField;
89
import org.apache.calcite.rel.type.RelDataTypeImpl;
9-
import org.apache.calcite.rel.type.RelDataTypeSystem;
1010
import org.apache.calcite.rel.type.RelProtoDataType;
1111
import org.apache.calcite.sql.type.SqlTypeFactoryImpl;
1212
import org.apache.calcite.sql.type.SqlTypeName;
@@ -27,6 +27,10 @@ public final class AvroConverter {
2727
private static final String KEY_OPTION = "key.fields";
2828
private static final String KEY_PREFIX_OPTION = "key.fields-prefix";
2929

30+
// Fractional-second precisions for Avro's timestamp logical types.
31+
private static final int MILLIS_PRECISION = 3;
32+
private static final int MICROS_PRECISION = 6;
33+
3034
private AvroConverter() {
3135
}
3236

@@ -75,8 +79,12 @@ public static Schema avro(String namespace, String name, RelDataType dataType) {
7579
return createAvroSchemaWithNullability(
7680
LogicalTypes.date().addToSchema(Schema.create(Schema.Type.INT)), dataType.isNullable());
7781
case TIMESTAMP:
82+
// Avro distinguishes millisecond- and microsecond-precision timestamps. Map any
83+
// precision beyond millis (3) to timestamp-micros, otherwise timestamp-millis.
84+
LogicalType timestampLogicalType = dataType.getPrecision() > MILLIS_PRECISION
85+
? LogicalTypes.timestampMicros() : LogicalTypes.timestampMillis();
7886
return createAvroSchemaWithNullability(
79-
LogicalTypes.timestampMillis().addToSchema(Schema.create(Schema.Type.LONG)), dataType.isNullable());
87+
timestampLogicalType.addToSchema(Schema.create(Schema.Type.LONG)), dataType.isNullable());
8088
case BOOLEAN:
8189
return createAvroTypeWithNullability(Schema.Type.BOOLEAN, dataType.isNullable());
8290
case ARRAY:
@@ -101,7 +109,7 @@ public static Schema avro(String namespace, String name, RelDataType dataType) {
101109
}
102110

103111
public static Schema avro(String namespace, String name, RelProtoDataType relProtoDataType) {
104-
RelDataTypeFactory factory = new SqlTypeFactoryImpl(RelDataTypeSystem.DEFAULT);
112+
RelDataTypeFactory factory = new SqlTypeFactoryImpl(HoptimatorTypeSystem.INSTANCE);
105113
return avro(namespace, name, relProtoDataType.apply(factory));
106114
}
107115

@@ -118,7 +126,7 @@ public static Pair<Schema, Schema> avroKeyPayloadSchema(String namespace, String
118126
return new Pair<>(null, avro(namespace, payloadSchemaName, dataType));
119127
}
120128

121-
RelDataTypeFactory typeFactory = new SqlTypeFactoryImpl(RelDataTypeSystem.DEFAULT);
129+
RelDataTypeFactory typeFactory = new SqlTypeFactoryImpl(HoptimatorTypeSystem.INSTANCE);
122130
RelDataTypeFactory.Builder keyBuilder = new RelDataTypeFactory.Builder(typeFactory);
123131
RelDataTypeFactory.Builder payloadBuilder = new RelDataTypeFactory.Builder(typeFactory);
124132

@@ -289,8 +297,11 @@ public static RelDataType rel(Schema schema, RelDataTypeFactory typeFactory, boo
289297
case LONG:
290298
if (schema.getLogicalType() != null) {
291299
String logicalName = schema.getLogicalType().getName();
292-
if ("timestamp-millis".equals(logicalName) || "timestamp-micros".equals(logicalName)) {
293-
return createRelType(typeFactory, SqlTypeName.TIMESTAMP, nullable);
300+
if ("timestamp-millis".equals(logicalName)) {
301+
return createRelType(typeFactory, SqlTypeName.TIMESTAMP, MILLIS_PRECISION, nullable);
302+
}
303+
if ("timestamp-micros".equals(logicalName)) {
304+
return createRelType(typeFactory, SqlTypeName.TIMESTAMP, MICROS_PRECISION, nullable);
294305
}
295306
}
296307
return createRelType(typeFactory, SqlTypeName.BIGINT, nullable);
@@ -335,7 +346,7 @@ public static RelDataType rel(Schema schema, RelDataTypeFactory typeFactory, boo
335346
}
336347

337348
public static RelDataType rel(Schema schema) {
338-
return rel(schema, new SqlTypeFactoryImpl(RelDataTypeSystem.DEFAULT));
349+
return rel(schema, new SqlTypeFactoryImpl(HoptimatorTypeSystem.INSTANCE));
339350
}
340351

341352
private static RelDataType createRelType(RelDataTypeFactory typeFactory, SqlTypeName typeName, boolean nullable) {
@@ -349,7 +360,7 @@ private static RelDataType createRelType(RelDataTypeFactory typeFactory, SqlType
349360
}
350361

351362
public static RelProtoDataType proto(Schema schema) {
352-
return RelDataTypeImpl.proto(rel(schema, new SqlTypeFactoryImpl(RelDataTypeSystem.DEFAULT)));
363+
return RelDataTypeImpl.proto(rel(schema, new SqlTypeFactoryImpl(HoptimatorTypeSystem.INSTANCE)));
353364
}
354365

355366
private static String describe(RelDataTypeField dataType) {

‎hoptimator-avro/src/main/java/com/linkedin/hoptimator/avro/AvroTableValidator.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public void validate(Issues issues, DeploymentContext context) {
5353
}
5454

5555
private void validate(SchemaPlus schema, Table table, Table originalTable, Issues issues) {
56-
RelDataTypeFactory typeFactory = new JavaTypeFactoryImpl();
56+
RelDataTypeFactory typeFactory = new JavaTypeFactoryImpl(HoptimatorTypeSystem.INSTANCE);
5757
RelDataType rowType = table.getRowType(typeFactory);
5858
RelDataType originalRowType = originalTable.getRowType(typeFactory);
5959
Schema avroSchema = AvroConverter.avro("ns", "n", rowType);
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.linkedin.hoptimator.avro;
2+
3+
import org.apache.calcite.rel.type.RelDataTypeSystem;
4+
import org.apache.calcite.rel.type.RelDataTypeSystemImpl;
5+
import org.apache.calcite.sql.type.SqlTypeName;
6+
7+
8+
/**
9+
* Hoptimator's {@link RelDataTypeSystem}. It behaves exactly like Calcite's default system except
10+
* that it raises the maximum precision of the datetime types from Calcite's hard-coded
11+
* {@link SqlTypeName#MAX_DATETIME_PRECISION} (3, i.e. milliseconds) up to 9 (nanoseconds), matching
12+
* what Flink SQL supports. Without this, {@code createSqlType(TIMESTAMP, 6)} — as produced for an
13+
* Avro {@code timestamp-micros} column — is silently clamped down to {@code TIMESTAMP(3)}, so
14+
* micro/nanosecond precision could never survive.
15+
*
16+
* <p>Exposed via a public {@link #INSTANCE} so it can be named through the {@code typeSystem} Calcite
17+
* connection property (see {@code HoptimatorDriver}), which routes the whole planner/validator
18+
* through it. Nothing is forced to a higher precision; the ceiling is merely raised.
19+
*/
20+
public final class HoptimatorTypeSystem extends RelDataTypeSystemImpl {
21+
22+
public static final RelDataTypeSystem INSTANCE = new HoptimatorTypeSystem();
23+
24+
// Flink SQL supports TIMESTAMP(0) through TIMESTAMP(9).
25+
private static final int MAX_DATETIME_PRECISION = 9;
26+
27+
@Override
28+
public int getMaxPrecision(SqlTypeName typeName) {
29+
switch (typeName) {
30+
case TIME:
31+
case TIME_WITH_LOCAL_TIME_ZONE:
32+
case TIMESTAMP:
33+
case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
34+
return MAX_DATETIME_PRECISION;
35+
default:
36+
return super.getMaxPrecision(typeName);
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)