Skip to content

Commit e1eaaeb

Browse files
committed
Add support for TIMESTAMP types in Exasol connector
1 parent 59f6866 commit e1eaaeb

File tree

4 files changed

+565
-14
lines changed

4 files changed

+565
-14
lines changed

docs/src/main/sphinx/connector/exasol.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ Trino data type mapping:
103103
* - `HASHTYPE`
104104
- `VARBINARY`
105105
-
106+
* - `TIMESTAMP(n)`
107+
- `TIMESTAMP(n)`
108+
-
109+
* - `TIMESTAMP(n) WITH LOCAL TIME ZONE`
110+
- `TIMESTAMP(n) WITH TIME ZONE`
111+
-
106112
:::
107113

108114
No other types are supported.

plugin/trino-exasol/src/main/java/io/trino/plugin/exasol/ExasolClient.java

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
import io.trino.plugin.jdbc.JdbcTypeHandle;
3131
import io.trino.plugin.jdbc.LongReadFunction;
3232
import io.trino.plugin.jdbc.LongWriteFunction;
33+
import io.trino.plugin.jdbc.ObjectReadFunction;
34+
import io.trino.plugin.jdbc.ObjectWriteFunction;
3335
import io.trino.plugin.jdbc.QueryBuilder;
3436
import io.trino.plugin.jdbc.SliceReadFunction;
3537
import io.trino.plugin.jdbc.SliceWriteFunction;
@@ -43,34 +45,57 @@
4345
import io.trino.spi.connector.ColumnPosition;
4446
import io.trino.spi.connector.ConnectorSession;
4547
import io.trino.spi.connector.ConnectorTableMetadata;
48+
import io.trino.spi.type.LongTimestamp;
49+
import io.trino.spi.type.LongTimestampWithTimeZone;
50+
import io.trino.spi.type.TimeZoneKey;
51+
import io.trino.spi.type.TimestampType;
52+
import io.trino.spi.type.TimestampWithTimeZoneType;
4653
import io.trino.spi.type.Type;
4754

4855
import java.sql.Connection;
4956
import java.sql.Date;
57+
import java.sql.PreparedStatement;
58+
import java.sql.SQLException;
59+
import java.sql.Timestamp;
5060
import java.sql.Types;
61+
import java.time.Instant;
5162
import java.time.LocalDate;
63+
import java.time.LocalDateTime;
5264
import java.util.HexFormat;
5365
import java.util.List;
5466
import java.util.Map;
5567
import java.util.Optional;
5668
import java.util.OptionalLong;
5769
import java.util.Set;
70+
import java.util.TimeZone;
5871
import java.util.function.BiFunction;
5972

73+
import static com.google.common.base.Preconditions.checkArgument;
74+
import static io.trino.plugin.jdbc.PredicatePushdownController.FULL_PUSHDOWN;
6075
import static io.trino.plugin.jdbc.StandardColumnMappings.bigintColumnMapping;
6176
import static io.trino.plugin.jdbc.StandardColumnMappings.booleanColumnMapping;
6277
import static io.trino.plugin.jdbc.StandardColumnMappings.decimalColumnMapping;
6378
import static io.trino.plugin.jdbc.StandardColumnMappings.defaultCharColumnMapping;
6479
import static io.trino.plugin.jdbc.StandardColumnMappings.defaultVarcharColumnMapping;
6580
import static io.trino.plugin.jdbc.StandardColumnMappings.doubleColumnMapping;
81+
import static io.trino.plugin.jdbc.StandardColumnMappings.fromLongTrinoTimestamp;
82+
import static io.trino.plugin.jdbc.StandardColumnMappings.fromTrinoTimestamp;
6683
import static io.trino.plugin.jdbc.StandardColumnMappings.integerColumnMapping;
6784
import static io.trino.plugin.jdbc.StandardColumnMappings.smallintColumnMapping;
85+
import static io.trino.plugin.jdbc.StandardColumnMappings.toLongTrinoTimestamp;
86+
import static io.trino.plugin.jdbc.StandardColumnMappings.toTrinoTimestamp;
6887
import static io.trino.plugin.jdbc.TypeHandlingJdbcSessionProperties.getUnsupportedTypeHandling;
6988
import static io.trino.plugin.jdbc.UnsupportedTypeHandling.CONVERT_TO_VARCHAR;
7089
import static io.trino.spi.StandardErrorCode.NOT_SUPPORTED;
7190
import static io.trino.spi.connector.ConnectorMetadata.MODIFYING_ROWS_MESSAGE;
91+
import static io.trino.spi.type.DateTimeEncoding.packDateTimeWithZone;
92+
import static io.trino.spi.type.DateTimeEncoding.unpackMillisUtc;
7293
import static io.trino.spi.type.DateType.DATE;
7394
import static io.trino.spi.type.DecimalType.createDecimalType;
95+
import static io.trino.spi.type.TimestampType.createTimestampType;
96+
import static io.trino.spi.type.TimestampWithTimeZoneType.createTimestampWithTimeZoneType;
97+
import static io.trino.spi.type.Timestamps.NANOSECONDS_PER_MILLISECOND;
98+
import static io.trino.spi.type.Timestamps.PICOSECONDS_PER_NANOSECOND;
7499
import static io.trino.spi.type.VarbinaryType.VARBINARY;
75100
import static java.lang.String.format;
76101
import static java.util.Locale.ENGLISH;
@@ -79,11 +104,15 @@
79104
public class ExasolClient
80105
extends BaseJdbcClient
81106
{
107+
private static final int EXASOL_TIMESTAMP_WITH_TIMEZONE = 124;
108+
82109
private static final Set<String> INTERNAL_SCHEMAS = ImmutableSet.<String>builder()
83110
.add("EXA_STATISTICS")
84111
.add("SYS")
85112
.build();
86113

114+
private static final int MAX_EXASOL_TIMESTAMP_PRECISION = 9;
115+
87116
@Inject
88117
public ExasolClient(
89118
BaseJdbcConfig config,
@@ -241,6 +270,10 @@ public Optional<ColumnMapping> toColumnMapping(ConnectorSession session, Connect
241270
return Optional.of(defaultVarcharColumnMapping(typeHandle.requiredColumnSize(), true));
242271
case Types.DATE:
243272
return Optional.of(dateColumnMapping());
273+
case Types.TIMESTAMP:
274+
return Optional.of(timestampColumnMapping(typeHandle));
275+
case EXASOL_TIMESTAMP_WITH_TIMEZONE:
276+
return Optional.of(timestampWithTimeZoneColumnMapping(typeHandle));
244277
}
245278

246279
if (getUnsupportedTypeHandling(session) == CONVERT_TO_VARCHAR) {
@@ -256,6 +289,257 @@ private boolean isHashType(JdbcTypeHandle typeHandle)
256289
&& typeHandle.jdbcTypeName().get().equalsIgnoreCase("HASHTYPE");
257290
}
258291

292+
private static ColumnMapping timestampColumnMapping(JdbcTypeHandle typeHandle)
293+
{
294+
int timestampPrecision = typeHandle.requiredDecimalDigits();
295+
TimestampType timestampType = createTimestampType(timestampPrecision);
296+
if (timestampType.isShort()) {
297+
return ColumnMapping.longMapping(
298+
timestampType,
299+
longTimestampReadFunction(timestampType),
300+
longTimestampWriteFunction(timestampType),
301+
FULL_PUSHDOWN);
302+
}
303+
return ColumnMapping.objectMapping(
304+
timestampType,
305+
objectTimestampReadFunction(timestampType),
306+
objectTimestampWriteFunction(timestampType),
307+
FULL_PUSHDOWN);
308+
}
309+
310+
private static LongReadFunction longTimestampReadFunction(TimestampType timestampType)
311+
{
312+
return (resultSet, columnIndex) -> {
313+
Timestamp timestamp = resultSet.getObject(columnIndex, Timestamp.class);
314+
return toTrinoTimestamp(timestampType, timestamp.toLocalDateTime());
315+
};
316+
}
317+
318+
private static ObjectReadFunction objectTimestampReadFunction(TimestampType timestampType)
319+
{
320+
verifyObjectTimestampPrecision(timestampType);
321+
return ObjectReadFunction.of(
322+
LongTimestamp.class,
323+
(resultSet, columnIndex) -> {
324+
Timestamp timestamp = resultSet.getObject(columnIndex, Timestamp.class);
325+
return toLongTrinoTimestamp(timestampType, timestamp.toLocalDateTime());
326+
});
327+
}
328+
329+
private static void verifyObjectTimestampPrecision(TimestampType timestampType)
330+
{
331+
int precision = timestampType.getPrecision();
332+
checkArgument(precision > TimestampType.MAX_SHORT_PRECISION && precision <= MAX_EXASOL_TIMESTAMP_PRECISION,
333+
"Precision is out of range: %s", precision);
334+
}
335+
336+
private static ObjectWriteFunction objectTimestampWriteFunction(TimestampType timestampType)
337+
{
338+
int precision = timestampType.getPrecision();
339+
verifyObjectTimestampPrecision(timestampType);
340+
341+
return new ObjectWriteFunction() {
342+
@Override
343+
public Class<?> getJavaType()
344+
{
345+
return LongTimestamp.class;
346+
}
347+
348+
@Override
349+
public void set(PreparedStatement statement, int index, Object value)
350+
throws SQLException
351+
{
352+
LocalDateTime localDateTime = fromLongTrinoTimestamp((LongTimestamp) value, precision);
353+
Timestamp timestamp = Timestamp.valueOf(localDateTime);
354+
statement.setObject(index, timestamp);
355+
}
356+
357+
@Override
358+
public String getBindExpression()
359+
{
360+
return getTimestampBindExpression(timestampType);
361+
}
362+
363+
@Override
364+
public void setNull(PreparedStatement statement, int index)
365+
throws SQLException
366+
{
367+
statement.setNull(index, Types.VARCHAR);
368+
}
369+
};
370+
}
371+
372+
private static LongWriteFunction longTimestampWriteFunction(TimestampType timestampType)
373+
{
374+
return new LongWriteFunction()
375+
{
376+
@Override
377+
public String getBindExpression()
378+
{
379+
return getTimestampBindExpression(timestampType);
380+
}
381+
382+
@Override
383+
public void set(PreparedStatement statement, int index, long epochMicros)
384+
throws SQLException
385+
{
386+
LocalDateTime localDateTime = fromTrinoTimestamp(epochMicros);
387+
Timestamp timestampValue = Timestamp.valueOf(localDateTime);
388+
statement.setObject(index, timestampValue);
389+
}
390+
391+
@Override
392+
public void setNull(PreparedStatement statement, int index)
393+
throws SQLException
394+
{
395+
statement.setNull(index, Types.VARCHAR);
396+
}
397+
};
398+
}
399+
400+
private static ColumnMapping timestampWithTimeZoneColumnMapping(JdbcTypeHandle typeHandle)
401+
{
402+
int timestampPrecision = typeHandle.requiredDecimalDigits();
403+
TimestampWithTimeZoneType timestampWithTimeZoneType = createTimestampWithTimeZoneType(timestampPrecision);
404+
405+
if (timestampWithTimeZoneType.isShort()) {
406+
return ColumnMapping.longMapping(
407+
timestampWithTimeZoneType,
408+
longTimestampWithTimeZoneReadFunction(),
409+
longTimestampWithTimeZoneWriteFunction(timestampWithTimeZoneType),
410+
FULL_PUSHDOWN);
411+
}
412+
return ColumnMapping.objectMapping(
413+
timestampWithTimeZoneType,
414+
objectTimestampWithTimeZoneReadFunction(timestampWithTimeZoneType),
415+
objectTimestampWithTimeZoneWriteFunction(timestampWithTimeZoneType),
416+
FULL_PUSHDOWN);
417+
}
418+
419+
private static LongReadFunction longTimestampWithTimeZoneReadFunction()
420+
{
421+
return (resultSet, columnIndex) -> {
422+
Timestamp timestamp = resultSet.getObject(columnIndex, Timestamp.class);
423+
return packDateTimeWithZone(timestamp.getTime(), TimeZone.getDefault().getID());
424+
};
425+
}
426+
427+
private static ObjectReadFunction objectTimestampWithTimeZoneReadFunction(
428+
TimestampWithTimeZoneType timestampType)
429+
{
430+
verifyObjectTimestampWithTimeZonePrecision(timestampType);
431+
return ObjectReadFunction.of(
432+
LongTimestampWithTimeZone.class,
433+
(resultSet, columnIndex) -> {
434+
Timestamp timestamp = resultSet.getObject(columnIndex, Timestamp.class);
435+
436+
long millisUtc = timestamp.getTime();
437+
long nanosUtc = millisUtc * NANOSECONDS_PER_MILLISECOND + timestamp.getNanos() % NANOSECONDS_PER_MILLISECOND;
438+
int picosOfMilli = (int) ((nanosUtc - millisUtc * NANOSECONDS_PER_MILLISECOND) * PICOSECONDS_PER_NANOSECOND);
439+
440+
return LongTimestampWithTimeZone.fromEpochMillisAndFraction(
441+
millisUtc,
442+
picosOfMilli,
443+
TimeZoneKey.getTimeZoneKey(TimeZone.getDefault().getID()));
444+
});
445+
}
446+
447+
private static void verifyObjectTimestampWithTimeZonePrecision(TimestampWithTimeZoneType timestampType)
448+
{
449+
int precision = timestampType.getPrecision();
450+
checkArgument(precision > TimestampWithTimeZoneType.MAX_SHORT_PRECISION && precision <= MAX_EXASOL_TIMESTAMP_PRECISION,
451+
"Precision is out of range: %s", precision);
452+
}
453+
454+
private static ObjectWriteFunction objectTimestampWithTimeZoneWriteFunction(TimestampWithTimeZoneType timestampType)
455+
{
456+
verifyObjectTimestampWithTimeZonePrecision(timestampType);
457+
458+
return new ObjectWriteFunction() {
459+
@Override
460+
public Class<?> getJavaType()
461+
{
462+
return LongTimestampWithTimeZone.class;
463+
}
464+
465+
@Override
466+
public void set(PreparedStatement statement, int index, Object value)
467+
throws SQLException
468+
{
469+
LongTimestampWithTimeZone longTimestampWithTimeZone = (LongTimestampWithTimeZone) value;
470+
Instant instant = Instant.ofEpochMilli(longTimestampWithTimeZone.getEpochMillis())
471+
.plusNanos(longTimestampWithTimeZone.getPicosOfMilli() / PICOSECONDS_PER_NANOSECOND);
472+
Timestamp timestamp = Timestamp.from(instant);
473+
statement.setObject(index, timestamp);
474+
}
475+
476+
@Override
477+
public String getBindExpression()
478+
{
479+
return getTimestampWithTimeZoneBindExpression(timestampType);
480+
}
481+
482+
@Override
483+
public void setNull(PreparedStatement statement, int index)
484+
throws SQLException
485+
{
486+
statement.setNull(index, Types.VARCHAR);
487+
}
488+
};
489+
}
490+
491+
private static LongWriteFunction longTimestampWithTimeZoneWriteFunction(TimestampWithTimeZoneType timestampType)
492+
{
493+
return new LongWriteFunction()
494+
{
495+
@Override
496+
public String getBindExpression()
497+
{
498+
return getTimestampWithTimeZoneBindExpression(timestampType);
499+
}
500+
501+
@Override
502+
public void set(PreparedStatement statement, int index, long dateTimeWithTimeZone)
503+
throws SQLException
504+
{
505+
long epochMillis = unpackMillisUtc(dateTimeWithTimeZone);
506+
Timestamp timestampValue = Timestamp.from(Instant.ofEpochMilli(epochMillis));
507+
statement.setObject(index, timestampValue);
508+
}
509+
510+
@Override
511+
public void setNull(PreparedStatement statement, int index)
512+
throws SQLException
513+
{
514+
statement.setNull(index, Types.VARCHAR);
515+
}
516+
};
517+
}
518+
519+
private static String getTimestampBindExpression(TimestampType timestampType)
520+
{
521+
return getTimestampBindExpression(timestampType.getPrecision());
522+
}
523+
524+
private static String getTimestampWithTimeZoneBindExpression(TimestampWithTimeZoneType timestampWithTimeZoneType)
525+
{
526+
return getTimestampBindExpression(timestampWithTimeZoneType.getPrecision());
527+
}
528+
529+
/**
530+
* Returns a {@code TO_TIMESTAMP} bind expression using the appropriate format model
531+
* based on the given fractional seconds precision.
532+
* See for more details: https://docs.exasol.com/db/latest/sql_references/formatmodels.htm
533+
*/
534+
private static String getTimestampBindExpression(int precision)
535+
{
536+
//negative precisions are not supported by TimestampType and TimestampWithTimeZoneType
537+
if (precision == 0) {
538+
return "TO_TIMESTAMP(?, 'YYYY-MM-DD HH24:MI:SS')";
539+
}
540+
return format("TO_TIMESTAMP(?, 'YYYY-MM-DD HH24:MI:SS.FF%d')", precision);
541+
}
542+
259543
private static ColumnMapping dateColumnMapping()
260544
{
261545
// Exasol driver does not support LocalDate

0 commit comments

Comments
 (0)